最近、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") # => []