has_many
*1にblockを渡すとassociationにメソッドを追加することができる。
class User < ActiveRecord::Base has_many :posts do def stats group(:status).count # このcontextで`self`は`ActiveRecord::Associations::CollectionProxy` # `proxy_association.owner`で`user.posts`の`user`にアクセスすることもできる end end end user.posts.stats # => {'draft'=>3,'published'=>42}
内部的にはblockがModule.new(&block)
されてextend
optionに渡される。なので始めからextend
optionとして渡してやってもよい。
module StatsCounter def stats group(:status).count end end class User < ActiveRecord::Base has_many :posts, extend: StatsCounter # has_many :posts, extend: [StatsCounter] でもよい end user.posts.stats # => {'draft'=>3,'published'=>42}
ちなみにblockを渡した際にnew
されたModule
はActiveRecord::Associations::Builder.define_extensions
によってUser
classにUser::PostsAssociationExtension
のような名前でconst_set
される*2。
使いどころを考える
子テーブルの絞り込みを行うfinderを書くなら子テーブルのほうのモデルのscope
でよいし、何を書いても子テーブル側の詳細や知識が親テーブル側のモデルに漏れている感じがする。
親テーブルから辿ったときにしか使わせたくない、つまり対象となるassociationの一部としてのみ扱いたいロジックを書きたいときが使いどころといえそう。
公式ドキュメントではファクトリーメソッドのような例が挙げられている。
class Account < ActiveRecord::Base has_many :people do def find_or_create_by_name(name) first_name, last_name = name.split(" ", 2) find_or_create_by(first_name: first_name, last_name: last_name) end end end person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") person.first_name # => "David" person.last_name # => "Heinemeier Hansson"
うーん、上記の例でもPeople
classに書いたほうが良さそうな気もする
This article corresponds to the 7th day of ohbarye Advent Calendar 2020.
*1:新規に足すことはほぼないと思うけどhas_and_belongs_to_manyでもできる
*2:https://github.com/rails/rails/blob/939fe523126198d43ecedeacc05dd7fdb1eae3d9/activerecord/lib/active_record/associations/builder/collection_association.rb#L24-L26