Web備忘録

雑多に書いています

Ruby on Railsガイドを通読してまとめる Part.2

第二弾です。前回はこちら。

fujiten3.hatenablog.com

railsguides.jp

これのコールバック〜関連付け(アソシエーション)を読んでまとめました。

めちゃくちゃ長いです。

モデル

AvtiveRecord コールバック

ActiveRecordコールバックとは、ActiveRecordを継承したクラスのインスタンス(User.new等)について、それにsaveやupdateを実行したさいにトリガーされる処理のこと。

before_saveといったトリガと、それに呼び出されるメソッドのことですね。

ActiveRecordのオブジェクトライフサイクルへのフック」と言ってもよい。

コールバックの登録(2.1)

一般的に以下のような記述でコールバックを定めておく。

class User < ApplicationRecord
  #バリデーションの前にコールバックメソッドを実行
  before_validation :normalize_name,
 
  # :onは配列を取ることもできる。この場合はcreateアクションとupdateアクションのみにトリガし、destroyではスルー。
  after_validation :set_location, on: [ :create, :update ]
 
  # コールバックはprivateにしておくことが望ましい
  private
    def normalize_name
      self.name = name.downcase.titleize
    end
 
    def set_location
      #selfはUserクラスのインスタンス自身を表す。
      self.location = LocationService.query(self)
    end
end

個人的に気になった細かい注意点としては、コールバックはUserクラスの特異メソッドとしてでなく、インスタンスメソッドとして定めるというところかな。バリデーションをするかしないか決定するのはUserクラスだけど、その処理自体の内容はインスタンス自身が知っているように定めておく、 ということらしい。

利用可能なコールバック(3)

before_validation、before_saveといったもの。

結構量があるけど、挙動が複雑なのはaround_saveやaround_createといったaround_*系のみだろうか。ガイドで詳しい説明はなかったけど、ググってみた所どうやらサンドイッチ的に処理を書けるようです。

以下、実際に挙動を確認してはいないけど、コードに起こしてみました。たぶんちゃんと動いてくれる。

class User < ApplicationRecord
  around_save :do_around_save
  
  def do_around_save
     #save前の処理を記述
     yield #saves
     #save後の処理を記述
  end

end

この場合は yield によってインスタンスにsaveが実行されるってことですね。

参考記事:

ruby - Rails: around_* callbacks - Stack Overflow

ActiveRecord::Callbacks

after_initializeとafter_find

初めて見たコールバック。こんなのあるんですね。

after_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出される。インスタンス化は、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われる。

インスタンス自体が生まれたときに使えるわけだから、インスタンスに対して最初に実行しておきたい処理とかを記述できるということだろう。

いやあActive_Record先輩すごいぜ。何でもやってくれるな。

挙動がそのまま乗ってたので掲載します。

class User < ApplicationRecord
  after_initialize do |user|
    puts "オブジェクトは初期化されました"
  end
 
  after_find do |user|
    puts "オブジェクトが見つかりました"
  end
end
 
>> User.new
オブジェクトは初期化されました
=> #<User id: nil>
 
>> User.first
オブジェクトが見つかりました
オブジェクトは初期化されました
=> #<User id: 1>

after_touch(3.5)

touchメソッドの後に実行。touch自体知らなかったけど、update_atを更新したい場合に使えるらしい。そのインスタンスにタッチするってことですね。

挙動例にスマートなコードがあったので掲載。親子関係のモデルで、子どもにタッチしたときの動き。

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'Employeeモデルにtouchされました'
  end
end
 
class Company < ApplicationRecord
  has_many :employees
  after_touch :log_when_employees_or_company_touched
 
  private
  def log_when_employees_or_company_touched
    puts 'Employee/Companyにtouchされました'
  end
end
 
>> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
 
# @employee.company.touchをトリガーする
>> @employee.touch
Employeeにtouchされました
Employee/Companyにtouchされました
=> true

子どもにタッチしたとき、子ども側で「belongs_to :parent, touch: true」といったオプションがあれば、親側もタッチされる。(そして親のupdated_atも更新される)。親側がタッチされたとき、それにコールバックする処理を書いておくことも可能ということですね。連鎖するタッチ。優しさの連鎖。

コールバックの実行(4)

コールバックを実行するメソッドのうち「toggle!」だけ知らなかったので調べたら、true/falseを反転させて保存という処理を行うそうです。保存処理(save)にトリガしてコールバックがかかるんでしょうね。「toggle」単体だとただtrue/falseを変えるだけ。

リレーションシップのコールバック(7)

インスタンスのdestroyにフックして子インスタンスのdestroyを呼び出す処理にしていたとき、子側でコールバックを定められる。

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
 
class Post < ApplicationRecord
  after_destroy :log_destroy_action
 
  def log_destroy_action
    puts 'Post destroyed'
  end
end
 
>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>

コールバックで複数の条件と指定する(8.3)

記事にコメントをするさい、記事の作者が「メール希望」であり、かつ「その記事がコメント無視する設定にしていない」ときにだけ作者にメールを送りたいときのコードが以下。

class Comment < ApplicationRecord
  after_create :send_email_to_author, if: :author_wants_emails?,
    unless: Proc.new { |comment| comment.post.ignore_comments? }
end

Proc.new{}の処理はメソッドにしてもよい。

コールバッククラス(9)

この章で一番難しいな、これな。

バリデーションをクラスで切り取れるように、コールバックの処理自体もクラスで切り取り、いろいろなモデルに流用できるよという概念。

ただコードへの解説の少なさがハンパなかったので自分で調べました。

# コールバッククラスに処理を委譲する(今回の場合はPictureFileCallbacks.newに委譲)
class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks.new
end


class PictureFileCallbacks
  #トリガした側が「after_destroy」で委譲した場合、委譲された側では「after_destroy(record)」が実行される。
  #recordとはつまり、トリガした側のインスタンスのこと。ここでは「PictureFile.find(id: n)」が渡されるようになっている。
  def after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

参考記事読みながら頑張って理解しましょう! 実務でもガンガン出てきそうです(想像です)

てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ! - Qiita

トランザクションコールバック(10)

もっと難しいとこきた!!

after_crate_commit と after_createの違いについて話してくれていますが…かなり難解。

自分の解釈を書きますが、怪しいところもあるので自分で確認してください。

2つの違いはトランザクションの範囲(例外が出たさい、ロールバックする範囲)がポイントに見える。「after_crate_commit」の場合は、それが指定するコールバック内で例外が起きても、コミットした内容までロールバックすることはなさそう。

つまり、「Aを作成したあと、Bを削除する処理」を書く場合、after_crate_commit でAを作成したときは、Bの削除で例外が起きるとAの作成自体までロールバックされることはない。一方、after_createの場合は一つのトランザクション内に存在している動作だから、Bの削除における例外でAの作成自体もロールバックする(Aは生まれない)という処理になる、ということかなあ。

正直、難しい。これは自分でコード起こさないと理解できなさそうです。またのちほど検証します。

参考記事:

ruby - Difference between after_create, after_save and after_commit in rails callbacks - Stack Overflow

ruby on rails - after_create :foo vs after_commit :bar, :on => :create - Stack Overflow

ActiveRecord アソシエーション

関連付けの種類(2)

Railsでサポートされている関連付けは以下の6種類。

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

6種類しかなかったのね。もっとあるかと思ってた。

belongs_to と has_one どちらを選ぶか(2.7)

区別の決め手の一つは「外部キー」をどちらに置くのか、ということ。ほかにも、どちらがどちらに所属するのか考えるのも大事。たとえば供給者とアカウントの関係を考えたとき、アカウントが供給者に属すると見たほうが自然。ということで、以下のようなマイグレーションファイルが望ましい。

マイグレーションファイルの例。


class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.integer :supplier_id
      t.string  :account_number
      t.timestamps
    end
 
    add_index :accounts, :supplier_id
  end
end

t.integer :supplier_id は t.references :supplier と書いてもよい。同じ意味。 indexは自分で貼りましょう。

referencesで関連付けると、foreign_key: trueオプションをつかって、DB自体に参照整合性を設定できる。強い。大事ですね。

参考記事:

Railsの外部キー制約とreference型について - Qiita

has_many :throughとhas_and_belongs_to_manyのどちらを選ぶか(2.8)

似てるように見えて、違いがあります。どちらも中間テーブルが必要ですが、has_and_belongs_to_manyはその中間テーブルをモデルとして扱わない。「ただつなげる」だけ。

has_many :throughは、結合モデルを作るので、バリデーションやコールバック、追加の属性の設定などができる。

DBは一度作ると変更コストが大きいので、しっかり考えて作りたいですね。ここテキトーに選ぶと負債が大きそう。

ポリモーフィック関連付け(2.9)

高等テクニック。1つのモデルが他の複数のモデルに属すことを、1つの関連付けだけで表現できる。

SQLアンチパターン読んでたときにアンチパターン気味に紹介されてはいたけど、「のちのちへの対応力が高い」ということで、たしか拡張性の面では評価されてた。

従業員、製品から「Pictureモデル」にアクセスしたいときのポリモーフィック関連の記述法。

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

これで@employee.picturesや@product.picturesで写真を引っ張ってこれるし、逆に@picture.emageableで親モデルのタイプを知ることなく親を引っ張ってこれる。あとはEmployeeとProductに同名のメソッドを作って、それぞれの振る舞いさえ記述して上げれば、まさにオブジェクト指向的なコードになりそう。

つまりこんな感じですね。

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable

  def greet
    puts "こんにちは!"
  end

end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable

  def greet
    puts "コンニチハ"
  end

end

マイグレーションファイルはこのようにしましょう。

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

これで、picturesテーブルのカラムに「imageable_type」と「imageable_id」カラムが追加されます。 imageable_typeはモデル名がString型で格納されてて、それでどの親モデルのidに属してるか判断します。

自己結合(2.10)

1つのテーブル内での関連付け。Employeeテーブルがマネージャと部下をもつ場合などに使う。

関連付けで知っておくべき注意事項(3)

  • キャッシュ制御
  • 名前衝突の回避
  • スキーマの更新
  • 関連付けのスコープ制御
  • 双方向関連付け

キャッシュ制御(3.1)

オブジェクトは実行したクエリの結果を保持する。再読込したい場合はreloadメソッドを実行しよう。

author.books                 # データベースからbooksを取得する
author.books.size            # booksのキャッシュコピーが使用される
author.books.reload.empty?   # booksのキャッシュコピーが破棄される
                             # その後データベースから再度読み込まれる

名前衝突の回避(3.2)

関連付け名はメソッドとして使われるため、ActiveRecord::Baseが持つメソッドと被らないように。「belongs_to :save」みたいな関連付けしたらダメだよってことですね。

スキーマの更新(3.3)

コード上で関連付けてもDB自体に必要な設定を施してないとうまく働かない。

  1. belongs_to関連付けを使用する場合は、外部キーを作成する
  2. has_and_belongs_to_many関連付けを使用する場合は、適切な結合テーブルを作成する

2について、中間テーブルにはプライマリキーを設定しない。設定すると動作不良を起こします。

関連付けのスコープ制御(3.4)

関連付けのさい、オブジェクトは現在のモジュールのスコープ内で探索を行う。 別の名前空間同士での関連付けの場合はclass_nameとして、完全なクラス名を宣言しなければならない。

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account,
      class_name: "MyApplication::Billing::Account"
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier,
      class_name: "MyApplication::Business::Supplier"
    end
  end
end

双方向の関連付け

基本的に、親モデル側からのあるインスタンスに対して、子側からアクセスしにかかっても、同じインスタンスを参照するようにActiveRecordは働く。これが双方向の関連付け。

しかし子側で:through、または:foreign_keyオプションを使ったさいは、inverse_ofオプションを使って親側からも子側を指定しないと、双方向にならない。

class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

詳しい説明見たい場合はガイドへGo!

アソシエーションの詳細情報

アソシエーションすることで得られるメソッドの解説。

AvtiveRecordっていろんなことしてくれるんですね(ActiveRecordへの感謝定期)

belongs_toで得られるメソッド(4.1.1)

全部で6つ。よく見るのばかり。

#association という言葉は、関連付け相手のテーブル名に動的に代わります

association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association

build_associationだけ解説。(associationという言葉はbelongs_to :author といった関連付けをすればauthorに置き換わる)

@author = @book.build_author(author_number: 123,
                                  author_name: "John Doe")

子側から親のインスタンスを作る動きですね。作った時点ではDBに保存されない。

create_asociationで親のインスタンスを作れば、作ったときに保存処理が行われる(なのでもちろんバリデーション等に弾かれる可能性もある)

belongs_toのオプション(4.1.2)

全部で11個!  多いぜ…。

  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :primary_key
  • :inverse_of
  • :polymorphic
  • :touch
  • :validate
  • :optional

知らなかったもの、かつ重要そうなものをまとめていきます。

:autosave

APIガイド読みましたが、挙動の詳細がいまいちつかめません。 Railsガイドといろいろなサイト見たところ、つまりこういうことかもしれない。

  • autosave: true → 親オブジェクトのsaveで、「子モデルの作成・更新」をともに保存する。
  • autosave: false → 親オブジェクトのsaveで、「子モデルの作成・更新」をともに保存しない。
  • autosave未設定 →親オブジェクトのsaveで、「子モデルの作成」は保存し、「子モデルの更新」は保存しない。

挙動チェックまではしてないので、実装の際はご注意を!

参考記事: ActiveRecord::AutosaveAssociation

:class_name

実際のモデル名を指定するオプション。 メソッドとしてauthorで呼びたいが、モデル名がPersonのときは以下の設定。

class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron"
end

:counter_cache

親モデルが持つ子モデルの数をキャッシュしておく設定。

詳細、結構調べましたが、古い記事が多く難しかったです。とりあえずクエリ発行の結果をキャッシュするという挙動はわかったんですが、クエリ結果のIntegerを普通に変数に保持しておいてアレコレするっていう動作の方が安全に思えるんですけどどうなんでしょうね。

実務で見かけたらまた調べようと思います。

counter_cacheを強化したcounter_cultureというgemもあるみたいです。

Rails tips: ActiveRecord count系機能の基本と応用(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

Railsで関連レコード数の集計(カウンターキャッシュ) | 株式会社ランチェスター

:dependent

:destroyで関連付けのdestroyメソッドを実行。:deleteだと直接DBから削除。

子側の削除で親側の削除をフックすることはあまりなさそう。

:foreign_key

自分が持つ外部キーのカラム名を指定。

:primary_key

親側のprimary_keyがself.primary_key=hogeで変更されてる場合は、子側でも明示しないと参照できない。

:inverse_of

前述した通り。inverseは逆側という意味で、関連付けしているモデル目線から見た自分の呼び出し方を記述する。

:polymorphic

前述した通り。

:touch

上でも解説したが、足りないので補足。

関連しているオブジェクトの保存または削除に合わせて自分のupdated_atを更新する。

:validateと:optional

説明が薄かったので割愛。

belongs_toのスコープ(4.1.3)

アソシエーションが自動的につくるクエリをカスタマイズしたいとき、独自に設定することが出来る。

class Book < ApplicationRecord
  belongs_to :author, -> { where active: true },
                        dependent: :destroy
end

この記述をしておけば、book.authorで発行するクエリの条件に、active: trueが付随してくれる。 他にも状況に応じて3つほどオプションがあったけど、このwhereの例が一番頻出しそう(想像)。

has_oneで得られるメソッド(4.2.1)

belongs_toとほとんど同じ。

has_oneのオプション(4.2.2)

belongs_toとほぼ同じだが、大きく違うものだけ見ていく。

:dependent

親側オブジェクトの削除に対して、それに紐づく子側の挙動を設定できる。

  • :destroyを指定すると、関連付けられたオブジェクトも同時にdestroyされます。
  • :deleteを指定すると、関連付けられたオブジェクトはデータベースから直接削除されます。このときコールバックは実行されません。
  • :nullifyを指定すると、外部キーがNULLに設定されます。このときコールバックは実行されません。
  • :restrict_with_exceptionを指定すると、関連付けられたレコードがある場合に例外が発生します。
  • :restrict_with_errorを指定すると、関連付けられたオブジェクトがある場合にエラーがオーナーに追加されます。

:nullifyはDBのNOT NULL制約に弾かれるので注意。というか:nullifyオプション自体使用頻度低そう。

:primary_key

詳細が書いてなかったけど、子側のprimary_keyが変わってたらこのオプションで明示しないとダメなんだろう。 おそらくそういうことは滅多にない。

has_manyで追加されるメソッド(4.3.1)

全部で17個!!?マジ?? belongs_toは6個だったのに… 

#collection という単語は関連付け先のテーブル名に動的に代わります。

collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {}, ...)
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload

collection.delete(object, ...)とcollection.destroy(object, ...)の違い

うーん、ここガイドが逆なように感じた。

ただAPIリファレンスからすぐに見つけられなかったから、一旦そのまままとめておきます。

書いてあることとしては「collection.delete(obj,...)はデフォルトでは外部キーにNULLセットで子モデルを削除扱い、親モデルにdependentがあればそれに応じてdestroyなりdelete」。一方「collection.destroy(obj,...) は問答無用でDBから削除」という挙動だそうです。

deleteがモデル層見つつ削除して、destroyが直接クエリ打つって、なんか逆に感じる。。

うーむ、また調べておきたい。

collection.clear(4.3.1.8)

親モデルの:dependentオプションに応じながら全削除。

@author.books.clear

@authorをdestroyするわけじゃないけど、まるでdestroyしたような挙動になりますね。 親モデルを一旦論理削除しておくけど、それに紐づく子モデルはDBから物理削除しておきたい、みたいなときに使うのかな。

collection.where(...)(4.3.1.12)

条件を指定して検索できるが、遅延読み込みされる点に注意。

#クエリはまだ発行されない
@available_books = @author.books.where(available: true) 

#クエリが発行される
@available_book = @available_books.first

has_manyのオプション(4.3.2)

has_oneとほぼ同じ。

has_manyのスコープ

has_oneより種類がある。重要そうなものを抜粋。

includes(4.3.3.4)

第2関連付けをeagar_loadしたいときに使う。

class Author < ApplicationRecord
  has_many :books, -> { includes :line_items }
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class LineItem < ApplicationRecord
  belongs_to :book
end

@author.books.line_itemsを取り出す頻度が多いときに予めincludeしておける、とのこと。

@author.booksでクエリ発行したときに、line_itemsのことも考えてincludeしてキャッシュしておくということでしょうかね。

@author.books.line_items単体のクエリのパフォーマンスが上がる(SQLの効率化)ってわけではなさそうなんだけどどうなんだろう? 要検証。

limitやoffsetやorderも

あらかじめ設定しておけるみたいだけど、author.booksに暗黙的にlimit(100)させたりするより、コントローラー側でlimit(100)するか、クラスで使用頻度高いクエリをまとめたscopeを作っておいた方が良さげに思えますね。

まあ、あくまで状況に応じてということでしょうが。

distinct

中間テーブルを介して多:多で、相手のレコードを探しにいくとき、中間テーブルの一意性を検証して、「参照前と参照後が同じであるレコード」が合った場合に、それが1つになるよう弾いてくれる。

DB自体で「その中間テーブルの一意性」を保証した場合は、以下の設定をしておこう。

add_index :readings, [:person_id, :article_id], unique: true

これをしておけば、「著者と記事のidが全く同じ中間テーブルが2つ以上ある」という状況が起きない。

include?などで一意性チェックしにかかるのはアンチパターンなのでやめましょう。

シングルテーブル継承 (STI

あるテーブルにtypeカラムを作り、そのテーブルを他に継承させる。 そうすることで、共通のメソッドやテーブルのカラムを再利用できる。

が…アンチパターン気味にしか見えない…。

現場Railsでも「STIはよっぽど同じ条件のモデルたちがあるときじゃないと使うべきじゃない(モジュールとかで対応できるよね)」って書いてた気がするし、安易に作らないようにしたい。

感想

大事だなあと思う所が多くて長くなりました。ぜんぜんまとめれてねえ。

内容はめちゃくちゃ為になるので、Part.3も引き続きやっていこうと思います。

要検証

  • トランザクションコールバック
  • collection.delete(object, ...)とcollection.destroy(object, ...)の違い
  • has_manyのオプションにおけるincludesのクエリの発行の仕方