Rails association definition has "magic number" - ruby-on-rails

I am an beginner/intermediate Rails developer and I have inherited a large Rails project. While digging through the codebase I found that one of our models has association definitions that rely on a hard-coded ID:
class Account < ActiveRecord::Base
has_many :customer_rels, :class_name => 'EntityAccountRel', :conditions => 'entity_account_rels.entity_type_id=1'
has_many :vendor_rels, :class_name => 'EntityAccountRel', :conditions => 'entity_account_rels.entity_type_id=2'
has_many :customers, :through => :customer_rels, :source => :entity
has_many :vendors, :through => :vendor_rels, :source => :entity
customer and vendor are both Entities, with a relationship to Account (through EntityAccountRel). They are distinguished from one another by entity_type_id, which refers to EntityType, a model whose few members are static enough to let us get away with referring to them by hard-coded ids. I recognize this is a bad practice, but I'm wondering about the 'best' way to refactor:
1) redefine the association definition to use a subquery like so:
has_many :customer_rels, :class_name => 'EntityAccountRel', :conditions => 'entity_account_rels.entity_type_id=(select id from entity_type where name="Customer")'
This is the most obvious solution to me, but also seems terribly inelegant and likely inefficient as well
2) define a scope on EntityAccountRel like so:
scope :customer, joins(:entity_type).where('entity_type.code="CUST"')
and then somehow tie this to the association definition:
has_many :customer_rels, :class_name => 'EntityAccountRel', :scope => :customer
this doesn't work, (Unknown key(s): scope (ArgumentError)) but seems like maybe there would be support for it if I knew how to define the association correctly
3) set up an inheritance relationship on EntityAccountRel, obviating the need for entity_type_id (and maybe EntityType as well):
class CustomerAccountRel < EntityAccountRel
This would simplify my association definition but it seems like a major refactor with far-reaching implications. Plus I'm told to use inheritance cautiously, as it can make code difficult to understand.
These are all the options I've come up with, but I think I'm missing something obvious.

EntityType#id seems to be considered static and strongly related to code behavior, so you could give them names as constants:
class EntityType
CUSTOMER = 1
VENDOR = 2
end
and then just substitute the magic number by the constant. That is quick and gives you a good return as you can reference that in other parts of the code easily. I'd keep all references to these constants in the model layer.

Related

Iterating Over Rails Model Classes with Polymorphic Associations

In an application there are many models with polymorphic associations defined like:
has_many :mentions, :as => :mentionable, :dependent => :destroy
In a library class all mentionable models are collected for later iterating.
mentionables = Model1.all + Model2.all + Model3.all ...
This works but it is just static which is not desirable for a library code. Bottom statement would be much more intuitive hovever it won't work.
mentionables = Mentionable.all
Is there an API in Rails to iterate over models with Polymorphic relations defined with ':as => ...' directive?
I think there are two questions in here and I'll try my best to answer both of them. First, you'd need a way to list all of the models in your Rails application. There are methods offered in this question, and I prefer this answer (by sj26).
# Really depends on how your classes are loaded, usually environment specific.
Rails.application.eager_load!
ActiveRecord::Base.descendants
After, you need to parse the model associations to determine if they have an :as #option assigned, and pull out the associated class name. This is assuming that your association has been created as such:
class Mentionee < ActiveRecord::Base
has_many :mentions, :as => :mentionable, :dependent => :destroy, :class_name => 'Mentionable'
end
You can do this using the reflect_on_all_associations method (there is probably a more ruby-esque way to write this):
Mentionee.reflect_on_all_associations.select {|a| a.options[:as] == :mentionable }
Which will return the polymorphic class model for Mentionable. To join these up, you could do something as follows (untested!):
Rails.application.eager_load!
mentionables = []
ActiveRecord::Base.descendants.each do |descendent|
mentionables << descendent.reflect_on_all_associations.select{|a| a.options[:as] == :mentionable}
end
mentionables.each do |mentionable|
# Do your work here
end

Setup a relation from one model to another, using multiple associations

I'd like to know how to setup a relation from one model to another, but through multiple associations.
In this example there is a Member model, a Sportsorginazation model, a Basketballclub model and a Footballclub model (for the sake of this example it is necessary to have these clubs defined as different models).
Let's say this Sportsorganization has multiple Basketballclubs and multiple Footballclubs. The Basketballclubs and Footballclubs both have many members.
How would I get all the members from the sportsorganization?
I can imagine to have something like:
class Sportsorganization
has_many :basketballclubs
has_many :footballclubs
has_many :members, :through => :basketballclubs
end
But how would I get all the members from both the basketballclubs and the footballclubs? I know that you can make an alias for members and let Sportsorganization have basketballclubmembers and footballclubmembers, but is it also possible to get all members using basketballclubs and footballclubs?
#tadman is correct as far as I'm concerned, but there is a naive way to do this:
has_many :basketballclub_members, :through => :basketballclubs, :class_name => 'Member'
has_many :footballclub_members, :through => :footballclubs, :class_name => 'Member'
def all_members
basketballclub_members + footballclub_members
end
This is very naive, though—you'd probably want to memoize (cache) the result of that +, for example.
Slightly less naive would be to use composed_of, e.g.:
has_many :basketballclub_members, :through => :basketballclubs, :class_name => 'Member'
has_many :footballclub_members, :through => :footballclubs, :class_name => 'Member'
composed_of :members, [ 'basketballclub_members', 'footballclub_members' ],
:class_name => 'Array',
:constructor => proc {|bb_members, fb_members| bb_members + fb_members }
I haven't tested this so it may need some tweaking, but that's the basic idea.
Caveat emptor, though—with either of these methods you'll lose all of the handy ActiveRecord collection methods (you'll just get a plain old array of objects).
Relationships like this can get more complicated than ActiveRecord's standard patterns can accommodate, so you either need to come up with a creative solution, or you can try and wrangle it back in the box.
One approach that might work is creating an STI model to encapsulate all the different kinds of clubs. A second approach is to associate each of the members back to the associated Sportsorganization directly by de-normalizing an attribute.

Rails: Many to many polymorphic relationships

See comments for updates.
I've been struggling to get a clear and straight-forward answer on this one, I'm hoping this time I'll get it! :D
I definitely have a lot to learn still with Rails, however I do understand the problem I'm facing and would really appreciate additional help.
I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set of subclasses of Target.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order.
In more detail, I will be able to do things like this in the console (and of course elsewhere):
task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]
But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:
store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]
The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships:
Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.
Similar to the last point, people still want to define Store, Software, Office and Vehicle in Task in one way shape or form. The important bit here is that the relationship is blind to the subclassing. My polymorphs will initially only be interacted with as Targets, not as their individual subclass types. Defining each subclass in Task again starts to eat away at the purpose of the polymorphic relationship.
I see that a model for the join table might be in order, that seems somewhat correct to me except that it adds some complexity I assumed Rails would be willing to do away with. I plea inexperience on this one.
It seems to be a small hole in either rails functionality or the collective community knowledge. So hopefully stackoverflow can chronicle my search for the answer!
Thanks to everyone who help!
You can combine polymorphism and has_many :through to get a flexible mapping:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :targets, :through => :assignment
end
class Store < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
...And so forth.
Although the answer proposed by by SFEley is great, there a some flaws:
The retrieval of tasks from target (Store/Vehicle) works, but the backwards wont. That is basically because you can't traverse a :through association to a polymorphic data type because the SQL can't tell what table it's in.
Every model with a :through association need a direct association with the intermediate table
The :through Assignment association should be in plural
The :as statement wont work together with :through, you need to specify it first with the direct association needed with the intermediate table
With that in mind, my simplest solution would be:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :assignments
# acts as the the 'has_many targets' needed
def targets
assignments.map {|x| x.target}
end
end
class Store < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment
end
class Vehicle < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment, :as => :target
end
References:
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
The has_many_polymorphs solution you mention isn't that bad.
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end
Seems to do everything you want.
It provides the following methods:
to Task:
t = Task.first
t.targets # Mixed collection of all targets associated with task t
t.stores # Collection of stores associated with task t
t.softwares # same but for software
t.offices # same but for office
t.vehicles # same but for vehicles
to Software, Store, Office, Vehicle:
s = Software.first # works for any of the subtargets.
s.tasks # lists tasks associated with s
If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.
There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:
Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)
store
software
office
vehicle
Create config/initializers/subtargets.rb and fill it with this:
SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.
Sorry, I don't really feel like walking you through that right now, but here are some resources
Finally replace the list in the has_many_polymorphs declaration with SubtargetList
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => SubtargetList
end
From this point on you could add a new subtarget with
$ script/generate subtarget_model home
And this will automatically update your polymorphic list once you reload your console or restart the production server.
As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.
Using STI:
class Task < ActiveRecord::Base
end
class StoreTask < Task
belongs_to :store, :foreign_key => "target_id"
end
class VehicleTask < Task
belongs_to :vehicle, :foreign_key => "target_id"
end
class Store < ActiveRecord::Base
has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end
In your databse you'll need:
Task type:string and Task target_id:integer
The advantage is that now you have a through model for each task type which can be specific.
See also STI and polymorphic model together
Cheers!
This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.
I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like
create_table :targets_tasks do |t|
t.integer :target_id
t.string :target_type
t.integer :task_id
end
Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.
But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.
Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!
I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.
At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.
You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.
Have you pursued that brute force approach:
class Task
has_many :stores
has_many :softwares
has_many :offices
has_many :vehicles
def targets
stores + softwares + offices + vehicles
end
...
It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.

rails polymorphic association (legacy database)

I am using a legacy database, so i do not have any control over the datamodel. They use a lot of polymorphic link/join-tables, like this
create table person(per_ident, name, ...)
create table person_links(per_ident, obj_name, obj_r_ident)
create table report(rep_ident, name, ...)
where obj_name is the table-name, and obj_r_ident is the identifier.
So linked reports would be inserted as follows:
insert into person(1, ...)
insert into report(1, ...)
insert into report(2, ...)
insert into person_links(1, 'REPORT', 1)
insert into person_links(1, 'REPORT', 2)
And then person 1 would have 2 linked reports, 1 and 2.
I can understand possible benefits having a datamodel like this, but i mostly see one big shortcoming: using constraints is not possible to ensure data integrity. But alas, i cannot change this anymore.
But to use this in Rails, i was looking at polymorphic associations but did not find a nice way to solve this (since i cannot change the columns-names, and did not readily find a way to do that).
I did come up with a solution though. Please provide suggestions.
class Person < ActiveRecord::Base
set_primary_key "per_ident"
set_table_name "person"
has_and_belongs_to_many :reports,
:join_table => "person_links",
:foreign_key => "per_ident",
:association_foreign_key => "obj_r_ident",
:conditions => "OBJ_NAME='REPORT'"
end
class Report < ActiveRecord::Base
set_primary_key "rep_ident"
set_table_name "report"
has_and_belongs_to_many :persons,
:join_table => "person_links",
:foreign_key => "obj_r_ident",
:association_foreign_key => "per_ident",
:conditions => "OBJ_NAME='REPORT'"
end
This works, but i wonder if there would be a better solution, using polymorphic associations.
At least as of Rails 4.2.1, you can pass foreign_type to a belongs_to declaration to specify the name of the column to be used for the 'type' of the polymorphic association
http://apidock.com/rails/v4.2.1/ActiveRecord/Associations/ClassMethods/belongs_to
You can override the column names, sure, but a quick scan of the Rails API didn't show me anywhere to override the polymorphic 'type' column. So, you wouldn't be able to set that to 'obj_name'.
It's ugly, but I think you'll need a HABTM for each type of object in your table.
You might be able to do something like this:
{:report => 'REPORT'}.each do |sym, text|
has_and_belongs_to_many sym,
:join_table => "person_links",
:foreign_key => "obj_r_ident",
:association_foreign_key => "per_ident",
:conditions => "OBJ_NAME='#{text}'"
end
At least that way all the common stuff stays DRY and you can easily add more relationships.

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.
Book
Movie
Tag
Category
I would like to able to do:
book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]
movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]
Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.
Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.
Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?
Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.
class Relation
belongs_to :owner, :polymorphic => true
belongs_to :child_item, :polymorphic => true
end
class Book
has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'
# and so on for each type of relation
has_many :pwned_movies, :through => :pwned_relations,
:source => :child_item, :source_type => 'Movie'
has_many :pwning_movies, :through => :pwning_relations,
:source => :owner, :source_type => 'Movie'
end
A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:
( pwned_movies + pwning_movies ).uniq
A common example of this problem is the "friend" relationship in social networking apps.
One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.
class Relation
after_create do
unless Relation.first :conditions =>
[ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?', child_item_id, child_item_type, owner_id, owner_type ]
Relation.create :owner => child_item, :child_item => owner
end
end
end
I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.
So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.
book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]
I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.
http://gist.github.com/123966
I really hope I don;t have to completely re-implement the association proxy to work with this though.
I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.
depending on how closesly related your movies/books db tables are
what if you declared
class Items < ActiveRecord::Base
has_many :tags
has_many :categories
has_and_belongs_to_many :related_items,
:class => "Items",
:join_table => :related_items,
:foreign_key => "item_id",
:associated_foreign_key => "related_item_id"
end
class Books < Items
class Movies < Items
make sure you put type in your items table

Resources