Search code examples
ruby-on-railsrubyactiverecordactivemodelacts-as-audited

How to make a plain ruby object assignable as active record association


I have an Audit class which is backed by ActiveRecord.

class Audit < ActiveRecord::Base
  belongs_to :user, polymorphic: true
end

I have a User class which is a plain ruby object with some ActiveModel functionality included. It's not a database model because my users are actually stored in a different database and served over an API.

class User
  include ActiveModel::Conversion
  include ActiveModel::Validations
  extend  ActiveModel::Naming

  def self.primary_key
    'id'
  end

  def id
    42
  end

  def persisted?
    false
  end
end

I'm trying to assign a user to an audit like this:

audit = Audit.new
audit.user = User.new
audit.save!

From a data perspective, this should work ok. To define a polymorphic association, we need to put values into two columns on the audits table. We can set audits.user_id to the value 42 and audits.user_type to the string "User".

However, I hit an exception:

undefined method `_read_attribute' for #<User:0x007f85faa49520 @attributes={"id"=>42}> (NoMethodError)
active_record/associations/belongs_to_polymorphic_association.rb:13:in `replace_keys'

I traced that back to the ActiveRecord source and it seems to be defined here. Unfortunately, the fact that it's ActiveRecord rather than ActiveModel means that I can't include that mixin into my class.

I tried defining my own _read_attribute method but I go down a rabbit hole of having to redefine more and more ActiveRecord functionality like AttributeSet and so on.

I also realise that I can workaround the problem by assigning Audit#user_type and Audit#user_id. That is unsatisfactory however because, in reality, I would have to fork a gem and edit it to do that.

How can I modify my User class so that I can cleanly assign it to an audit.

P.S. Here's a sample app so you can try this yourself.


Solution

  • Instead of hacking deeper and deeper to replicate ActiveRecord functionality, you may want to consider actually inheriting from ActiveRecord::Base instead of including ActiveModel. Your only constraint is that you don't have a table. There's a gem for that:

    activerecord-tableless

    This class works with your sample app:

    require 'active_record'
    require 'activerecord-tableless'
    
    class User < ActiveRecord::Base
      has_no_table
    
      # required so ActiveRecord doesn't try to create a new associated
      # User record with the audit
      def new_record?
        false
      end
    
      def id
        42
      end
    end