Ruby on Railsガイドを通読してまとめる Part.2
第二弾です。前回はこちら。
これのコールバック〜関連付け(アソシエーション)を読んでまとめました。
めちゃくちゃ長いです。
- モデル
- AvtiveRecord コールバック
- ActiveRecord アソシエーション
- 関連付けの種類(2)
- belongs_to と has_one どちらを選ぶか(2.7)
- has_many :throughとhas_and_belongs_to_manyのどちらを選ぶか(2.8)
- ポリモーフィック関連付け(2.9)
- 自己結合(2.10)
- 関連付けで知っておくべき注意事項(3)
- キャッシュ制御(3.1)
- 名前衝突の回避(3.2)
- スキーマの更新(3.3)
- 関連付けのスコープ制御(3.4)
- 双方向の関連付け
- アソシエーションの詳細情報
- belongs_toで得られるメソッド(4.1.1)
- belongs_toのオプション(4.1.2)
- belongs_toのスコープ(4.1.3)
- has_oneで得られるメソッド(4.2.1)
- has_oneのオプション(4.2.2)
- has_manyで追加されるメソッド(4.3.1)
- has_manyのオプション(4.3.2)
- has_manyのスコープ
- シングルテーブル継承 (STI)
- 感想
- 要検証
モデル
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
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 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自体に必要な設定を施してないとうまく働かない。
- belongs_to関連付けを使用する場合は、外部キーを作成する
- 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のクエリの発行の仕方