こんにちは、鈴木です。
Rails で SELECT するカラムを追加する scope を定義する方法をご紹介します。
やりたいこと
SELECT するカラムを指定するには、以下のように select メソッドを使用します。
1 |
User.select('email') |
他のカラムから計算した値が必要な場合、例えば姓(family_name)と名(given_name)を結合した値を full_name という名前で欲しいという場合は次のようになるでしょう。
1 |
User.select('family_name || given_name AS full_name') |
複数個所で使う場合は scope として定義しておくと便利です。
1 2 3 4 5 6 7 |
class User < ActiveRecord::Base scope :select_full_name, lambda { select('family_name || given_name AS full_name') } end |
scope を活用することでコードがすっきりし、みんなハッピー!と思いきや。
次のコードを見てください。
1 2 3 |
User.select_full_name.each do |user| puts "#{user.full_name} さんの誕生日は #{user.birthday} です" end |
実行すると「ActiveModel::MissingAttributeError: missing attribute: birthday」という例外が発生します。
当たり前ではあるのですが、select で full_name しか指定していないので、birthday にアクセスしようとすると例外が発生します。
しかし、そうではなくて、full_name 「も」SELECT してきてほしいんです。
full_name 「だけ」SELECT するのではなく、full_name「も」SELECT してほしいんです。
このように全てのカラムと計算した値(full_name)を SELECT したい、というケースは時々発生します。
以下のように対応することもできますが、あまり綺麗な解決ではありません。
1 2 3 4 5 |
# これだと scope ではないので、複数の場所に同じコードが散らばることになる. User.select('*, family_name || given_name AS full_name') # これだと select('*') の指定が非常に面倒で忘れやすい. User.select('*').select_full_name |
これならどうでしょうか?
1 2 3 4 5 6 7 |
class User < ActiveRecord::Base scope :select_full_name, lambda { select('*, family_name || given_name AS full_name') } end |
これだと以下のように、本当に指定したカラムだけ必要な場合に対応できません(select_full_name の中で select('*, ...') としているため)。
1 |
User.select(:email).select_full_name |
以上の課題を解決する方法はあるのでしょうか。
SELECTするカラムを追加するscopeを定義する
いきなり答えですが、以下のように実装することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class User < ActiveRecord::Base scope :select_full_name, lambda { # (1) 今の scope を取る (無ければ relation) scope = current_scope || relation # (2) select が指定されていなかったら select('*') する. scope = scope.select('*') if scope.select_values.blank? # それに対して本来やりたかった select を行う. scope.select('family_name || given_name AS full_name') } end |
(1) では現在の scope を取得しています。何も scope が指定されていない場合は nil が返されるので代わりに relation を取得します。
(2) では、if scope.select_values.blank? によって既に select が行われているか判断しています。select_values は select で指定されたカラムの一覧を返すメソッドです。
(3) でようやく本来やりたかった select('family_name || given_name AS full_name') を指定します。
こうすることで、既に select が指定されている場合はそれに追加、そうでなければ select('*') した上で追加する、ということが実現できます。
以下のように add_select_field というメソッドに分割しておけば、同様の処理を行う場合に便利でしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class User < ActiveRecord::Base scope :add_select_field, lambda {|*fields| scope = current_scope || relation scope = scope.select(Arel.star) if scope.select_values.blank? scope.select(*fields) } scope :select_age, lambda { add_select_field('EXTRACT(year from AGE(birthday)) AS age') } end |
Enjoy Rails!!