20200708のRailsに関する記事は23件です。

gemでインストールしたrailsコマンドがなぜ使えるのか??

はじめに

※windowsのローカル環境下です。※

表題の通り、気になったので調べました。

bin/railsで実行するかrailsで実行するか??


まずは事前知識として、bin/railsで実行するかrailsで実行するか??

結論、どちらを使っても基本問題ないですが、参照元が違います。

binをつけるとプロジェクトディレクトリを参照。

binを付けないとローカル(グローバル)のrailsを参照。

続いて本題。

gemでインストールしたrailsコマンドがなぜ使えるのか??


railsコマンドはローカル環境変数でPath通してるはず。。。

→これはとても浅い勘違い!!

railsは以下のようにgemでインストールしています。

$ gem install rails -v "5.2.3"
$ rails -v
Rails 5.2.3

では、なぜgemでインストールしたrailsでrailsコマンドが通るんだろう??

調べました。

環境変数には以下があります。

C:\Ruby26-x64\bin

ここを確認するとこんな感じ。
$ ls Ruby26-x64/bin
_guard-core*             coderay.bat  nokogiri*     rails.bat  ruby_builtin_dlls/  spring*
_guard-core.bat          erb*         nokogiri.bat  rake*      rubyw.exe*          spring.bat
bundle.cmd               erb.cmd      pry*          rake.bat   sass*               sprockets*
bundler.cmd              gem*         pry.bat       rake.cmd   sass.bat            sprockets.bat
byebug*                  gem.cmd      puma*         rdoc*      sass-convert*       sqlite3.def
byebug.bat               guard*       puma.bat      rdoc.cmd   sass-convert.bat    sqlite3.dll*
chromedriver-helper*     guard.bat    pumactl*      ri*        scss*               thor*
chromedriver-helper.bat  irb*         pumactl.bat   ri.cmd     scss.bat            thor.bat
chromedriver-update*     irb.cmd      rackup*       ridk.cmd   setrbvars.cmd       tilt*
chromedriver-update.bat  listen*      rackup.bat    ridk.ps1   slimrb*             tilt.bat
coderay*                 listen.bat   rails*        ruby.exe*  slimrb.bat          x64-msvcrt-ruby260.dll*

railsがありました。これを参照しているっぽい。

ですが確証が得られないのでさらに調べる。

gemsでインストールした各種ライブラリは以下にあります。

C:\Ruby26-x64\lib\ruby\gems\2.6.0\gems

rails 5.2.3の中身を確認
$ ls Ruby26-x64/lib/ruby/gems/2.6.0/gems/rails-5.2.3
README.md

あれ??どうやって参照している??

whichコマンドを使ってみる。

$ which rails
/c/Ruby26-x64/bin/rails

解決しました◎

やはりRuby直下のbinディレクトリでrailsは参照されていました。

ちなみに、なんで参照されるか。。。

どうやらwhichコマンドは以下のルールらしい。

whichコマンドは、環境変数のPATHに設定されているディレクトリ順に調べ、最初に見つかったコマンドを表示

whichコマンドがこのような参照方法であればコマンドもそうであるはず!!

合点✌

備考:bundle installする際にパスを指定する


指定しないで実行すると前述のとおり、以下にインストールされます。
C:\Ruby26-x64\lib\ruby\gems\2.6.0\gems

パスを指定する方法は以下
bundle install --path vendor/bundle

これでプロジェクト直下のvendor/bundleにインストールできます。

ちなみにrails new時デフォルトでbundle installも実行されますが、Bオプションを付けることで実行スキップできます。

rails new rails_app -B

一度オプションを付けてインストールすれば、以下に設定が保存されて以降は指定不要。

プロジェクト/.bundle/config

---
BUNDLE_PATH: "vendor/bundle"

参考


Rails 4.1以降のコンソールコマンドは必ず bin/ を付けなきゃいけないの?

コマンドの場所を調べるには

railsでbundle installする時にインストールパスを指定

おわりに


日ごろからローカル環境の扱いにはもっと意識を向けていきたいと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

☆初投稿:TECH CAMP学習、個人アプリ作成①、駆け出しエンジニア

TECH CAMPに通い出して

初めて投稿させていただきました。
TECH CAMPの夜間コースに通い出して3ヶ月が経ちました。
カリキュラムではわからないことも沢山あり、その都度Googleで調べたり、メンターさんに質問したりと、苦労も沢山ありました。エラーを繰り返しながらも、無事に動いたときというのは何とも言えない喜びもありました。

諸々、何とかこなし、いよいよ個人アプリ開発となりました。

正直、現在も試行錯誤の連続でありますが、やはり自分のやっていることというのは、時間がたつと忘れてしまうこともありますので、自分が苦労して解決したことなどは記録していった方がよいと考え、投稿させていただきました。

まだまだ不慣れであり、かつ知識も未熟なため、書いてあることが間違っていることもあるかもしれませんが、はっきりいって投稿することに意味があるのだと思いましたので、間違っているところははっきり指摘していただけたら嬉しいです。

個人アプリの作成時、viewを表示させようとしたのですが
まずは以下のエラーがありました。

image.png

原因はhamlの導入ができていませんでした。
なのでGemfileに

『gem 'haml-rails'』

を入力して
ターミナルにて

『bundle install』

を実行、その後すでにあるerbをhamlに変換

『rails haml:erb2haml』

すると「would you like〜」と出てくるので「y」を入力
これでエラーは解決しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでdeviseを追加したのに反映されなかったときの対処法

dockerで環境構築を行いgemの追加をした際にエラーが発生しましたので
備忘録として情報共有しようと思います!

開発環境

docker
rails (5.2.0)
ruby (2.7.1)
mysql (5.7)
nginx
puma

※docker-compose buildでgemの更新をしますが時間がかかるので下記の記事を参考に
gemの更新を素早くする設定をしています。
 https://qiita.com/neko-neko/items/abe912eba9c113fd527e

エラー内容

Gemfileでdeviseのgemを追加し、docker-compose run --rm rails bundle installをしたところ以下のエラーが発生

~ RailsApp % docker-compose run --rm rails bundle install              #bundle installを実行
Starting railsapp_db_1 ... done
Fetching gem metadata from https://rubygems.org/.............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies....
Bundler could not find compatible versions for gem "railties":         # エラー発生
  In snapshot (Gemfile.lock):
    railties (= 5.2.4.3)

  In Gemfile:
    devise (= 4.1.0) was resolved to 4.1.0, which depends on
      railties (< 5.1, >= 4.1.0)

    rails (~> 5.2.2) was resolved to 5.2.4.3, which depends on
      railties (= 5.2.4.3)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

何が原因なのかわからなかったが、とりあえず「bundle updateしたら解決するかもよ」と書いてあったので
docker-compose run --rm rails bundle updateを実行

↓↓↓↓↓↓↓↓

~ RailsApp % docker-compose run --rm rails bundle update           #bundle updateを実行
Starting railsapp_db_1 ... done
Fetching gem metadata from https://rubygems.org/.............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...........................
Bundler could not find compatible versions for gem "railties":      # 同じエラーが発生
  In snapshot (Gemfile.lock):
    railties (= 5.2.4.3)

  In Gemfile:
    devise (= 4.1.0) was resolved to 4.1.0, which depends on
      railties (< 5.1, >= 4.1.0)

    rails (~> 5.2.2) was resolved to 5.2.4.3, which depends on
      railties (= 5.2.4.3)

ダメでしたね・・・

原因

結論から申し上げるとdeviseというgemがlistにないことがエラーの原因だったみたいです。


下記の記事を参考にさせていただきました。
https://qiita.com/hatorijobs/items/2928e152f22d009b07d0

こちらの記事をそのまま実行すれば解決しますが、dockerで作業している方は若干、コマンドが
異なりますのでこちらで説明させていただこうと思います。

対処方法

以下の順に操作をしていきます。

1. docker-compose exec [任意サービス名] gem list でGemの一覧を確認し、deviseのgemがないことを確認する。
2. docker-compose exec [任意サービス名] gem install devise でdeviseをインストールする。
3. もう一度docker-compose exec [任意のサービス名] gem list でGemの一覧を確認し、devise(4.7.2)があることを確認する。
4. Gemfileに**gem 'devise','4.7.2'を記述する
5. docker-compose run --rm rails bundle updateを実行

順番に見ていきましょう。
↓↓↓↓↓↓↓↓

1. docker-compose exec [任意サービス名] gem list でGemの一覧を確認し、deviseのgemがないことを確認する。

~ RailsApp % docker-compose exec app gem list

*** LOCAL GEMS ***

actioncable (5.2.4.3, 5.2.2)
actionmailer (5.2.4.3, 5.2.2)
actionpack (5.2.4.3, 5.2.2)
actionview (5.2.4.3, 5.2.2)
actionjob (5.2.4.3, 5.2.2)

~省略~

web-console (3.7.0)

2. docker-compose exec [任意サービス名] gem install devise でdeviseをインストールする。

~ RailsApp % docker-compose exec app gem install devise
Fetching warden-1.2.8.gem
Fetching bcrypt-3.1.13.gem

~省略~

Successfully installed devise-4.7.2
5 gems installed

3. もう一度docker-compose exec [任意のサービス名] gem list でGemの一覧を確認し、devise(4.7.2)があることを確認する。

~ RailsApp % docker-compose exec app gem list

*** LOCAL GEMS ***

actioncable (5.2.4.3, 5.2.2)
actionmailer (5.2.4.3, 5.2.2)
actionpack (5.2.4.3, 5.2.2)
actionview (5.2.4.3, 5.2.2)
actionjob (5.2.4.3, 5.2.2)

~省略~

devise (4.7.2)

~省略~

web-console (3.7.0)

4. Gemfileにgem 'devise','4.7.2'を記述する

Gemfile.
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.0', '>= 5.0.2.1'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.3.18', '< 0.5'
# Use Puma as the app server

~省略~

#ログイン機能の追加
gem 'devise','4.7.2'

~省略~

5. docker-compose run --rm rails bundle updateを実行

~ RailsApp % docker-compose run --rm rails bundle update

これでエラーが出なければ我々の勝ちです

気づいたこと

エラー対応している際に気づいたことがいくつかあったので共有させていただきます

docker-compose exec app gem install deviseを実行すると最新版のgemがインストールされます。
最新版でも特に問題ないと判断したため、バージョン指定する方法の説明は省略させていただきました。
※逆に方法がわかる方は教えてください!

ちなみにdeviseの全バージョン履歴が掲載されたサイトを見つけましたのでURLを連携させていただきます。
https://rubygems.org/gems/devise/versions
※2020.06.10時点での最新バージョンは4.7.2


[任意のサービス名]とはdocker-compose.ymlで指定しているサービス名のことです。
任意で決めることができますが、どのサイトでもdbとかappとかwebて設定している方が多い印象です。

※わからないて方は、「docker-compose.yml サービス名」で調べると記事がヒットすると思います。

終わりに

Railsやdockerについては今後もアウトプットのためにエラーに関する記事等を定期的に投稿していこうと思っております。
また、初めての投稿で至らない点があったかと思いますが、最後まで読んでいただきありがとうございました!!!

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでdeviseを追加したいのにbundle installができない

dockerで環境構築を行いgemの追加をした際にエラーが発生しましたので
備忘録として情報共有しようと思います!

開発環境

docker
rails (5.2.0)
ruby (2.7.1)
mysql (5.7)
nginx
puma

※docker-compose buildでgemの更新をしますが時間がかかるので下記の記事を参考に
gemの更新を素早くする設定をしています。
 https://qiita.com/neko-neko/items/abe912eba9c113fd527e

エラー内容

Gemfileでdeviseのgemを追加し、docker-compose run --rm rails bundle installをしたところ以下のエラーが発生

~ RailsApp % docker-compose run --rm rails bundle install              
Starting railsapp_db_1 ... done
Fetching gem metadata from https://rubygems.org/.............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies....
Bundler could not find compatible versions for gem "railties":         
  In snapshot (Gemfile.lock):
    railties (= 5.2.4.3)

  In Gemfile:
    devise (= 4.1.0) was resolved to 4.1.0, which depends on
      railties (< 5.1, >= 4.1.0)

    rails (~> 5.2.2) was resolved to 5.2.4.3, which depends on
      railties (= 5.2.4.3)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

何が原因なのかわからなかったが、とりあえず「bundle updateしたら解決するかもよ」と書いてあったので
docker-compose run --rm rails bundle updateを実行

↓↓↓↓↓↓↓↓

~ RailsApp % docker-compose run --rm rails bundle update           
Starting railsapp_db_1 ... done
Fetching gem metadata from https://rubygems.org/.............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...........................
Bundler could not find compatible versions for gem "railties":      
  In snapshot (Gemfile.lock):
    railties (= 5.2.4.3)

  In Gemfile:
    devise (= 4.1.0) was resolved to 4.1.0, which depends on
      railties (< 5.1, >= 4.1.0)

    rails (~> 5.2.2) was resolved to 5.2.4.3, which depends on
      railties (= 5.2.4.3)

ダメでしたね・・・

原因

結論から申し上げるとdeviseというgemがlistにないことがエラーの原因だったみたいです。


下記の記事を参考にさせていただきました。
https://qiita.com/hatorijobs/items/2928e152f22d009b07d0

こちらの記事をそのまま実行すれば解決しますが、dockerで作業している方は若干、コマンドが
異なりますのでこちらで説明させていただこうと思います。

対処方法

以下の順に操作をしていきます。

1. docker-compose exec [任意サービス名] gem list でGemの一覧を確認し、deviseのgemがないことを確認する。

2. docker-compose exec [任意サービス名] gem install devise でdeviseをインストールする。

3. もう一度docker-compose exec [任意のサービス名] gem list でGemの一覧を確認し、devise(4.7.2)があることを確認する。

4. Gemfileに**gem 'devise','4.7.2'を記述する

5. docker-compose run --rm rails bundle updateを実行

順番に見ていきましょう。
↓↓↓↓↓↓↓↓

1. docker-compose exec [任意サービス名] gem list でGemの一覧を確認し、deviseのgemがないことを確認する。

~ RailsApp % docker-compose exec app gem list

*** LOCAL GEMS ***

actioncable (5.2.4.3, 5.2.2)
actionmailer (5.2.4.3, 5.2.2)
actionpack (5.2.4.3, 5.2.2)
actionview (5.2.4.3, 5.2.2)
actionjob (5.2.4.3, 5.2.2)

~省略~

web-console (3.7.0)

2. docker-compose exec [任意サービス名] gem install devise でdeviseをインストールする。

~ RailsApp % docker-compose exec app gem install devise
Fetching warden-1.2.8.gem
Fetching bcrypt-3.1.13.gem

~省略~

Successfully installed devise-4.7.2
5 gems installed

3. もう一度docker-compose exec [任意のサービス名] gem list でGemの一覧を確認し、devise(4.7.2)があることを確認する。

~ RailsApp % docker-compose exec app gem list

*** LOCAL GEMS ***

actioncable (5.2.4.3, 5.2.2)
actionmailer (5.2.4.3, 5.2.2)
actionpack (5.2.4.3, 5.2.2)
actionview (5.2.4.3, 5.2.2)
actionjob (5.2.4.3, 5.2.2)

~省略~

devise (4.7.2)

~省略~

web-console (3.7.0)

4. Gemfileにgem 'devise','4.7.2'を記述する

Gemfile.
source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.0.0', '>= 5.0.2.1'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.3.18', '< 0.5'
# Use Puma as the app server

~省略~

#ログイン機能の追加
gem 'devise','4.7.2'

~省略~

5. docker-compose run --rm rails bundle updateを実行

~ RailsApp % docker-compose run --rm rails bundle update

これでエラーが出なければ我々の勝ちです

気づいたこと

エラー対応している際に気づいたことがいくつかあったので共有させていただきます

docker-compose exec app gem install deviseを実行すると最新版のgemがインストールされます。
最新版でも特に問題ないと判断したため、バージョン指定する方法の説明は省略させていただきました。
※逆に方法がわかる方は教えてください!

ちなみにdeviseの全バージョン履歴が掲載されたサイトを見つけましたのでURLを連携させていただきます。
https://rubygems.org/gems/devise/versions
※2020.06.10時点での最新バージョンは4.7.2


[任意のサービス名]とはdocker-compose.ymlで指定しているサービス名のことです。
任意で決めることができますが、どのサイトでもdbとかappとかwebて設定している方が多い印象です。

※わからないて方は、「docker-compose.yml サービス名」で調べると記事がヒットすると思います。

終わりに

Railsやdockerについては今後もアウトプットのためにエラーに関する記事等を定期的に投稿していこうと思っております。
また、初めての投稿で至らない点があったかと思いますが、最後まで読んでいただきありがとうございました!!!

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerでrails+mySQLの環境構築したけどlocalhost:3000にアクセス出来なくて困ってたら驚愕の事実が発覚した話

こちらの記事を参考に、docker-composeでrails+mySQLの環境構築をしてみた。

DBの作成まですんなり行き「docker最高!!!!!!!!」みたいになってたが、localhost:3000にアクセスすると「このページは動作していません。」と返ってくる…

うーわ、最悪

環境構築には嫌な思い出しかない。
一度virtualboxとvagrantで環境構築したとき無限にエラーが出続けて「これはもう神がプログラミングをやめろと言っているのでは???」みたいになって以来、環境構築という単語を聞いただけで寒気がしてくる。

まあプログラミングの学習をする上で避けては通れない道なのでやるしかない。

とりあえずコンテナ内のサーバーが起動しているか確認。

$ docker exec -it コンテナ名 bash
**** # curl http://localhost:3000/

コンテナの中に入ってlocalhost:3000にアクセスしてみると普通にHTMLが返ってくる。こっちは問題なさそう。

次にログを見てみる。

docker logs ***_***_web

見たけど特にエラーは出ていない。
もう一度コンテナ内からアクセスしたのちログをみると、さっきと同様のログが一つ増えていたので多分このログはコンテナ内からアクセスしたものっぽい。

ブラウザからアクセスしてもログが残っていないということは、そもそもブラウザからWebコンテナの3000ポートまでリクエストが到達してないってことらしい。

なるほどね……………ドユコト???クッソ、横文字ばっか並べやがって…

その後も格闘は続いた。ありとあらゆるサイトを読み漁り、英語を必死に読解し、嫌になってスプラトゥーンをやって、YouTubeを見て、twitterを見て…とかなんとかしているうちに夜になっていた。

もう諦めようか…そんな考えが頭をよぎり出したとき、ある記事が目に入る。
私と同じようなエラーが出て困っているようだったのだが、その人が「esetのファイアウォールを無効にしたらいけました〜」と言うているのだ。

eset??まって私もセキュリティソフトesetだわ、ウイルスバスターって名前がダサくて嫌だったから特に何も考えずに購入したeset…まさかお前が…?いや、こんな悩みに悩んだ挙句セキュリティソフトに通信遮断されてました!は流石に酷いと思わない…???ねえ、違うよね??違うって言ってよ!!!

とか思いながら恐る恐るesetのファイアウォールを無効にしてlocalhost:3000にアクセスしてみたら

image.png

で、出た〜〜〜〜〜〜〜〜〜!!!!!!!!!!!!お前、あっさり出た〜〜〜〜〜〜〜〜!!!!!!!!

くそがよ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでhtml.erbのclassを条件付きで追加する方法

概容

下記のクラスに「ある条件」の時だけクラスを追加したいとする。

<div class="container">
</div>

例えば今回はusers_controllerの時だけクラスを追加するとする。
その場合の条件式は下記のようになる。

<div class="container<%= ' user-container' if controller_name = 'users' %>">
</div>

※注意

追加したいクラス名の先頭文字の前はスペースを開けること。

正しい: <%= ' user-container' if・・・
間違い: <%= 'user-container' if・・・

このような条件付きでclassを追加することによってその条件下のみでCSSでスタイルの変更や上書きができるようになるのでぜひ覚えておきたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

productionとdevelopmentをなぜ使い分けるのか

railsになんでわざわざdevelop(開発環境)とproduction(本番環境)を使い分けているのか判明したので記録

相変わらず表面的な知識なので、誰かツッコミを入れてくれると嬉しいです。
それではいってみよーーーーーー!!!!!

development(開発環境)

  • 長所

エラーが発生すれば、詳細を表示してくれたりする。

  • 欠点

詳細を表示してまうと、ソースコード情報が外に漏れちゃうからセキュリティ的な脆弱性がある

production(本番環境)

  • 長所

エラーが発生してもコードの詳細を表示しいひんから、安全性・安定性が高い

  • 欠点

エラーが起こっても、「何か例外が発生したよ」ぐらいしか教えてくれない
(最初どうやってエラー見るねんって焦った)

まとめ

セキュリティの観点から、本番デプロイ時にはproductionでrailsを動かす必要があるんやね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]form_forを使ったselect-boxの注意点

form_forを使ったselect-boxにはrubyオプションとhtmlオプションが存在する。

双方のオプション記述場所を誤ると実装に反映されないため注意が必要!
オプションの種類に関しては公式ドキュメントを見ると簡単に理解できるであろう。

Ruby公式▼
https://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/select

html公式▼
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select

参考にしてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]form_forを使ったcelect-boxの注意点

form_forを使ったcelect-boxにはrubyオプションとhtmlオプションが存在する。

双方のオプション記述場所を誤ると実装に反映されないため注意が必要!
オプションの種類に関しては公式ドキュメントを見ると簡単に理解できるであろう。

Ruby公式▼
https://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/select

html公式▼
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select

参考にしてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker&Rails環境で、gemを永続化させる

本記事について(お断り)

この記事は、しがないプログラミング初心者がお勉強よろしくプログラミング・技術に触れるべく、行ったことの備忘録として書き綴ったものです。ご了承ください。
ご指摘は甘んじて受け入れます?‍♂️

起こったこと

とあるプロダクトでおもむろにdocker-compose upしてたら、

...
web_1  | Bundler::GemNotFound: Could not find gem 'rspec-rails' in any of the gem sources listed in your Gemfile.
...
myapp_web_1 exited with code 1

と怒って勝手に出て行ってしまった。
おかしい、確かにインストールしたはずのgemがないとは、、、

一度落として、再度docker-compose buildすると問題なく動いた。
まあこれでもいいんだけど、毎回ビルドしてコンテナ起動するのは面倒だと思う。

調べてみると、volumeの永続化を行うことで、こうした問題が解消されるとのこと。

docker-compose.ymlの修正

現在のファイルの中身。

docker-compose.yml
ersion: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

これを書き加える

docker-compose.yml
ersion: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
      - gem_data:/usr/local/bundle # <= ココと
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  gem_data: # <= ココ。

パスは、コンテナの中に入って確認できます。

$ docker-compose exec web bash
$ gem environment
RubyGems Environment:
  - RUBYGEMS VERSION: 3.0.3
  - RUBY VERSION: 2.6.5 (2019-10-01 patchlevel 114) [x86_64-linux]
  - INSTALLATION DIRECTORY: /usr/local/bundle
  ...(略)

これでgemを永続化する環境を構築できたことになります。
試しに、gemを追加、

Gemfile
+ gem 'rails-erd'

その後、bundle installしてみます。

$ docker-compose exec web bundle install

そして、一度コンテナを削除した後に再度起動してみます。

$ docker-compose down
$ docker-compose up

Rails6.png

問題なくコンテナを起動できました!

コンテナ削除前にインストールしたgemもちゃんと入っています。

$ docker-compose exec web bundle list |grep 'rails-erd'
  * rails-erd (1.6.0)

参考

今回の作業にあたり、以下のページを参考にさせていただきました。とても勉強になりました。ありがとうございます?‍♂️

https://nishinatoshiharu.com/datavolume-for-gem/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_with を使った検索フォームに初期値 value を設定する方法

筆者はRails6.0ですが、Rails5(Rails4も?)でも動くはずです。またSearchkick(Elasticsearchを簡単にするGem)を使っているので、もしかすると環境依存があるかもしれません。

最新のRailsではform_withを使った記法が推奨されています。しかしこと検索フォームの作成においては、未だに情報が少なく苦戦したのでメモ。

正解のコード

# index.html.erb

<%= form_with model: @post, method: "get", local: true do %>
    <%= text_field_tag :search, params[:search], class: "form-control ds-input", placeholder: "検索‥" %>
<% end %>

初期値valueを入れる場合はtext_fieldは使えないようです。form_withを使っているのでURLの指定は不要ですがmethod: "get", local: trueの記述は必要でした。

local: trueというのは form_with のデフォルト設定でAjaxによるHTTPリクエストを送ってしまうのをやめさせているようです(Ajaxだと画面遷移してくれないらしい)。

text_field_tagの引数たちをかっこやハッシュ?({})で囲んでる記述も見かけましたが、よくわからんがこれでいいようです。

post_controller.rb
  def index
    @posts = self.query
  end

  private
    def query
      if params[:search]
        # ここの検索方法の記述はSearchkickを使っているためこうなので、標準の検索方法に置き換えるなどしてください
        Post.search params[:search], fields: [:name, :description, :category_name]
      else
        Post.all
      end
    end

前提知識

筆者はProgateあがりですが、RubyやRailsの公式チュートリアルをやっていないせいかformまわりの基礎知識がバラバラです?もしかすると当たり前かもしれませんが、知らなかった知識を書いときます。

text_field は text_field_tag のヘルパー

フォームの中身に使うtext_fieldtext_field_tagのヘルパーであり、ヘルパーとは簡単にいうと記述を省略して書けるようにしてくれるものらしい?

フォームのコントロールの命名法や大量の属性を扱わなければならず、フォームのマークアップは作成もメンテナンスも退屈な作業になりがちです。そこでRailsでは、フォームのマークアップを生成するビューヘルパーを提供し、こうした煩雑な作業を行わないで済むようにしました
https://railsguides.jp/form_helpers.html

最初の引数はインスタンス変数名、2番目の引数はオブジェクトを呼び出すためのメソッド名

たとえば、コントローラで@personが定義されており、その人物の名前がHenryの場合

<%= text_field(:person, :name) %>
# こうなる?
<input id="person_name" name="person[name]" type="text" value="Henry"/>

なので

<%= text_field :search, params[:search] %>
# と書いてしまうと、こうなってしまう?
<input id="search_" name="search[]" type="text">

Railsの検索に search.html.erb はいらない

検索をする場合は search.html.erb を作成して、ルーティングにGETで追加して、index.html.erb と同じコードを毎回貼り付けてました(笑)コントローラー側でキーワードがあるかないかでif分岐してやればスッキリ書けます。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails5でECサイトを作る⑤ ~Customerモデル編~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る④の続きです。
ようやく機能の実装に入ります。まずは顧客サイトの親モデル周辺機能から順番に……ということで、今回はCustomerモデルのCRUDを中心に作ります。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

deviseのコントローラ

deviseで新規登録できず、何でだろうと思ったらコントローラ側にパラメータの許可を与えていなかったためと判明しました。

app/controllers/customers/registrations_controller.rb
class Customers::RegistrationsController < Devise::RegistrationsController

  before_action :authenticate_customer!
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def after_sign_up_path_for(resource)
    customer_top_path
  end

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:is_active, :first_name, :first_name_kana, :family_name, :family_name_kana, :post_code, :address, :tel])
  end

  protected
  def update_resouce(resource, parans)
    resource.update_without_password(params)
  end

end

ついでにsessions_controllerもログイン、ログアウト後の遷移先を指定しておきました。

app/controllers/customers/sessions_controller.rb
class Customers::SessionsController < Devise::SessionsController

  protected

  def after_sign_in_path_for(resource)
    customer_top_path
  end

  def after_sign_out_path_for(resource)
    customer_top_path
  end

end

Bootstrapのflashを使えるようにする

Bootstrapでは、class指定だけできれいに整ったflashメッセージを使うことができます。
部分テンプレートを作っておくとrenderで呼び出せて便利です。

app/controllers/application_controller.rb
add_flash_types :success, :danger, :info
app/helpers/application_helper.rb
def flash_class_for flash_type
  case flash_type
    when 'success' then 'alert-success'
    when 'danger' then 'alert-danger'
    when 'info' then 'alert-info'
  end
end
app/views/layouts/_flash.html.erb
<% flash.each do |type, msg| %>
  <div class="alert <%= flash_class_for(type) %> row" role="alert">
    <a class="close" data-dismiss="alert">×</a>
    <%= msg %>
  </div>
<% end %>
app/views/layouts/application.html.erb
<body>
  <%= render 'layouts/header' %>
  <div class="container-fluid">
    <%= render 'layouts/flash', flash: flash %>
    <div class="row">
      <%= yield %>
    </div>
  </div>
  <%= render 'layouts/footer' %>
</body>

Controller

多くの機能は基本的なCRUDですが、退会処理のみ異なります。

編集画面にて「退会する」ボタンを押下(withdrawアクション呼出)
↓
退会確認画面に遷移
↓
「退会する」ボタンを押下(alert出現)
↓
「はい」を押すと退会処理が行われる(withdraw_doneアクション呼出)

以上のような流れになっています。
また、退会処理はCustomerデータの削除ではなく、is_activeカラムをtrueからfalseに変更する処理としています。

app/controllers/customers_controller.rb
class CustomersController < ApplicationController

  before_action :authenticate_customer!
  before_action :set_customer
  before_action :baria_customer

  def edit
  end

  def show
  end

  def update
    @customer = current_customer
    if @customer.update(customer_params)
      redirect_to customer_path(@customer), success: 'お客様情報が更新されました!'
    else
      flash[:danger] = 'お客様の情報を更新出来ませんでした。空欄の箇所はありませんか?'
      render :edit
    end
  end

  def withdraw
  end

  def withdraw_done
    @customer = current_customer
    @customer.update(is_active: false)
    reset_session
    redirect_to customer_top_path, info: 'ありがとうございました。またのご利用を心よりお待ちしております。'
  end

  private

  def customer_params
    params.require(:customer).permit(:is_active, :first_name, :first_name_kana, :family_name, :family_name_kana, :post_code, :address, :email, :tel, cart_items_attributes: [:_destroy])
  end

  def set_customer
    @customer = current_customer
  end

  # 他者のページにアクセスしようとすると直前のページに戻る
  def baria_customer
    if params[:id].to_i != current_customer.id
      redirect_back(fallback_location: root_path)
    end
  end
end

View

Sign_up

app/views/customers/registrations/new.html.erb
<div class='col-lg-6 offset-lg-3 offset-2 space'>
  <div class='row'>
    <h2>新規会員登録</h2>
  </div>

  <div class='row'>
    <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
    <%= render 'devise/shared/error_messages', resource: resource %>


    <div class='form-group row space'>
      <div class='col-lg-4'>
        <h5><%= f.label :名前 %></h5>
      </div>
      <div class='col-lg-8'>
        <div class="row">
          <div class="col-lg-6">
            (姓)<%= f.text_field :family_name, autofocus: true, autocomplete: 'family_name', class: 'form-control' %>
          </div>
          <div class="col-lg-6">
            (名)<%= f.text_field :first_name, autofocus: true, autocomplete: 'first_name', class: 'form-control' %>
          </div>
        </div>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :フリガナ %></h5>
      </div>
      <div class='col-lg-8'>
        <div class="row">
          <div class="col-lg-6">
            (セイ)<%= f.text_field :family_name_kana, autofocus: true, autocomplete: 'family_name_kana', class: 'form-control' %>
          </div>
          <div class="col-lg-6">
            (メイ)<%= f.text_field :first_name_kana, autofocus: true, autocomplete: 'first_name_kana', class: 'form-control' %>
          </div>
        </div>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :Eメール %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :email, autofocus: true, autocomplete: 'email', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :郵便番号(ハイフンなし) %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :post_code, autofocus: true, autocomplete: 'post_code', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :住所 %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :address, autofocus: true, autocomplete: 'address', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :電話番号(ハイフンなし) %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :tel, autofocus: true, autocomplete: 'tel', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :パスワード(6文字以上) %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.password_field :password, autocomplete: 'new-password', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :パスワード確認用 %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.password_field :password_confirmation, autocomplete: 'new-password', class: 'form-control' %>
      </div>
    </div>

    <div class='row'>
      <div class='col-lg-2 offset-lg-5 actions'>
        <%= f.submit '新規登録', class:'btn btn-danger' %>
      </div>
    </div>

    <% end %>
  </div>

  <div class="space">
    <h4>すでに登録済みの方</h4>
    <h5><%= link_to 'こちら', new_customer_session_path  %>からログインしてください</h5>
  </div>
</div>

Sign_in

html/app/views/customers/sessions/new.html.erb
<div class='col-lg-6 offset-lg-4 offset-1 space'>
  <div class="row">
    <h3>
      <span style="display: inline-block;">会員の方は</span>
      <span style="display: inline-block;">こちらからログイン</span>
    </h3>
  </div>

  <div class="row">
    <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
    <%= render 'devise/shared/error_messages', resource: resource %>

    <div class="form-group row space">
      <div class="col-lg-4">
        <h5><%= f.label :Eメール %></h5>
      </div>
      <div class="col-lg-8">
        <%= f.text_field :email, autofocus: true, autocomplete: "email", class: 'form-control' %>
      </div>
    </div>

    <div class="form-group row">
      <div class="col-lg-4">
        <h5><%= f.label :パスワード %></h5>
      </div>
      <div class="col-lg-8">
        <%= f.password_field :password, autocomplete: "current-password", class: 'form-control' %>
      </div>
    </div>

    <div>
      <%= link_to "=>パスワードを忘れた方はこちら", new_password_path(resource_name) %>
    </div>

    <div class="form-group row space">
      <div class="col-lg-2 offset-lg-5">
        <%= f.submit "ログイン", class:"btn btn-danger" %>
      </div>
    </div>

    <% end %>
  </div>

  <div class="space">
    <h4>登録がお済みでない方</h4>
    <h5><%= link_to "こちら", new_customer_registration_path  %>から新規登録してください。</h5>
  </div>

</div>

編集

app/views/customers/edit.html.erb
<div class='col-lg-10 offset-lg-1 space'>
  <div class='row'>
    <h2>登録情報編集</h2>
  </div>

  <div class='row'>
    <%= form_with(model: @customer, local: true, class: 'container') do |f| %>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :名前 %></h5>
      </div>
      <div class='col-lg-8'>
        <span style='display: inline-block;'>
          (姓)<%= f.text_field :family_name, autofocus: true, autocomplete: 'family_name', class: 'form-control' %>
        </span>
        <span style='display: inline-block;'>
          (名)<%= f.text_field :first_name, autofocus: true, autocomplete: 'first_name', class: 'form-control' %>
        </span>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :フリガナ %></h5>
      </div>
      <div class='col-lg-8'>
        <span style='display: inline-block;'>
          (セイ)<%= f.text_field :family_name_kana, autofocus: true, autocomplete: 'family_name_kana', class: 'form-control' %>
        </span>
        <span style='display: inline-block;'>
          (メイ)<%= f.text_field :first_name_kana, autofocus: true, autocomplete: 'first_name_kana', class: 'form-control' %>
        </span>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :Eメール %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :email, autofocus: true, autocomplete: 'email', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :郵便番号(ハイフンなし) %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :post_code, autofocus: true, autocomplete: 'post_code', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :住所 %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :address, autofocus: true, autocomplete: 'address', class: 'form-control' %>
      </div>
    </div>

    <div class='form-group row'>
      <div class='col-lg-4'>
        <h5><%= f.label :電話番号(ハイフンなし) %></h5>
      </div>
      <div class='col-lg-8'>
        <%= f.text_field :tel, autofocus: true, autocomplete: 'tel', class: 'form-control' %>
      </div>
    </div>

    <div class="form-group row">
      <div class="col-lg-4 offset-lg-8">
        <%= f.submit '編集内容を保存する', class:'btn btn-danger' %>
        <%= link_to '退会する', customer_withdraw_path(@customer), class:'btn btn-danger'  %>
      </div>
    </div>
    <% end %>
  </div>
</div>

詳細

app/views/customers/show.html.erb
<div class="col-lg-6 offset-lg-3 space">
  <div class="container">
    <h2>マイページ</h2>
  </div>
  <!-- お客様情報 -->
  <div class="container space">
    <div class="row">
      <div class="col-lg-5"><h3>お客様情報</h3></div>
      <div class="col-lg-2">
        <%= link_to '編集', edit_customer_path, class: 'btn-sm btn-danger' %>
      </div>
      <div class="col-lg-4">
        <%= link_to 'パスワード変更', new_customer_password_path, class: 'btn-sm btn-danger' %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>氏名</h4>
      </div>
      <div class="col-lg-9">
        <%= @customer.full_name %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>カナ</h4>
      </div>
      <div class="col-lg-9">
        <%= @customer.family_name_kana %> <%= @customer.first_name_kana %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>郵便番号</h4>
      </div>
      <div class="col-lg-9"><%= @customer.post_code %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>住所</h4>
      </div>
      <div class="col-lg-9">
        <%= @customer.address %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>電話番号</h4>
      </div>
      <div class="col-lg-9">
        <%= @customer.tel %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>Eメール</h4>
      </div>
      <div class="col-lg-9">
        <%= @customer.email %>
      </div>
    </div>
  </div>

  <!-- その他のリンク -->
  <div class="container space">
    <div class="row">
      <div class="col-lg-3">
        <h4>配送先</h4>
      </div>
      <div class="col-lg-9">
        <%= link_to '一覧を見る', addresses_path, class: 'btn btn-danger' %>
      </div>
    </div>

    <div class="row">
      <div class="col-lg-3">
        <h4>注文履歴</h4>
      </div>
      <div class="col-lg-9">
        <%= link_to '一覧を見る', orders_path, class: 'btn btn-danger' %>
      </div>
    </div>
  </div>
</div>

退会確認

app/views/customers/withdraw.html.erb
<div class="col-lg-10 offset-lg-1 space">
  <div class="row space">
    <h2>本当に退会しますか?</h2>
  </div>

  <div class="row space">
    <h4>退会すると、会員登録情報や
      <br>これまでの購入履歴が閲覧できなくなります。
      <br>退会する場合は「退会する」をクリックしてください。
    </h4>
  </div>

  <div class="row space">
    <%= link_to "退会しない",customer_path(current_customer),class: "btn btn-danger" %>
    <%= link_to "退会する", customer_withdraw_done_path(current_customer), method: :put, "data-confirm" => "本当に退会しますか?", class: "btn btn-danger" %>
  </div>
</div>

SCSS

app/assets/stylesheets/application.scss
// initialize
*{
  margin:0;
  padding:0;
  box-sizing:border-box;
  color: #120136;
}

a{
  text-decoration: none;
}

.space {
  padding: 50px 0 30px;
}

後記

今回、機能そのものは難しくないのですが、画面をレスポンシブ対応にしようとするとやはり手間取りました。ボタンは後ほどテーマカラーの色に変更する予定で、今はとりあえず全て赤にしてあります。目立つので。
また、Viewファイルに書いたリンクも一応は機能しますが、飛んだ先のページにまだ何も書いていないので、例の「Find me in ...」という文言が出るだけです。

メインの画面を作ったことで、ようやくサイトらしい見た目になってきましたね。機能はまだまだですが、これから実装していきます。気になっていたIK●Aカラーも、進めるうちに「これはこれでアリなんじゃないか?」と思えるようになってきました。単に見慣れただけかも知れませんが。

写真を載せればきっとパン屋さんのサイトになると信じています。次回へ続く!

参考

RailsでTwitterBootstrapを使ったflashメッセージ

データ登録の際、適当な名前や住所が思い付かない時に便利。「文月太郎」とかでは流石に味気ないので……。
日本人名前自動生成機
ランダム日本地名ジェネレータ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails の API doc作成ツール apipie-rails

rails で RESTful APIを作るに当たって、ドキュメンテーションツールを導入してみました

https://github.com/Apipie/apipie-rails

背景

rails でフロントとバックエンドと別々の担当者が作業しようとしたときに、フロントから受け渡されるべきパラメーターはなんで、その結果どんな結果がわたるのかどうやってドキュメントしようかなと。。

javadocみたいに定番の書式があるわけではなさそうなので、gem周りで検索してみました。

さらに、世の中の定番としては こんなかんじなのかな
https://qiita.com/jian-feng/items/c79955713e6dd8b5244a

結果としてたどり着いたのが、 apipie-rails です

何が出来るのか

  1. ソースコード上にDSLを使ってAPIインターフェースを定義できる
  2. Webサーバー経由でHtmlドキュメントとして可視化された情報にアクセス出来る
  3. 引数のバリデーションをする → ArgumentError exception が上がる

導入

  1. "gem 'apipie-rails'" を追加(一旦、開発環境のみに導入されるように設定する)
  2. bundle install 後、rails g apipie:install で以下が追加されました a. routes.rb に apipie ルートが追加 b. config/initializers/apipie.rb が追加
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Docker][Rails] 新しくGemを導入した後の3コマンド

これはなに

Railsでアプリ開発中新しくGemを導入した後、若干躓いたので備忘録として残します。
ご指摘等あれば頂けると幸いです。

3コマンド

gem bundle install
docker-compose run web bundle install

参考記事
Docker Compose + Railsでイメージ内でbundle installしているはずなのにgemが無いとエラーがでる。

docker-compose restart

参考記事
dockerの起動、停止、再起動

以上です!ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】editアクションでのエラー解決

本記事を書いた理由

  • 今回引っかかったエラーに対してどのように解決をしたのかを記録として残すことで同じミスをしないようにするため

目次

  • どんなエラーが出たのか
  • 考え方の道筋(実際にやってみたこと)
  • 反省点

参考記事

どんなエラーが出たのか

todoアプリで編集機能をつけようとしたがうまく動かない。
具体的にはeditボタンという編集ページに飛ぶボタンを押すとエラー画面が出てくる状態。

スクリーンショット 2020-07-08 16.00.39.png

エラー画面
スクリーンショット 2020-07-08 16.04.04.png

考え方の道筋(実際にやってみたこと)

結論
edit.html.erbのform_forの記述にパスを指定してあげる必要があった

実際にやってみたこと

  • エラー画面をみてeidt.html.erbに問題があると考える

  • コントローラの記述に誤りはないのか(編集したい情報をきちんと持ってきてこれているのか)

  • 改めてedit.html.erbを見てみる(記述に変更を加えてみた)

エラー画面をみてeidt.html.erbに問題があると考える

スクリーンショット 2020-07-08 16.37.25.png

  • エラー画面からパスに問題があると推定した
  • しかしこの時点でパスの何を直せばいいのかよくわからないので他のサイトからform_forの記述方法を調べて誤りがないか調べてみた。結果として特に誤りがないと判断した

コントローラの記述に誤りはないのか(編集したい情報をきちんと持ってきてこれているのか)

スクリーンショット 2020-07-08 16.06.17.png

  • コントローラの記述に誤りがないかどうか確認をした(特に問題なし)

スクリーンショット 2020-07-08 16.42.27.png

  • 確認のためにbinding.pryを使って編集したい情報が持ってこれているか確認をした(特に問題なし)
  • この時点でルーティングに問題がないと判断した(コントローラがしっかりと動いていることが確認できたため)

改めてedit.html.erbを見てみる(記述に変更を加えてみた)

  • ルーティングとコントローラに問題がない時点でビュー(edit.html.erb)に問題があることを再確認した
  • そうなるとエラー画面の通りform_forの記述方法に問題がある可能性が高いので改めて確認をした
  • 結果としてform_forは自動的にcreateアクションやeditアクションを分けてくれる。しかしうまくいかない時もあるのでその時は直接アクション名(パス)を書いてあげることで解決できるらしい。(以下のように編集)

スクリーンショット 2020-07-08 16.50.54.png

  • 上記のようにパスを新しく指定することでエラーが解決できました。 *action="index"になっていますが実際はpatchを入れて編集内容を保存します。

エラー解決後の画面
スクリーンショット 2020-07-08 16.14.27.png

反省点

  • エラー画面の詳細をもう少しみてそこから解決できる方法を考えるべきだった(最初のエラー画面の時点で意味がよくわからないのでコントローラの方などに原因があると考えてしまった)
  • 上記のようなことをしなかったので解決までに時間がかかってしまった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Elasticsearchで多対多&階層化されたカテゴリーを検索対象に含める方法

前提

以下の前提で、ひとまずローカル環境(Rails s)での検索を実現する

  • Ruby on Rails で多対多のモデル(例としてPostとCategoryとします)を使っている
  • Ancestryを使ってモデルを多階層にしている(Categoryが多階層です)
  • Elasticsearch と Kibana がインストール済み
  • Searchkick(Elasticsearchを簡単にするGem)を使って検索をしたい
  • Postを検索するときに検索対象にCategory.nameを含めたい

PostCategoryが中間テーブルを使って多対多の関係であり、Categoryだけ多階層になっている。

データベース ER 図 .png

構築がまだの方へのおすすめ記事

中間テーブル実装後にカテゴリ別ページを表示
https://qiita.com/otterminal/items/a8859a1fad0027a9bc5b

多階層カテゴリでancestryを使ったら便利すぎた
https://qiita.com/Sotq_17/items/120256209993fb05ebac

ElasticsearchとKibanaのインストール(HomebrewでOSS版をインストール)
https://qiita.com/maztak/items/0f722ad01c982a96de59

速攻で検索機能を実装できるsearchkickを調べた(Ruby)
https://qiita.com/kentosasa/items/f0af67f62f4692d68370

ただしElasticsearch、Ancestry、Searchkickなどは更新により変更が大きいので色んな方法がネットに転がっているので注意。

公式リポジトリ

Searchkick
https://github.com/ankane/searchkick

Ancestry
https://github.com/stefankroes/ancestry

コード

普通にsearch_dataの中でcategories.map(&:name)としてやれば良い。

post.rb
class Post < ApplicationRecord
    searchkick
    has_many :post_category_relations
    has_many :categories, through: :post_category_relations

    def search_data
        {
            name: name,
            description: description,
            category_name: categories.map(&:name)
        }
    end
end

Categoryのインデックスはまだしてないのだが、今のところ以下のようになっている。after_commit以降のコードはカテゴリーが更新された時にPostをReindexしてね、というコードなので、今回の件とは直接は関係しない。

category.rb
class Category < ApplicationRecord
    has_many :post_category_relations
    has_many :posts, through: :post_category_relations
    has_ancestry

    after_commit :reindex_post

    def reindex_post
        post.reindex
    end
end

Searchkick開発者の回答?

Rails: Elasticsearch :through association mapping
https://stackoverflow.com/questions/19721200/rails-elasticsearch-through-association-mapping/19751244

Reindex は Rails Console の再起動の必要あり

あくまで開発環境(ローカル・developmentインデックス?)での話だが、Modelでsearch_dataメソッドやMappingを変更した後に Rails Console を再起動せずにPost.reindexしても、変更前の情報でインデックスしてしまうので注意。

Kibanaでの検証Tips

http://localhost:5601/app/kibana#/dev_tools/console
スクリーンショット 2020-07-08 15.26.05.png

Kibanaでのインデックス一覧やマッピング、検索結果の確認・検証において、上記コンソールを使うが、ここは(SwiftのPlaygroundのように)記述したブロックごとの実行が可能。なのでいちいちHistroyからメソッドを全部書き換えないでも、よく使うメソッドを全部貼り付けて適宜ブロックごとに実行すればいい。


# kiban_console

# インデックスの削除(エイリアスでの指定は不可)
DELETE /posts_development_20200708003504586

# インデックス一覧の確認
GET /_aliases?pretty

# マッピングの確認
GET /posts_development/_mapping?pretty

# すべてのドキュメントを返す(上限100件)
GET /posts_development/_search
{
  "query": { 
    "match_all": {}
  },
  "size": 100
}

# 複数のフィールドを検索対象にして検索
GET /posts_development/_search
{
  "query": {
    "multi_match": {
      "fields": [ "name", "description", "category_name"],
      "query": "美容"
    } 
  }
}

#  
GET posts_development/_analyze
{
  "text" : "美容"
}

その他のメソッドはこちらを参照
- 初心者のためのElasticsearchその1
- 初心者のためのElasticsearchその1
- Analyze API 公式ドキュメント

その他

as_jsonを使った関連付けされたデータの追加というQiitaもあったが、これは1対多で中間テーブルがない場合の記法のようである。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]gem 'ancestry'による多階層カテゴリーの導入[備忘録]

はじめに

初めて記事を投稿します。
某プログラミングスクールの課題でフリマアプリを作っていて、
gem 'ancestry'を導入してカテゴリー機能を実装したのでメモしておきます。
以下のようなものを目指します
FURIMAancestry.gif

なお、この記事ではデータベースにカテゴリーを入れて表示するところまでを記述します。

前提

Productモデル(カテゴリーを追加したいモデル)とCategoryモデルは実装してある。
大丈夫だとは思いますが、'Category'の複数形は'Categories'なので、
使うときは注意しましょう。(周りにそれが原因でエラーを出していた人がいたため。)

導入

初めて使うgemのときは公式ドキュメントをまず参照。
https://github.com/stefankroes/ancestry

gem 'ancestry'

bundle installを実行

カテゴリーテーブルにデータを入れる

さて、早速 rails db:seed を実行してデータを入れようと思ったところ、上手いかない・・・

原因

1.DB設計の際、先にancestryカラムを追加していたが、データ型がintegerになっていた。
 ancestryカラムはstring型です
2.null: falseが設定されており、下記のエラーが出た。

Mysql2::Error: Field 'ancestry' doesn't have a default value

解消

1.データ型を変更

rails g migration ChangeDatatypeAncestryOfCategories
def change
  change_column :categories, :ancestry, :string, index: true
end

2.null:false を外す

rails g migration ChangeColumnToNotNull
def up
  change_column :categories, :ancestry, :string, null: true
end

def down
  change_column :categories, :ancestry, :string, null: false
end

これでエラーが出ずに通るようになったので、
データを記述してテーブルに入力

db/seeds.rb
lady = Category.create(name: "レディース")
lady_1 = lady.children.create(name: "トップス")
lady_1.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"},{name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "シャツ/ブラウス(七分/長袖)"},{name: "ポロシャツ"},{name: "キャミソール"},{name: "タンクトップ"},{name: "ホルターネック"},{name: "ニット/セーター"},{name: "チュニック"},{name: "カーディガン/ボレロ"},{name: "アンサンブル"},{name: "ベスト/ジレ"},{name: "パーカー"},{name: "トレーナー/スウェット"},{name: "ベアトップ/チューブトップ"},{name: "ジャージ"},{name: "その他"}])

#以下省略

rails db:seed を実行!
スクリーンショット 2020-07-08 15.04.33.png

詳しくは公式ドキュメントに書いてありますが、入力されたCategoriesテーブルの中身を説明すると、
- 親要素(id: 1)のancestryは、nullが入る
- 子要素(id: 2)ののancestryは、親要素のid(1)が入る
- 孫要素(id: 3~20)のancestryは、親と子のidが入る(1/2)
これにより、多階層のカテゴリーを表現できる。

例えば、他のカテゴリーだとこんな感じ。
スクリーンショット 2020-07-08 15.19.35.png

一つのテーブル、一つのカラムで実装出来るのは管理もしやすくて使い勝手がいいですね。

 ビュー

考え方を記載します。

products_controller.rb
def index
  # ancestry:nil つまり、親要素だけを取得
  @parents = Category.where(ancestry: nil)
end
_category.html.haml
- @parents.each do |parent|
  = parent.name
- parent.children.each do |child|
  = child.name
- child.children.each do |grandchild|
  = grandchild.name
#細かい部分は省略しています
#インデント等が正しくないですが、親に子を、子に孫を入れ子にしていくと上手く表示できます。
category.scss
// 親、子、孫に対して初期状態では
display: none;
// カテゴリーから探すにホバーされたときに、親要素を
display: block;
//親要素にホバーされたときには子要素、子要素にホバーされたときは孫要素に
display: block;
//ホバーされた要素自体に対しては、背景色や文字色を変更するとわかりやすくなります。

このように連続してタブを出現させるには、
CSSを使う方法とJavaScriptを使う方法があると思いますが、
ホバーされたときに出現したり消えたりするだけならSCSSにまとめて書いた方が、
他の人が見たときに分かりやすいと思いこのような実装をしました。

学んだこと

・必要なときに必要なテーブルを作り、必要なカラムを追加する。
・gemの使い方はまず公式ドキュメントを読む。

何かの参考になれば幸いです。
ここまで記事を読んでくださりありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mobile Safari は再起動時に POST リクエストする場合がある

Rails アプリケーションで、iPhone の Safari からのリクエストでだけ ActionController::InvalidAuthenticityToken 例外が発生するので調査していたところ、Mobile Safari は再起動後、開いていたタブをロードする際に、POST リクエストで得たページであっても再度リクエストを送信しているらしいことがわかった。

※ iOS 13.5.1 の iPhone 11 で検証

検証方法

下記の Ruby スクリプトを作成して ruby server.rb -o 0.0.0.0 で起動

# gem install sinatra が必要
require "sinatra"

html = <<~HTML
<!doctype html>
<form action="/" method="post">
  <input type="text" name="email">
  <button type="submit">Submit</button>
</form>
HTML

get "/" do
  html
end

post "/" do
  p params
  html
end
  1. Mobile Safari で http://[上記スクリプト実行したPCのアドレス]:4567 にアクセス
  2. 適当に入力して submit
  3. Mobile Safari を終了
  4. Mobile Safari を起動 (確認などなしでページが表示される)

この手順でログを見ると、4 で 2 と同じリクエストが送られていることが確認できた。

x.x.x.x - - [08/Jul/2020:14:24:37 +0900] "GET / HTTP/1.1" 200 131 0.0072
{"email"=>"foo"}
x.x.x.x - - [08/Jul/2020:14:24:41 +0900] "POST / HTTP/1.1" 200 131 0.0022
{"email"=>"foo"}
x.x.x.x - - [08/Jul/2020:14:24:48 +0900] "POST / HTTP/1.1" 200 131 0.0007

Rails で InvalidAuthenticityToken 例外が起こる理由と対策

Rails で...

  1. Cookie をセッションストアとして使っていて、かつ有効期限が Session (デフォルト)
  2. protect_from_forgery with: :exception を指定している

場合に、上記のように 2 度 POST リクエストが送られると、2 度めのリクエストでは Cookie がクリアされているので session[:_csrf_token] が nil になり、params[:authenticity_token] の検証が失敗し、InvalidAuthenticityToken 例外が発生してしまう。

解決策としては下記のものが考えられる。

  1. POST でページを render せず、redirect してかならず GET させるようにする
  2. protect_from_forgery で null_session などにする
  3. Cookie の有効期限を伸ばす

3 の場合は config/initializers 下に適当なファイルを作って下記の内容を記述すればよい。

# key の部分はなんでもいいがデフォルトは "_#{アプリケーション名}_session"
# ここでは有効期限を 2 週間にしている
Rails.application.config.session_store :cookie_store, key: "_foo_session", expire_after: 2.weeks
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラー:Trying to get property 'id' of non-object

解決法

このエラーはそのObjectには、idというプロパティはないのに、取得しようとしているという意味なので,idがnullになっている可能性が高い
そのnullに値を入れれば解決するはず、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_withのform.〇〇について

form_withのform.〇〇について

form.withのform.〇〇について、わからないところがあったので、備忘録としてまとめていく。

form.text_field

<%= form.text_field :title %>

幅20文字の入力欄が設定される

form.text_area

<%= form.text_area :content %>

40×20の入力欄が設定される

form.email_filed

<%= form.email.field :email %>

投稿内容に@が含まれていないとメールアドレスとみなされず、エラーになる。
テスト時に注意!

他にもいろいろあるが今回はメールの入力欄にてテスト時にエラーが出たので、備忘録としてまとめた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

i18nのロケールをJavaScriptにわたす

多言語対応のアプリを作成中に詰まった

jQueryとAjaxを使って、親カテゴリを選択すると子カテゴリの候補がプルダウンで表示されるようにしました。
ところが、子カテゴリだけ、デフォルト表記のベトナム語でしか翻訳されてませんでした。

例:親カテゴリ => 質問
  子カテゴリ => 1,Ngôn ngữ và văn hóa
         2,Tập huấn kỹ thuật
         3,Ứng dụng và thủ tục

原因

Ajaxで子カテゴリを取得する際、コントローラにロケールを渡してないことが原因でした。

category_pulldown.js
$("#parent").on("change", function () {
  // 選択した親カテゴリからIDを取得
  var id = document.getElementById("parent").value;
// 中略
$.ajax({
  type: 'GET',
  data: { parent_id: id }, // IDをparams[:parent_id]に入れて送信
  url: '/categories/pulldown', // categoriesコントローラにて、受け取ったIDで子カテゴリを取得
  dataType: 'json',
}).done(function (children) { // 取得した子カテゴリを表示させる処理...

一部抜粋ですがロケールの取得は以下の方法でやってました。

application_controller.rb
before_action :set_locale

  private

  def set_locale
    I18n.locale = locale
  end

  def locale
    @locale ||= params[:locale] ||= I18n.default_locale
  end

  def default_url_options(options = {})
    options.merge(locale: locale)
  end

現在のロケールをJava Scriptへわたす

まずは、inputタグを使って現在のロケールを保持させます。
以下のようにtype="hidden"と書くと、ユーザーから見ても画面には表示されません。

hoge.html.erb
<input type="hidden" class="current_locale" value="<%= I18n.locale %>">

次にlocale: $('.current_locale').val()を追加すると
親子カテゴリの翻訳言語が統一されました。

category_pulldown.js
$("#parent").on("change", function () {
  // 選択した親カテゴリからIDを取得
  var id = document.getElementById("parent").value;
// 中略
$.ajax({
  type: 'GET',
  data: { parent_id: id, locale: $('.current_locale').val() }, // IDをparams[:parent_id]に入れて送信
  url: '/categories/pulldown', // categoriesコントローラにて、受け取ったIDで子カテゴリを取得
  dataType: 'json',
}).done(function (children) { // 取得した子カテゴリを表示させる処理...

HTMLはRubyからもJavaScriptからも情報の受け渡しができるので便利ですね:smiley:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】Chartkickでカラム別集計の円グラフを作成

はじめに

railsでグラフを挿入したかったのでchartkickを使用しました。
導入自体も容易なので、集計結果の円グラフ化の方法を記載します。

参考にしたページ
-chartkick公式ドキュメント
-Railsでシンプルなグラフを扱うならchart-js-rails よりchartkickを使うべし
-爆速で円グラフを実装する[5分]

環境

ruby 2.5.1
Rails 5.2.4.3

前提と目標

ユーザの意見をまとめたvotesテーブルopinionカラム(integer)から円グラフを作成する。
opinionカラムの値が1の場合は賛成、0の場合は反対で集計しますが、他の値やNULL値が混ざっている状態で作成します。

votesテーブル

id opinion
1 1
2 1
3 0
4 1
5 0
6 2
7 NULL
8 3
9 NULL
10 0

完成図

pie_A.png
※実機では円グラフの各要素にマウスカーソルを当てると要素数が表示されます。

Chartkickの導入

参考ページに記載されていますが改めて記載します。

Gemfile
gem "chartkick"   #追記する
Gemfile
$ bundle install
app/javascript/packs/application.js
//= require chartkick
//= require Chart.bundle  #2行追記する

viewページへの反映

表示させるページはどこでもいいですが今回はpostsコントローラーのindex.html.erbにグラフを表示させたいと思います。

app/views/posts/index.html.erb
<%= pie_chart Vote.group(:opinion).count%>

chart (1).png

これだけでは何を表しているのかよくわからないグラフになっています。。

そこでコントローラーファイルに変数とグラフ用のメソッドを定義します。

app/controllers/posts_controller.rb
  def index
    @opinion = Vote.pluck(:opinion)
    @aggregate = aggregateOpinion(@opinion)
    @sum = sumOpinion(@opinion)
  end

  def aggregateOpinion(array)
    result = [["賛成",0],["反対",0],["どちらでもない",0],["無回答",0]]
    array.each do |i|
      if i == 1
        result[0][1] += 1
      elsif i == 0
        result[1][1] += 1
      elsif i == nil
        result[3][1] += 1
      else 
        result[2][1] += 1
      end
    end
    return result
  end

  def sumOpinion(array)
    result = [["総投票数",0],["有効投票数",0],["無効投票数",0]]
    array.each do |i|
      if i == nil
        result[2][1] += 1
      else 
        result[1][1] += 1
      end
    end
    result[0][1] = array.length 
    return result
  end

解説

pluckメソッドで対象のカラムだけを配列で取得しています。
その後、aggregateOpinionメソッドで円グラフ用のデータに整形しています。

result配列のインデックスがそのまま円グラフの表示順になるのでパラメータが
NULL値のものを最後にしてその次に所謂"その他"(今回の場合は[どちらでもない]という名前)が来るようにしています。

sumOpinionメソッドは直接グラフには関係ありませんが総投票数の情報をグラフに載せることが難しいので別途作成しています。

以上の内容を踏まえ、改めてビューページに反映させたいと思います。

app/views/posts/index.html.erb
<%= pie_chart @aggregate, width: "500px"%>

chart (2).png

これでも問題ないですが最後にレイアウトとsumOpinionメソッドで取得した内容をtable要素で表示させて完成です。

app/views/posts/index.html.erb
<%= pie_chart @aggregate,colors: ["#3333cc","#cc3333","#339966","333"], donut: true , width: "500px"%>
<table border="1" width="500">
  <tr>
    <th>総投票数</th>
    <th>有効投票数</th>
    <th>無効投票数</th>
  </tr>
    <tr>
    <th><%=@sum[0][1]%></th>
    <th><%=@sum[1][1]%></th>
    <th><%=@sum[2][1]%></th>
  </tr>
</table> 

pie_A.png

終わりに

いかがでしたでしょうか。もう少しメソッドの部分に応用を効かせられたなぁと自分でも思いますね。。
もちろんchartkickが対応しているのは円グラフだけではないので是非調べてみてください。
見て頂いてありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Arel::Nodesを最低限読めるようになりたい

Arel::Nodes を使って書かれたコードを最低限読んで理解できるようになりたい方のために、例文を紹介しながら、どのような処理なのか読み解いていきます。

※ 自分が実際に使用されているのを見たことがあるもののみを紹介しています。

ちなみに、arel_table から作れる条件式はこちらが参考になりました。
ActiveRecordのarel_tableから作れる条件式まとめ

Arel::Nodes#build_quoted

build_quoted メソッドは文字列などの値をラップして、Arel::Nodes::NamedFunction などの引数に渡せるようにします。

Arel::Nodes.build_quoted(" ")

上記例文は " " をラップしているだけですね。具体的な使用例は Arel::Nodes::NamedFunction を参照ください。

Arel::Nodes::NamedFunction

Arel::Nodes::NamedFunction は SQL の任意の関数を呼び出すことができます。
第1引数に関数名、第2引数に関数に渡す引数(配列)、第3引数にエイリアス名(任意)を渡します。

例文1
users_table = User.arel_table
separator = Arel::Nodes.build_quoted(" ")

User.select(
  Arel::Nodes::NamedFunction.new(
    "CONCAT",
    [users_table[:last_name], separator, users_table[:first_name]],
    "name"
  )
)

# => SELECT CONCAT(`users`.`last_name`, ' ', `users`.`first_name`) AS name FROM `users`

例文では、第1引数に CONCAT が指定されています。
CONCAT 関数は複数の文字列を結合してくれるもので、ここでは User テーブルの last_name" "first_name を繋ぎます。
例えば、last_name が「Yamada」で first_name が「Taro」だとすると「Yamada Taro」となります。
苗字と名前を分けてDBに保存しているが、つなげて取り出したい時の使い方になります。

Arel::Nodes::NamedFunction に関してはもう一つ例文を紹介します。

例文2
users_table = User.arel_table
format = Arel::Nodes.build_quoted("%Y-%m-%d")

User.select(
  Arel::Nodes::NamedFunction.new(
    "DATE_FORMAT",
    [users_table[:created_at], format],
    "registration_date"
  )
)

# => SELECT DATE_FORMAT(`users`.`created_at`, '%Y-%m-%d') AS registration_date FROM `users`

DATE_FORMAT 関数は名前の通り、日時を指定のフォーマットに整形してくれる関数です。
ここでは User テーブルの created_at%Y-%m-%d というフォーマットに変換します。
「2020-07-01」みたいな形です。
"日時"ではなく"日付"として取り出したい時の使用例になります。

Arel::Nodes::SqlLiteral

生の SQL 文字列を作ることができます。SQL の構文中に必要な文字列をラップするのに使われることが多いです。
ちなみに、Arel で唯一パブリックな関数である Arel.sql() は、内部で Arel::Nodes::SqlLiteral を使用しています。

Arel::Nodes::SqlLiteral.new("utf8_general_ci")

例文は utf8_general_ci をラップしているだけですね。具体的な使用例は Arel::Nodes:InfixOperation を参照ください。

Arel::Nodes::InfixOperation

Arel::Nodes::InfixOperation の「infix operation」は二項演算という意味で、2つの値・変数の計算を行うことができます。
第1引数に演算子、第2引数に演算子の左側の値、第3引数に演算子の右側の値が入ります。

User.where(
  Arel::Nodes::InfixOperation.new(
    "COLLATE",
    User.arel_table[:nickname],
    Arel::Nodes::SqlLiteral.new("utf8_general_ci")
  )
  .matches(Arel::Nodes.build_quoted("トム%"))
)

# => SELECT `users`.* FROM `users` WHERE `users`.`nickname` COLLATE utf8_general_ci LIKE 'トム%'

例文では演算子として COLLATE を指定しています。COLLATE 関数は検索条件で照合順序を指定する時に使用されるものです。そして演算子の左にくるのが User テーブルの nickname、右に来るのが utf8_genera_ci です。

ちなみに、utf8_genera_ci は UTF-8 文字コードで半角や全角、濁点を区別し、英語の大文字小文字を区別しない指定です。

つまり、User の nickname の「AAA」と「aaa」は一緒とみなして、「あああ」と「アアア」や「ぁぁぁ」は区別するということになります。

そして、最後の matches()トム% を指定しているため、この例文は nickname が「トム」から始まる User を取得するというクエリになります。このとき、"とむ" や "ドム" などから始まる User はヒットしません。

Arel::Nodes::OuterJoin

外部結合(OUTER JOIN)する際に使用します。使い方としては、JOIN句の第2引数に Arel::Nodes::OuterJoin を指定し、最後に join_sources を呼び出すことで結合できます。

messages_table = Message.arel_table
users_table = User.arel_table

Message.joins(
  messages_table
    .join(users_table, Arel::Nodes::OuterJoin)
    .on(
      messages_table[:user_id].eq(users_table[:id])
      .and(users_table[:is_active].eq(true))
    )
    .join_sources
)

# => SELECT `messages`.* FROM `messages` LEFT OUTER JOIN `users` ON `messages`.`user_id` = `users`.`id` AND `users`.`is_active` = TRUE

例文では、Message テーブルに User テーブルを外部結合しています。
結合の条件としては、Message テーブルの user_id と User テーブルの id が等しい、かつ User テーブルの is_activetrue のものという条件になります。
つまり、active な状態の User のみ外部結合するという条件になっているわけです。

Arel::Nodes::Descending, Arel::Nodes::Ascending

Arel::Nodes::Descending は ORDER 句の DESC (降順)の部分を担当します。Arel::Nodes::Ascending は ASC (昇順)です。引数には、ORDER BY の右側の値が入ります。

User.order(
  Arel::Nodes::Descending.new(
    User.arel_table[:user_type].in([10,11,12])
  )
)

# => SELECT `users`.* FROM `users` ORDER BY `users`.`user_type` IN (10, 11, 12) DESC

例文では、Arel::Nodes::Descending の引数に User.arel_table[:user_type].in([10,11,12]) が指定されています。
in 関数がついていなければ、この例文は User.order(user_type: :desc) と同じクエリになり user_type の降順に並ぶわけですが、.in([10,11,12]) と絞り込むことで、これらを一つのグループとして並び替えています。
この場合は user_type が 10, 11, 12 のユーザー群が前方に、それ以外のユーザー群が後方に来るように並びます。
前方と後方に分かれた後の並びは特に指定していないので、id の昇順になります。
特定の条件に合致するユーザーを前方に持ってきたいときなどに使用されますね。

Arel::Nodes::Case

SQL の CASE 構文を表現することができます。使い方は直感的ですので、例文を参考にしてください。

products_table = Product.arel_table
new_cond = products_table[:created_at].gt(Time.zone.now - 3.days)

Product.order(
  Arel::Nodes::Ascending.new(
    Arel::Nodes::Case.new
      .when(new_cond).then(1)
      .else(9)
  )
)

# => SELECT `products`.* FROM `products` ORDER BY CASE WHEN `products`.`created_at` > '2020-07-01 12:34:56' THEN 1 ELSE 9 END ASC

Arel::Nodes::Case.new の箇所では、Product が作成されて3日以内であれば 1、そうでなければ 9 という値をとります。これと Arel::Nodes::Ascending を組み合わせることで、3日以内に作成された Product を前方にそれ以外を後方に並び替えています。

「Arel::Nodes::Descending, Arel::Nodes::Ascending」 の例文と同じ感じですね。

最後に

例文で見たように Arel::Nodes を使うことで表現力はかなりアップします。しかし、慣れていない方にとっては何を行っているのかわかりづらいという欠点もあります。また、Arelでクエリを書くのはやめた方が良い5つの理由 という記事もあり、Arel を使うのには賛否両論があるようです。

ただ、環境によってはすでに Arel が頻繁に使用されていて、書かないまでも読む必要はあるような場面は結構多いように思います。こちらの記事がそのような環境に出くわした方の参考になれば幸いです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む