LoginSignup
73
65

More than 5 years have passed since last update.

プレーンなRubyオブジェクトでhas_manyとbelongs_toを使う

Last updated at Posted at 2014-05-08

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
73
65