4

guys we are building a sort of address book, we have Users, Contacts and a parent Table called UserEntity. We want to relate them in a many-to-many association through a table called Relationship, this relations have to be in a self related in UserEntity table.

One User has many Contacts and one Contact has many Users.

Later on we need relate new models with Users so the Relationship model must be polymorphic. We already built this:

class UserEntity < ActiveRecord::Base
  self.table_name = "users"
end

class User < UserEntity
  has_many :relationships, as: :relatable
  has_many :contacts, through: :relationships, source: :relatable, source_type "Contact"
end

class Contact < UserEntity
  has_many :relationships, as: :relatable
  has_many :users, through: :relationships, source: :relatable, source_type "User"
end

class Relationship < ActiveRecord::Base
  belongs_to :user
  belongs_to :relatable, polymorphic: true
end

but when we try to save the association user.contacts << contact the relatable_type field saves "UserEntity" value and not "Contact" or "User" value. We already tried to do this without any favorable results http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations

How can we make that relatable_type value saves the model name ("Contact" || "User") and not the STI parent model name ("UserEntity")?

kentverger
  • 475
  • 1
  • 5
  • 19
  • 1
    I'm sure there's a way, but I don't know if off the top of my head. However, as an alternative, you could opt to not inherit from UserEntity for User and Contact; instead, use Concerns. Breaking that inheritance chain will get you the right model names, while concerns will keep your code DRY. – GoGoCarl Feb 11 '16 at 02:31
  • Ok, that works! I know that is not the best solution, but it works jaja thanks a lot, I will keep trying with STI for a little more time. – kentverger Feb 11 '16 at 03:01
  • This is a standard problem, changing the inheritance is one way to fix it but a hack. There is a solution to this -- using the `class` of the subclass, rather than the superclass. I have posted information (a solution is being worked on) which explains – Richard Peck Feb 11 '16 at 10:54

2 Answers2

2

This is something known as STI base class, whereby Rails will save the Parent class, even though you've called the subclass. We have experience with it:

enter image description here

The above should have Node as Company...


I spent some time last year looking over this; there's a gem called store_sti_base_class:

Notice that addressable_type column is Person even though the actual class is Vendor.

Normally, this isn't a problem, however it can have negative performance characteristic in certain circumstances. The most obvious one is that a join with persons or an extra query is required to find out the actual type of addressable.

The way around it - at least from the perspective of the gem - is to extend ActiveRecord so that it will save the actual class of the model, not its parent.

The only code I have - which is out of date - is as follows:

#config/initializers/sti_base.rb
ActiveRecord::Base.store_base_sti_class = false

#lib/extensions/sti_base_class.rb
module Extensions
  module STIBaseClass #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
    extend ActiveSupport::Concern

    included do
      class_attribute :store_base_sti_class
      self.store_base_sti_class = true
    end
  end

  # include the extension 
  ActiveRecord::Base.send(:include, STIBaseClass)

  ####

  module AddPolymorphic
    extend ActiveSupport::Concern

    included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
      define_method :replace_keys do |record=nil|
        super(record)
        owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
        Rails.logger.info record.class.base_class.name
      end
    end
  end

  ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)
end

As I need to get this working, it might be worth bantering something around.

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • Really? Did you use the gem or my code? Which version of Rails are you running? – Richard Peck Feb 11 '16 at 18:02
  • 1
    We use the Gem, We have Rails 4.1.14 :) why you are suprised that worked? jaja with this we can do `user.contacts` but we are having trouble with `contact.users` but that relation in my case is no important. – kentverger Feb 11 '16 at 21:27
  • Ah the `gem`, I was surprised because I thought you were using my code, which I was highly dubious would work out of the box – Richard Peck Feb 12 '16 at 09:02
0

You must set UserEntity as abstract class:

class UserEntity < ActiveRecord::Base
  self.table_name = "users"
  self.abstract_class = true
end

Then value relatable_type can be User or Contact.

By default RoR save in relatable_type value of base_class method in your case:

class UserEntity < ActiveRecord::Base
end

class User < UserEntity
end

class Contact < UserEntity
end

irb(main):010:0> User.base_class.name 
=> "UserEntity"

irb(main):011:0> Contact.base_class.name 
=> "UserEntity"

When you make UserEntity as abstract class:

class UserEntity < ActiveRecord::Base
  self.abstract_class = true 
end

class User < UserEntity
end

class Contact < UserEntity
end

irb(main):010:0> User.base_class.name 
=> "User"

irb(main):011:0> Contact.base_class.name 
=> "Contact"

Here defined method which store class name (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb#L14):

def replace_keys(record)
  super
  owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
def polymorphic_name
  base_class.name
end
artamonovdev
  • 2,260
  • 1
  • 29
  • 33