[Rails] ActiveRecord のスコープはレコード作成時に値をセットしてくれる
目次
久しぶりの投稿です。私は Rails を数年業務で使っているのですが、実は最近タイトルの件を知り、ベテランの Rails ユーザーにとっては初歩的な内容 (機能自体も rdoc に書いてある) なので記事にするか悩みましたが、自己の理解を深めると共に、弊社には Rails (ActiveRecord) の初学者のメンバーもいるのであえて書いておきます。
また、例によって本記事で扱ってるコードを GitHub に公開しているので是非ご覧下さい: https://github.com/issei-m/rails_test_ar_relations
検証したバージョン
Ruby: 3.1.2
Rails: 7.0.3
本記事中の「スコープ」について
本記事ではカナ表記の「スコープ」は ActiveRecord::Relation
の事を指します。これは何なのかと言うと、ある種のクエリビルダ機能を持ったモデルのコレクション表現 (0行以上の複数レコードを保持する) の様なものです。.where
メソッドでモデルの attribute で絞り込んだり、 exists?
による存在確認、 .count
による件数取得、 to_ary
で現在の絞り込み状態にマッチするレコードを Array に変換する事ができます。ActiveRecord::Base
に組み込まれているので、実際にはモデルクラスから使う事になります。
次の様なスキーマ、モデルが定義されているとします:
# schema
create_table "people", force: :cascade do |t|
t.string "nickname", null: false
t.integer "sex", limit: 1, null: false
t.integer "prefecture_id", limit: 1, null: false
end
create_table "prefectures", force: :cascade do |t|
t.string "name", null: false
end
# models
class Prefecture < ApplicationRecord; end
class Person < ApplicationRecord
belongs_to :live_in, class_name: :Prefecture, foreign_key: :prefecture_id
enum sex: [ :male, :female, :other ]
end
この様な構成の時、 Person.where(sex: :male)
(性別が男性の Person
のみを絞り込む) を実行すると返ってくる値が ActiveRecord::Relation
(実際にはそれを継承した Person::ActiveRecord_Relation
) になります。
さて、次に以下の様な scope
を Person
モデルに宣言します:
class Person
# ...
scope :male, -> { where(sex: :male) }
end
この場合、 Person.male
は Person.where(sex: :male)
と同義なので、返り値は ActiveRecord::Relation
になります。
この辺りは ActiveRecord の基本的な機能で、普段からよく目にしているかと思います。
ちなみに rails console を使っていると Person.male
を評価すると即時にクエリが実行され、 Person
の結果がリストで返ってきますが、これは rails console の機能でActiveRecord::Relation
の様な Enumerable
な値を評価させると、中身を表示する為に .to_a
が実行されるからです。普通にアプリ内で使っている場合は、 .each
や .map
(勿論 .to_a
も) などを実行する等してリストが必要になるまでクエリの実行は遅延されます。
ActiveRecord::Relation から新規レコードを作る
通常、 ActiveRecord モデルのレコードを新たに作る場合は、普通にモデルクラスの .new
や .create
を使うと思いますが、 ActiveRecord::Relation
からも行う事ができます。
先程の構成では Person.where(sex: :male).new
や Person.male.create
の様に実行でき、こうすると予め sex
に :male
がセットされたレコードが作られます。
males = Person.male
# 男性のレコードが作られている場合は一番先頭の物を、まだ作られていない場合は新規作成して返す.
male = if males.exists?
males.first
else
males.create!(nickname: "Taro", live_in: Prefecture.find(13))
end
因みにこれは普通に公式の rdoc にも書いてあります。
何が便利か?
既に先ほどのスニペットでも見せましたが、設定したスコープから直接レコードが作れるのでコードの記述量が少なく、文脈的になるので見通しが良くなります。
males = Person.male
males.create!(nickname: "Taro", live_in: Prefecture.find(13))
これは、 Person.create!(nickname: "Taro", live_in: Prefecture.find(13), sex: :male)
と同義になります。(Person
は、 sex
以外に nickname
, live_in (prefecture_id)
が必須項目なので別途設定が必要です)
勘の良い方なら、スコープは連鎖させられる事に気づいたかもしれません。 Person.male.where(live_in: Prefecture.find(13))
、あるいは
class Person
# ...
scope :living_in_tokyo, -> { where(live_in: Prefecture.find(13)) }
end
と言う scope
の設定がある場合、 Person.male.living_in_tokyo
の様に複数のスコープを連鎖させる事で、「東京都に住む男性」と言うスコープを作り出す事ができます。
この状態では、
males_living_in_tokyo = Person.male.living_in_tokyo
males_living_in_tokyo.create!(nickname: "Taro")
ご覧の通り、 nickname
の設定だけでレコードを作る事ができる様になりました。
has_many な物もいける
続いて、 Prefecture
から見た Person
として次の様な has_many
の関係を定義しましょう:
class Prefecture < ApplicationRecord
has_many :people_living_here, class_name: :Person
# tokyo のショートカットもついでに設定しておく
def self.tokyo
find(13)
end
end
この場合も、先ほどと同様の事が行えます:
males_living_in_tokyo = Prefecture.tokyo.people_living_here.male
males_living_in_tokyo.create!(nickname: "Jiro")
繰り返しますが、スコープは連鎖させても実態は ActiveRecord::Relation
なので件数取得や削除、更新もお手のものです:
males_living_in_tokyo.count # 東京に住む男性の件数
males_living_in_tokyo.update_all "nickname = nickname || ' (tokyo)'" # 東京に住む全ての男性の nickname に " (tokyo)" を付け足す
males_living_in_tokyo.destroy_all # 東京に住む男性を削除
実は many-to-many でも行ける
モデルが持つ attribute や、 belongs_to
で紐付くリレーションモデルは全て、当該モデルに所属するカラム (people
の nickname
, sex
, prefecture_id
) にセットされるのでこの様な動作になるのは分かるのですが、 many-to-many の場合はどうでしょうか?
試しに、 Person
モデルに prefectures_to_want_to_live_in
と言う、 Prefecture
と many-to-many な関係を作ってみます:
# schema
create_table "prefectures_person_wants_to_live_in", force: :cascade do |t|
t.integer "prefecture_id"
t.integer "person_id"
t.index ["person_id"], name: "index_prefectures_person_wants_to_live_in_on_person_id"
t.index ["prefecture_id"], name: "index_prefectures_person_wants_to_live_in_on_prefecture_id"
end
# models
class PrefecturePersonWantsToLiveIn < ApplicationRecord
self.table_name = :prefectures_person_wants_to_live_in
belongs_to :prefecture
belongs_to :person
end
class Person < ApplicationRecord
belongs_to :live_in, class_name: :Prefecture, foreign_key: :prefecture_id
has_many :prefectures_person_wants_to_live_in, class_name: :PrefecturePersonWantsToLiveIn
has_many :prefectures_to_want_to_live_in, through: :prefectures_person_wants_to_live_in, source: :prefecture
# ...
end
この様なモデルの場合、 Prefecture
では Prefecture.tokyo.people_wanting_to_live_here
の様なスコープを取得できます。試しにこれを create!
してレコードを作ってみましょう:
# 東京に住みたいと考えている人
people_wanting_to_live_in_tokyo = Prefecture.tokyo.people_wanting_to_live_here
person_wanting_to_live_in_tokyo = people_wanting_to_live_in_tokyo.create!(nickname: "Saburo", sex: :male, live_in: Prefecture.find(1))
SQL のログを見ると、きちんと “prefectures_person_wants_to_live_in” のレコードも一緒に作られた事が分かります。実際に person_wanting_to_live_in_tokyo.prefectures_to_want_to_live_in
の結果は [Prefecture.tokyo]
が返ってきます。便利ですね。
ただし、注意したいのは new
でレコードを作った場合は many-to-many の関係は自動で生成されないと言う事です。 create!
により、その場で作る必要があります。(create
でも可能ですが、バリデーションエラーでレコードが戻ってきた場合、そのレコードについては new
同様に自動でリレーションはされないので注意)
ところで、 people_wanting_to_live_in_tokyo
は Person
のスコープ (ActiveRecord::Relation
) になっています。 (Prefecture
ではない点に注意) なので、更に以下の様にチェーンする事も勿論できます:
# 北海道に住む、東京に住みたいと考えている男性を作成
people_wanting_to_live_in_tokyo.male.create!(nickname: "Saburo", live_in: Prefecture.find(1))
sex
の設定を省略できました。 Person
に living_in_hokkaido
とかがある場合、更にこれも省略できる事は言うまでもありませんね。
まとめ
今回は、 ActiveRecord::Relation
の便利な機能についてまとめました。ごく基本的な機能ですが、使いこなすとコードの可読性がグッと上がりそうです。
また Rails は業務で4年くらい使っていて本家にプルリクをたまに送ったりするくらいには使っていますが、未だに新たな発見があり、その度によくできたフレームワークの1つだなと感心します。
コメントを残す