valid,invalid

関心を持てる事柄について

ActiveRecord Association extensionsでメソッドを追加する

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されたModuleActiveRecord::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.