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:
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:
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With