Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

has_one through and polymorphic associations over multi-table inheritance

In the project i'm currently developing under rails 4.0.0beta1, i had the need for a user based authentication in which each user could be linked to an entity. I'm kinda new to rails and had some troubles doing so.

The model is as following:

class User < ActiveRecord::Base
end

class Agency < ActiveRecord::Base
end

class Client < ActiveRecord::Base
  belongs_to :agency
end

What i need is for a user to be able to link to either an agency or a client but not both (those two are what i'll be calling entities). It can have no link at all and at most one link.

First thing i looked for was how to do Mutli-Table inheritance (MTI) in rails. But some things blocked me:

  • it was not available out of the box
  • MTI looked kinda hard to implement for a newbie such as me
  • the gems implementing the solutions seemed old and either too complexe or not complete
  • the gems would have probably broke under rails4 as they had not been updated for a while

So i looked for another solution and i found polymorphic associations.

I've be on this since yesterday and took some time to make it work even with the help of Rails polymorphic has_many :through and ActiveRecord, has_many :through, and Polymorphic Associations

I managed to make the examples from the question above work but it took a while and i finally have two problems:

  1. How to transform the relations in user into a has_one association and be able to access "blindly" the linked entity ?
  2. How to set a constraint so that no user can have more than one entity ?
  3. Is there a better way to do what i want ?
like image 274
Crystark Avatar asked Mar 14 '13 17:03

Crystark


People also ask

What is polymorphism in Ruby with example?

Polymorphism is one of the fundamental features of object oriented programming, but what exactly does it mean? At its core, in Ruby, it means being able to send the same message to different objects and get different results.

How does polymorphism work in Ruby?

Polymorphism works well in Ruby on Rails as an Active Record association. If models essentially do the same thing, we can turn them into one single model to create a polymorphic relationship. In this example, we have an ERD for an application where a user can post an instrument with its details.

Does Ruby support polymorphism?

Polymorphism is an important concept in object-oriented programming. It allows us to implement many different implementations of the same method, helping with code reusability and avoiding redundancy. In Ruby, we can implement polymorphism using inheritance or duck typing.

Why we use polymorphic association in Rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.


1 Answers

Here's a fully working example:

The migration file:

class CreateUserEntities < ActiveRecord::Migration
  def change
    create_table :user_entities do |t|
      t.integer :user_id
      t.references :entity, polymorphic: true

      t.timestamps
    end

    add_index :user_entities, [:user_id, :entity_id, :entity_type]
  end
end

The models:

class User < ActiveRecord::Base
  has_one :user_entity

  has_one :client, through: :user_entity, source: :entity, source_type: 'Client'
  has_one :agency, through: :user_entity, source: :entity, source_type: 'Agency'

  def entity
    self.user_entity.try(:entity)
  end

  def entity=(newEntity)
    self.build_user_entity(entity: newEntity)
  end
end

class UserEntity < ActiveRecord::Base
  belongs_to :user
  belongs_to :entity, polymorphic: true

  validates_uniqueness_of :user
end

class Client < ActiveRecord::Base
  has_many :user_entities, as: :entity
  has_many :users, through: :user_entities
end

class Agency < ActiveRecord::Base
  has_many :user_entities, as: :entity
  has_many :users, through: :user_entities
end

As you can see i added a getter and a setter that i named "entity". That's because has_one :entity, through: :user_entity raises the following error:

ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have a has_many :through association 'User#entity' on the polymorphic object 'Entity#entity' without 'source_type'. Try adding 'source_type: "Entity"' to 'has_many :through' definition.

Finally, here are the tests i set up. I give them so that everyone understands know ho you can set and access data between those objects. i won't be detailing my FactoryGirl models but they're pretty obvious

require 'test_helper'

class UserEntityTest < ActiveSupport::TestCase

  test "access entity from user" do
    usr = FactoryGirl.create(:user_with_client)

    assert_instance_of client, usr.user_entity.entity
    assert_instance_of client, usr.entity
    assert_instance_of client, usr.client
  end

  test "only right entity is set" do
    usr = FactoryGirl.create(:user_with_client)

    assert_instance_of client, usr.client
    assert_nil usr.agency
  end

  test "add entity to user using the blind rails method" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.build_user_entity(entity: client)
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add entity to user using setter" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.client = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add entity to user using blind setter" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    usr.entity = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id
  end

  test "add user to entity" do
    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)

    client.users << usr

    result = UserEntity.where(entity_id: client.id, entity_type: 'client')

    assert_equal 1, result.size
    assert_equal usr.id, result.first.user_id
  end

  test "only one entity by user" do

    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)
    agency = FactoryGirl.create(:agency)

    usr.agency = agency
    usr.client = client
    usr.save!

    result = UserEntity.where(user_id: usr.id)
    assert_equal 1, result.size
    assert_equal client.id, result.first.entity_id

  end

  test "user uniqueness" do

    usr = FactoryGirl.create(:user)
    client = FactoryGirl.create(:client)
    agency = FactoryGirl.create(:agency)

    UserEntity.create!(user: usr, entity: client)

    assert_raise(ActiveRecord::RecordInvalid) {
      UserEntity.create!(user: usr, entity: agency)
    }

  end

end

I Hope this can be of some help to someone. I decided to put the whole solution here cause it seems to me like a good one compared to MTI and i think it shouldn't take someone that much time to set something like that up.

like image 107
Crystark Avatar answered Sep 19 '22 05:09

Crystark