Railsガイドにきちんと目を通して新しい知識を得る - Active Recordの関連付け編 -

ドキュメントを読み込むのは大事、ということでRailsガイドを頭から読んでいく取り組みをしています。 各章ごとに、(Railsガイドにちゃんと書いてあるのに)知らなかった機能を雑にまとめていきます。

今回は、Active Recordの関連付けの章です。

railsguides.jp

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>