Search code examples
ruby-on-railsrubyactiverecordruby-on-rails-3single-table-inheritance

ActiveRecord Problems using callbacks and STI


Hey folks, following problem with Rails and STI:

I have following classes:

class Account < AC::Base
  has_many :users
end

class User < AC::Base
  extend STI
  belongs_to :account

  class Standard < User
    before_save :some_callback
  end

  class Other < User
  end
end

module STI
  def new(*args, &block)
    type = args.dup.extract_options!.with_indifferent_access.delete(:type)
    if type.blank? or (type = type.constantize) == self
      super(*args, &block)
    else
      type.new(*args, &block)
    end
  end
end

And now the problem: Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:

account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])

If I'm using a different approach for the module like:

module STI
  def new(*args, &block)
    type = args.dup.extract_options!.with_indifferent_access.delete(:type)
    if type.blank? or (type = type.constantize) == self
      super(*args, &block)
    else
      super(*args, &block).becomes(type)
    end
  end
end

Then instance variables are not shared, because it's creating a new object. Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?

Greetz Mario


Solution

  • So I solved my problems after moving my instance variables to @attributes and using my second approach for the module STI:

    module STI
      def new(*args, &block)
        type = args.dup.extract_options!.with_indifferent_access.delete(:type)
        if type.blank? or (type = type.constantize) == self
          super(*args, &block)
        else
          super(*args, &block).becomes(type)
        end
      end
    end
    
    class User < AR:Base
      extend STI
    
      belongs_to :account
    
      validates :password, :presence => true, :length => 8..40
      validates :password_digest, :presence => true
    
      def password=(password)
        @attributes['password'] = password
        self.password_digest = BCrypt::Password.create(password)
      end
    
      def password
        @attributes['password']
      end
    
      class Standard < User
        after_save :some_callback
      end
    end
    

    Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)