Polymorphic has_many through Associations in Ruby on Rails - ruby-on-rails

I have the following problem,
A user can have several professions, more than 10. For example, a user may be a doctor, teacher, and N. Each profession has its own attributes.
I could do, Doctor belongs_to User, but if I want to know all the professions of this user I will have to check each row of the User table.
I created the following code
class User < ApplicationRecord
has_many :jobables
end
class Job < ApplicationRecord
belongs_to :user
belongs_to :jobable
end
class Jobable < ApplicationRecord
has_one :job
end
class Medic < Jobable
end
class Programmer < Jobable
end
But I do not know if that would be the best answer

I would think that it would be much easier to do something like:
class User < ApplicationRecord
has_many :user_professions
has_many :professions, through: :user_professions
end
# == Schema Information
#
# Table name: professions
#
# id :integer not null, primary key
# name :string
# created_at :datetime not null
# updated_at :datetime not null
#
class Profession < ApplicationRecord
has_many :user_professions
has_many :users, through: :user_professions
end
class UserProfession < ApplicationRecord
belongs_to :user
belongs_to :profession
end
You could then create logic to ensure that a Profession is only assigned to a User once.
Then, you could simply do:
#user.professions
And get all the Professions for a User.
You could also do:
#profession.users
And get all the Users that belong to the Profession.
Based on the edit to your question, you could do something like:
class UserProfession < ApplicationRecord
belongs_to :user
belongs_to :profession
belongs_to :profession_detail, polymorphic: true
end
In which case you might have something like:
class DoctorDetail < ApplicationRecord
end
And you could do something like:
#user.professional_detail_for(:doctor)
Of course, you would need to implement the professional_detail_for method on the User model which might look something like:
class User < ApplicationRecord
has_many :user_professions
has_many :professions, through: :user_professions
def professional_detail_for(profession_type)
user_profession_for(profession_for(profession_type)).try(:profession_detail)
end
private
def profession_for(profession_type)
Profession.find_by(name: profession_type.to_s)
end
def user_profession_for(profession)
user_professions.find_by(profession: profession)
end
end
That's a little rough, but I imagine you get the idea.

Related

Possible to alias has many associations with the same alias with different models?

I have an Article model that can have many different types of content blocks. So an Article can have many HeadingBlocks and ParagraphBlocks like this:
class Article < ApplicationRecord
has_many :heading_blocks
has_many :paragraph_blocks
end
class HeadingBlock < ApplicationRecord
belongs_to :article
end
class ParagraphBlock < ApplicationRecord
belongs_to :article
end
I want to be able to alias both HeadingBlock and ParagraphBlock with the same name (blocks) so if I have an instance of an article, I can do something like this:
#article.blocks // returns all heading blocks and paragraph blocks associated
Is this possible in Rails? If so, could you please provide an example on how to alias multiple models in a has many association using the same name?
Thank you.
You can have a method that returns an array of blocks:
class Article < ApplicationRecord
has_many :heading_blocks
has_many :paragraph_blocks
# NOTE: returns an array of heading and paragraph blocks
def blocks
heading_blocks + paragraph_blocks
end
end
You can reorganize the relationships to have a polymorphic association:
class Article < ApplicationRecord
has_many :blocks
end
# NOTE: to add polymorphic relationship add this in your migration:
# t.references :blockable, polymorphic: true
class Block < ApplicationRecord
belongs_to :article
belongs_to :blockable, polymorphic: true
end
class HeadingBlock < ApplicationRecord
has_one :block, as: :blockable
end
class ParagraphBlock < ApplicationRecord
has_one :block, as: :blockable
end
If you can merge HeadingBlock and ParagraphBlock into one database table:
class Article < ApplicationRecord
has_many :blocks
end
# id
# article_id
# type
class Block < ApplicationRecord
belongs_to :article
# set type as needed to `:headding` or `:paragraph`
end
# NOTE: I'd avoid STI; but this does set `type` column automatically to `ParagraphBlock`
# class ParagraphBlock < Block
# end

How can I implement a has_many :through association two ways?

I have a property model and a user model.
A user with the role of 'admin', which is represented by a column on the users table, can have many properties.
A user with a role of 'guest' can also belong to a property, which gives them access to that property.
How should I do this in Rails?
authorizations table -> user_id, property_id
class Authorization < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :authorizations
has_many :properties, through: :authorizations
end
class Property < ActiveRecord::Base
has_many :authorizations
has_many :users, through: :authorizations
end
then you can do User.find(id).properties
First, you need a has_many :through association between your models User and Property. So, create a new table properties_users with columns user_id and propety_id. And do following changes to the models:
class PropertiesUser < ActiveRecord::Base
belongs_to :user
belongs_to :property
end
class User < ActiveRecord::Base
has_many :properties_users
has_many :properties, through: :properties_users
end
class Property < ActiveRecord::Base
has_many :properties_users
has_many :users, through: :properties_users
end
Now, we need to make sure that a guest user does not have more than one property. For that we can add a validation to model PropertiesUser like below:
class PropertiesUser < ActiveRecord::Base
validate :validate_property_count_for_guest
private
def validate_property_count_for_guest
return unless user && user.guest?
if user.properties.not(id: self.id).count >= 1
self.errors.add(:base, 'guest user cannot have more than one properties')
end
end
end
class User < ActiveRecord::Base
def guest?
# return `true` if user is guest
end
end
Finally, to access a guest user's property, define a dedicated method in model User:
class User < ActiveRecord::Base
def property
# Raise error if `property` is called on non-guest users
raise 'user has multiple properties' unless guest?
properties.first
end
end
Now, you can fetch a guest user's property by running:
user = User.first
user.guest?
=> true
user.property
=> <#Property 1> # A record of Property

Rails count number of records in polymorphic association

I have a polymorphic association in my application and I want to be able to count the number of users in a group, and list the users in each group. How can I achieve this?
class Group < ApplicationRecord
has_many :user_groups
has_many :users, through: :user_groups
end
class User < ApplicationRecord
has_many :user_groups
has_many :groups, through: :user_groups
end
class UserGroup < ApplicationRecord
belongs_to :user
belongs_to :group
end
I at the moment I can call User.first.groups however I am not able to call User.groups.all and User.all.groups but none of these work.
First off, that's not a polymorphic association. Just FYI.
It's hard to know what you're asking, because to count the users in a group, you do:
group.users.count
When you say, 'list them', do you mean in a view? That would be something like:
<%- group.users.each do |user| %>
# do something with user
<% end %>
Just to expand on the comment, if the class Image had a polymorphic belongs_to association, that might look something like:
# == Schema Information
#
# Table name: images
#
# id :integer not null, primary key
# imageable_id :integer
# imageable_type :string
# created_at :datetime not null
# updated_at :datetime not null
#
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
As you can see, the Image class has the attributes of imageable_id and imageable_type. Then, if you wanted to have Document have many images, you would same something like:
class Document < ApplicationRecord
has_many :images, as: :imageable
end
Then you could do:
image.imageable
To get the object that the image belongs to. To get all the images for a document, then simply:
document.images

Rails nested models and virtual attribute initialization

I have a problem understanding how are attributes "sent" to nested model(s), and if is possible to do this for model with virtual attrubute too. I have three models:
class User < ActiveRecord::Base
...
has_and_belongs_to_many :clearancegoods
has_many :clearanceitems, through: :user_clearanceitems_descriptions
has_many :user_clearanceitems_descriptions
...
end
class Clearanceitem < ActiveRecord::Base
...
has_many :users, through: :user_clearanceitems_descriptions
has_many :user_clearanceitems_descriptions
accepts_nested_attributes_for :user_clearanceitems_descriptions
...
def user_id
#user_id
end
def user_id=(val)
#user_id = val
end
end
class UserClearanceitemsDescription < ActiveRecord::Base
belongs_to :user
belongs_to :clearanceitem
end
in console:
desc = User.find(5).user_clearanceitems_descriptions.new
desc.user_id
### result is 5
item = User.find(5).clearanceitems.new
item.user_id
### result in nil
If Clearanceitem can have many users, then it can't have a single user_id, otherwise that attribute would have to have multiple values. If you're just trying to create Clearanceitems associated with the User, ActiveRecord will automagically create the associated join record:
User.find(5).clearanceitems.create
User.find(5).clearanceitems # Contains the Clearanceitem you just created
So just remove the user_id attribute from Clearanceitem.

ActiveRecord polymorphic association with unique constraint

I have a site that allows users to log in via multiple services (LinkedIn, Email, Twitter, etc..).
I have the below structure set up to model a User and their multiple identities. Basically a user can have multiple identieis, but only one of a given type (e.g. can't have 2 Twitter identiteis).
I decided to set it up as a polymorphic relationship, as drawn below. Basically there's a middle table identities that maps a User entry to multiple *_identity tables.
The associations are as follows (shown only for LinkedInIdentity, but can be extrapolated)
# /app/models/user.rb
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
...
end
# /app/models/identity
class Identity < ActiveRecord::Base
belongs_to :user
belongs_to :identity, polymorphic: true
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
has_one :identity, as: :identity
has_one :user, through: :identity
...
end
The problem I'm running into is with the User model. Since it can have multiple identities, I use has_many :identities. However, for a given identity type (e.g. LinkedIn), I used has_one :linkedin_identity ....
The problem is that the has_one statement is through: :identity, and there's no singular association called :identity. There's only a plural :identities
> User.first.linkedin_identity
ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :identity in model User
Any way around this?
I would do it like so - i've changed the relationship name between Identity and the others to external_identity, since saying identity.identity is just confusing, especially when you don't get an Identity record back. I'd also put a uniqueness validation on Identity, which will prevent the creation of a second identity of the same type for any user.
class User < ActiveRecord::Base
has_many :identities
has_one :linkedin_identity, through: :identity, source: :identity, source_type: "LinkedinIdentity"
end
# /app/models/identity
class Identity < ActiveRecord::Base
#fields: user_id, external_identity_id
belongs_to :user
belongs_to :external_identity, polymorphic: true
validates_uniqueness_of :external_identity_type, :scope => :user_id
...
end
# /app/models/linkedin_identity.rb
class LinkedinIdentity < ActiveRecord::Base
# Force the table name to be singular
self.table_name = "linkedin_identity"
has_one :identity
has_one :user, through: :identity
...
end
EDIT - rather than make the association for linkedin_identity, you could always just have a getter and setter method.
#User
def linkedin_identity
(identity = self.identities.where(external_identity_type: "LinkedinIdentity").includes(:external_identity)) && identity.external_identity
end
def linkedin_identity_id
(li = self.linkedin_identity) && li.id
end
def linkedin_identity=(linkedin_identity)
self.identities.build(external_identity: linkedin_identity)
end
def linkedin_identity_id=(li_id)
self.identities.build(external_identity_id: li_id)
end
EDIT2 - refactored the above to be more form-friendly: you can use the linkedin_identity_id= method as a "virtual attribute", eg if you have a form field like "user[linkedin_identity_id]", with the id of a LinkedinIdentity, you can then do #user.update_attributes(params[:user]) in the controller in the usual way.
Here is an idea that has worked wonderfully over here for such as case. (My case is a tad diffferent since all identites are in the same table, subclasses of the same base type).
class EmailIdentity < ActiveRecord::Base
def self.unique_for_user
false
end
def self.to_relation
'emails'
end
end
class LinkedinIdentity < ActiveRecord::Base
def self.unique_for_user
true
end
def self.to_relation
'linkedin'
end
end
class User < ActiveRecord::Base
has_many :identities do
[LinkedinIdentity EmailIdentity].each do |klass|
define_method klass.to_relation do
res = proxy_association.select{ |identity| identity.is_a? klass }
res = res.first if klass.unique_for_user
res
end
end
end
end
You can then
#user.identities.emails
#user.identities.linkedin

Resources