最近、Railsで開発していると、ActiveModelを使ってプレーンなオブジェクトを拡張してモデルとして利用する事が増えてきた。
そういうクラスを定義していると、しばしばActiveRecordなオブジェクトに対する参照が欲しくなる事がある。
で、単純にこういう定義をしたりする。
class Comment
  include ActiveModel::Model
  attr_accessor :body, :user_id
  def user
    @user ||= User.find_by(id: user_id)
  end
  def user=(user)
    self.user_id = user.id
    @user = user
  end
end
とは言え、タイプチェック入れたり、同じような定義がいくつも入ると邪魔である。こういうものは宣言的に済ませてソースコードには、もっとドメインのコードを表現したい。
で、ふと思い付いてActiveModelを利用したオブジェクトから直接ActiveRecordのhas_manyとかbelongs_toを呼べないものかと試してみた。
joker1007/activemodel-associations
Railsのソースを読んで夜にせっせと内職することで、何とか形にできた。
しかし、ActiveRecordの内部APIをオーバーライドしまくってるので、元のAPIがゴリっと変わると即死ぬという危険を秘めている。
もっと単純なDSL定義しても十分だったかもしれない……。
まあ、おかげでActiveRecordのリレーション回りがどうやって定義されてるか大体分かった気がするので、良かったか。
実装の方針としては、内部で使ってるメソッドをいくつかクラスに定義してActiveRecordをごまかしつつ、どうにもならん所だけモジュールをprependして処理奪って独自の関連定義クラスに差し替えている。
has_manyに関してはかなり限定的な動作だけをサポートしていて、使い所が無かったり実装が辛そうなオプションは無効化してある。
しかし、以前にQiitaに投稿したモジュールからメソッドを一つだけ借用するというテクニックを本当に使う機会があるとは思わなかった。
cf. Ruby - UnboundMethodを使って他のモジュールからメソッドを借りる - Qiita
ActiveRecordは流石に良く出来ていて、関連を定義するために必要なメソッドはそんなに多くなかった。
Associationクラスをもうちょっとしっかり実装すれば、勝手に独自の関連タイプを定義できそうな気がする。
とりあえず手元のユースケースは満たしているのだが、Railsがアップデートされるとすぐ壊れそうなので、プレーンなRubyオブジェクトからActiveRecordへの関連を定義する機能をRails本体に追加して欲しい所。
使い方
belongs_to
class User < ActiveRecord::Base; end
class Comment
  include ActiveModel::Model         # need ActiveModel::Model
  include ActiveModel::Associations  # include this
  attr_accessor :body, :user_id # belongs_to association need foreign_key attribute
  belongs_to :user
  # need hash like accessor, used internal Rails
  def [](attr)
    self.send(attr)
  end
  # need hash like accessor, used internal Rails
  def []=(attr, value)
    self.send("#{attr}=", value)
  end
end
user = User.create(name: "joker1007")
comment = Comment.new(user_id: user.id)
comment.user # => <User {name: "joker1007"}>
Polymorphic belongs_to
class User < ActiveRecord::Base; end
class Comment
  include ActiveModel::Model         # need ActiveModel::Model
  include ActiveModel::Associations  # include this
  attr_accessor :body, :commenter_id, :commenter_type
  belongs_to :commenter, polymorphic: true
  # need hash like accessor, used internal Rails
  def [](attr)
    self.send(attr)
  end
  # need hash like accessor, used internal Rails
  def []=(attr, value)
    self.send("#{attr}=", value)
  end
end
user = User.create(name: "joker1007")
comment = Comment.new(commenter_id: user.id, commenter_type: "User")
comment.commenter # => <User {name: "joker1007"}>
has_many
class User < ActiveRecord::Base; end
class Group
  include ActiveModel::Model
  include ActiveModel::Associations
  attr_accessor :name
  attr_reader :user_ids
  has_many :users
  def [](attr)
    self.send(attr)
  end
  def []=(attr, value)
    self.send("#{attr}=", value)
  end
end
user = User.create(name: "joker1007")
group = Group.new(user_ids: [user.id])
group.users # => ActiveRecord::Relation (SELECT * from users WHERE id IN (#{user.id}))
group.users.find_by(name: "none") # => []