Dynamic polymorphic has_many associations in Rails - ruby-on-rails

I have a polymorphic, has_many through association between Service and context through ServiceUsage. The context models (currently Scenario and Mapping) include a ServiceConsumer mixin that declares the following associations:
has_many :service_usages, as: :context, dependent: :destroy
has_many :services, through: :service_usages, dependent: :destroy
ServiceUsage, the join table, defines the following associations:
belongs_to :service, inverse_of: :service_usages
belongs_to :context, inverse_of: :service_usages, polymorphic: true
Service currently has:
has_many :service_usages, inverse_of: :service
With the current setup, there is no way to get from service to the to the associated objects (either Scenario or Mapping).
One solution is to add explicit associations to Service for each Scenario and Mapping as follows:
has_many :scenarios, through: :service_usages, source: :subject, source_type: Scenario
I think there is a better way to do this to avoid explicitly defining the associations on Service.
I've been thinking of something along these lines:
module ServiceConsumer
extend ActiveSupport::Concern
included do
has_many :service_usages, as: :context, dependent: :destroy
has_many :services, through: :service_usages, dependent: :destroy
Service.class_eval <<-EOF
has_many #{self.to_s.underscore.pluralize.to_sym}, through: :service_usages,
source: :context,
source_type: ::#{self}
EOF
end
end
The idea is that when a model includes the ServiceConsumer concern, it defines on Service a has_many association for that particular model.
This sounds great in principal but I haven't been successful in getting it to work yet.
Any thoughts/comments/suggestions would be greatly appreciated. Do you think this is too obscure? Would it be better just to explicitly define the has_many associations on Service for each polymorphic associated model?
Thanks!

has_many #{self.to_s.underscore.pluralize.to_sym} this line in your concern is fundamentally wrong. Like if Scenario includes it, it'll translate to has_many :scenario inside Scenario model, which makes no sense.
On the other hand, you must maintain a balance between dry and meta programming. Unnecessary Metaprogram may overkill the benefit.

This worked for me.
Tag.class_eval %Q"
has_many :#{model_name.plural}, through: : service_usages, source: :context, source_type: #{model_name.name}
"
and I don't think metaprogramming is an overkill in this case.

Related

has_one through association with condition

I have 3 relevant tables/models. I would like to retrieve the user which has organized a party, given a Party record, a join table with a boolean organized column, and a users table.
My best attempt so far (this one makes the most sense to me after much fiddling). I've omitted the irrelevant columns.
class Party
# party_id
has_many :parties_users
has_many :users, through: :parties_users, source: :user
has_one :organizer,
-> { where organizer: true },
through: :parties_users,
source: :user
class PartiesUser
# party_id
# user_id
# organized:bool
belongs_to :party
belongs_to :user
class User
# user_id
has_many : parties_users
The above setup raises the following error, which I honestly don't fully understand:
ActiveRecord::HasOneThroughCantAssociateThroughCollection (Cannot have a has_one :through association 'Party#organizer' where the :through association 'Party#parties_users' is a collection. Specify a has_one or belongs_to association in the :through option instead.)
I know I can do this via an instance method, but given the frequency types of use, my app would massively benefit from having this as an association.
As the error message says, you can't have a has_one through a has_many.
You could (instead) do it this way if the organizer flag is on the join record...
has_many :parties_users
has_many :users, through: :parties_users, source: :user
has_one :main_party_user, -> {where organizer: true}, class_name: 'PartiesUser'
has_one :organizer, through: :main_party_user, class_name: 'User'

Rails 4 has_many through naming

I'm having problems with a Rails 4 join table. I have quite a simple setup which is working elsewhere in my application using a non-conventionally named table for users, groups and usergroupmemberships. I'm trying to set it up this time using the proper conventional naming and it's just not working.
Models involved are User, ManagementGroup and ManagementGroupsUser
db tables: management_groups_user, management_groups, users
app/models/user.rb
Class User < ActiveRecord::Base
...
has_many :management_groups, through: management_groups_users
has_many :management_groups_users
....
app/models/management_group.rb
class ManagementGroup < ActiveRecord::Base
has_many :users, through: :management_groups_users
has_many :management_groups_users
app/models/management_groups_user.rb
class ManagementGroupsUser < ActiveRecord::Base
belongs_to :users
belongs_to :management_groups
The association appears to work from with #user.management_groups_users but nothing else. I'm fairly sure this is a problem with naming / plurality but I can't figure it out.
This is the model which joins the remaining models user.rb and management_group
#app/models/management_groups_user.rb
belongs_to :user
belongs_to :management_group
Since we are going to use model above to access another model management_group then
#app/models/user.rb
has_many :user_management_groups #This should come first
has_many :management_groups, through: user_management_groups
Since we are going to use model above to access another user model then
app/models/management_group.rb
has_many :user_management_groups
has_many :users, through: :user_management_groups
Now should work
Do it this way.
app/models/user.rb
has_many :user_management_groups
has_many :management_groups, through: user_management_groups
app/models/management_group.rb
has_many :user_management_groups
has_many :users, through: :user_management_groups
app/models/management_groups_user.rb
belongs_to :user
belongs_to :management_group
I hope these associations will help you.
This is another way if you pass foreign key and class name.
app/models/user.rb
has_many :user_management_groups, :foreign_key => "key", :class_name => "ClassName"
has_many :management_groups, through: user_management_groups, :foreign_key => "key", :class_name => "ClassName"
app/models/management_group.rb
has_many :user_management_groups
has_many :users, through: :user_management_groups
app/models/management_groups_user.rb
belongs_to :user, class_name: "ClassName"
belongs_to :management_group, class_name: "ClassName"
This is another way around.
It's important to realize there is a convention rails uses for HABTM and has many through. HABTM does not have a model, so it needs to infer the table name which, as others point out, is both plural and alphabetical order.
If you are doing has many through and have a model, the convention is that it wants singular first word, plural second. See examples
User has and belongs to many groups.
HABTM: table name should be groups_users
Has Many Through: table name should be user_groups (flip order is more intuitive)
Model for the latter would be UserGroup. Has many through would specify it as through: :user_groups

Rails 4 has_many through many

I'm stuck on this:
class Worker < ActiveRecord::Base
has_many :skills
has_many :jobs, through: :skills
..
end
class Skill < ActiveRecord::Base
belongs_to :worker
has_many :jobs
..
end
class Job < ActiveRecord::Base
has_many :skills
has_many :workers, through: :skills
..
end
What I'm trying to do is set up a many to many between Skill and Job inside of the `has_many' through relationship?
My question has three parts
Is this possible - using the has_many jobs rather than belongs_to jobs.
If it can be done and the code is wrong, how do I fix it?
How can I create Worker, Skill and Job records? (looking for syntax)
This is a picture (of sorts) of what I'm trying to do, hope it helps... :(
You're not giving active record enough information about your relationships. Every :has_many should have a corresponding :belongs_to so that active record knows which table holds the foreign key for each association. Notice that you only have one :belongs_to for three relationships. That smells.
As for fixing the problem, you have at least 2 options:
add :has_and_belongs_to_many associations
use explicit join tables
My preference is for the latter option. Being forced to name join tables often clarifies the nature of a relationship. On the flip side, I've found that :has_and_belongs_to_many is often too implicit and ends up making my designs more obscure.
Explicit join tables
You might setup your relationships like this (untested):
class Assignment < ActiveRecord::Base
belongs_to :worker
belongs_to :job
end
class Qualification < ActiveRecord::Base
belongs_to :worker
belongs_to :skill
end
class Worker < ActiveRecord::Base
has_many :qualifications
has_many :skills, through: :qualifications
has_many :assignments
has_many :jobs, through: :assignments
..
end
class Skill < ActiveRecord::Base
has_many :qualifications
has_many :workers, through: :qualifications
has_many :jobs
..
end
class Job < ActiveRecord::Base
has_many :skills
has_many :workers, through: :assignments
..
end
By making the relationships more explicit I think the model is clearer. It should be easier to troubleshoot from here.
EDIT:
If you need to do a traversal like Job.find(1).qualified_workers try making the following adjustment to the above model:
class Job
has_many :required_competencies
has_many :skills, through: :required_competencies
has_many :qualifications, through: :skills
has_many :qualified_workers, through: qualifications, class_name: :workers
end
class RequiredCompetency
belongs_to :job
belongs_to :skill
end
This is explicit about each traversal and names it. If you find these paths through your system are getting really long, I'd consider that a smell. There might be a more direct way to fetch your data or perhaps a better way to model it.

Ruby on rails many-to-many

I have two models Type and Activity. Type has_many activities and Activity has_many types. To do this I used the has_many :through thing. This is how it looks like
Activity
has_many :typeitems
has_many :types, :through => :typeitem
Typeitem
belongs_to :activity
belongs_to :type
Type
has_many :typeitems
belongs_to :activity
This does not feel right though. I would want to query 2 things
Activities of a particular type
Types of a particular activity
When I went into rails console and typed types.activity I got a nil which means I will get a single object. Should I change the belongs_to in Type model to has_many.But then it's back to many-to-many. There should be a way.
I looked at the docs and found has_and_belongs_to_many. I also read this
You should use has_many :through if you need validations, callbacks, or extra attributes on the join model.
I am not using it now but I might want to in the future.
Both sides need a has_many :through:
Activity
has_many :typeitems
has_many :types, :through => :typeitem
Typeitem
belongs_to :activity
belongs_to :type
Type
has_many :typeitems
has_many :activities, through: :typeitems

Two has_many_through Relationships To Same Model

I have a Contributor Model and a Resource Model. In a simple world I would have the following setup:
class Resource
has_many :authorships
has_many :contributors, through: :authorships
end
class Contributor
has_many :authorships
has_many :resources, through: :authorships
end
However, my requirements have changed. A contributor can now either be an editor of a resource or an author of a resource. A Contributor can be an Editor of one resource and the Author of another. So it seems I have two ways to handle this requirement:
Add some kind of is_editor? attribute to my Authorships join model and effectively annotate each relationship.
Create a second join model – Editorship:
class Resource
has_many :authorships
has_many :editorships
has_many :contributors, through: :authorships
has_many :contributors, through: :editorships
end
class Contributor
has_many :authorships
has_many :editorships
has_many :resources, through: :authorships
has_many :resources, through: :editorships
end
Which is the most sensible approach, or is there another approach I'm missing?
Given your clarification, I would use the first approach, but instead of just introducing an is_editor boolean for Authorship, you might want to the generalize the language and the concept and instead use ResourceContributorship with a contributor_type field which could now be either :author or :editor, but could be extended in the future.

Resources