ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。
今回は、Active Recordの関連付けの章です。
reload_xxxと、changerd?previously_changed?
リンクはこちら
reload_xxx
テストを書くときに、xxx.reload
みたいな使い方はよくしていましたが、関連モデルに使えるのは知りませんでした。
# (コンソールAで) book.user #=> #<User id: 1, email: "sample@example.com", created_at: ...> # (別のコンソールBで) User.find(1).update(email: 'sample02@example.com') # (コンソールAに戻って) book.user #=> #<User id: 1, email: "sample@example.com", created_at: ...> book.reload_user #=> #<User id: 1, email: "sample02@example.com", created_at: ...>
changed?, previously_changed?
changed?
は保存前、previously_changed?
は保存直後に判定をしてくれます。
ActiveModel::Dirtyというモジュールで提供されているとのこと。
product = Product.last product.price = 100 product.price_changed? #=> true product.price_previously_changed? #=> false product.save product.price_changed? #=> false product.price_previously_changed? #=> true
belongs_toで使える:autosave, :touch オプション
リンクはこちら
:autosave
class User < ApplicationRecord has_many :books end class Book < ApplicationRecord belongs_to :user, autosave: true end
この場合、子モデルのレコードが保存されるときに、親モデルのレコードも保存処理が走る
user = User.last user.email = 'update@example.com' book = user.books.last book.save # ここでuserも保存される User.last.email # => 'update@example.com'
逆に、falseだと、デフォルトで親モデルが保存される場合も保存されなくなる
class Book < ApplicationRecord belongs_to :user, autosave: false end user = User.new(email: 'aaa@example.com') book = user.books.new book.save # => NOT NULL constraint failed: books.user_id (SQLite3::ConstraintException)
:touch
関連モデルのレコードのupdated_at
が更新される
class Book < ApplicationRecord belongs_to :user, touch: true end user = User.last book = user.books.last book.title = 'update' book.save # ここで、userのupdated_atも現在時刻が入る
belongs_toで使えるスコープ
リンクはこちら
where
以下のように書くと、emailにsample123@example.com
を持つuserしかbookを関連として持てなくなる
class Book < ApplicationRecord belongs_to :user, -> { where email: 'sample123@example.com' } end user = User.create(email: 'test@example.com') user.books.create! # => ActiveRecord::RecordInvalid user = User.create(email: 'sample123@example.com') user.books.create! # => 成功
readonly
以下のように書くと、bookオブジェクトからuserオブジェクトは読み出し専用になる
class Book < ApplicationRecord belongs_to :user, -> { readonly } end book = Book.last book.user.update!(email: 'update@example.com') # => ActiveRecord::ReadOnlyRecord
select
selectで特定のカラムだけ取得できる。
重たいテーブルを読み出すけど特定のカラムしか使わない、というときに使えるのかなと思います。
class Book < ApplicationRecord belongs_to :user, -> { select :id } end book = Book.last book.user # => #<User id: 7>
has_manyで追加されるcollection_singular_ids
リンクはこちら
idの配列ベースで、関連の付け替えや新規関連付けができる。
付け替えの場合は、元のidがnilになったりするので使えるタイミングは限定されるかもですね。=
を使うとupdate文が即座にかかるということをきちんと理解する必要がありそうです。
新しく関連付けをするときの方が使うケースはあるかもなーと思いました。
Memo.all # => [#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 7>, #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 7>, #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: 1>, #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: 1>] user = User.find(1) user.memo_ids # => [3, 4] user.memo_ids = [1, 2] # ここでupdateされる Memo.all # => [#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 1>, #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 1>, #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: nil>, #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: nil>] user = User.find(2) user.memo_ids = [3, 4] Memo.all # => [#<Memo:0x000000010aa64bf8 id: 1, content: "sample01", user_id: 1>, #<Memo:0x000000010aa64b30 id: 2, content: "sample02", user_id: 1>, #<Memo:0x000000010aa64a18 id: 3, content: "sample03", user_id: 2>, #<Memo:0x000000010aa64950 id: 4, content: "sample04", user_id: 2>]
関連付けのコールバック
リンクはこちら
before_add
などのコールバックの存在自体知りませんでした、、
class User < ApplicationRecord has_many :memos, before_add: :before_add_method private def before_add_method(memo) p 'before_add called' end end user = User.first user.memos.new # => 'before_add called'
関連付けの拡張
リンクはこちら
普通に関連モデルにscopeとかクラスメソッドとか書いて解決している気がしますが、コンテキストが限られているのならアリな場面もあるかも?と思ったりしました。
class User < ApplicationRecord has_many :memos do def find_by_memo(content) find_by(content: content) end end end user = User.first user.memos.find_by_memo('sample01') # => #<Memo:0x000000010673e158 id: 1, content: "sample01", user_id: 1>