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.
Related
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.
I have a number of many to many relationships between two tables with conditionals representing different roles.
class Course < ActiveRecord::Base
...
has_many :course_mentors, :conditions => { :type_str => "mentor" }, :class_name => CourseUser
has_many :mentors, :through => :course_mentors, :source => :user
has_many :course_enrollees, :conditions => { :type_str => "enrollee" }, :class_name => CourseUser
has_many :enrollees, :through => :course_enrollees, :source => :user
...
end
To retrieve the contents of one of the associations on its own I can simply do #course.enrollees, #course.mentors etc. However sometimes it would be convenient to be able to get both enrollees and mentors together.
I have many different associations which makes it impractical to create additional associations for each combination of single associations. One can always do
(#course.enrollees + #course.mentors).sort
however this results in two requests to the database and possibly duplicate entries.
I have also investigated the merge function on associations, however this just returns an empty relation.
What is the best rails like way of doing this?
You can simply add a general association to CourseUser inside your Course model:
has_many :course_users
And then put a scope inside CourseUser model:
scope :of_type, lambda {|types| where(:type_str => types)}
Then you can access it via:
my_course = Course.first
my_course.course_users.of_type([:mentor, :enrollee])
I am trying to model a very simple system, but the Rails way of doing this is escaping me. I'd like to have two entities, a User and a Bet. A bet has two users and a user has many bets. I'd like the creating User of the bet to be able to mark it as resolved (do any form of update/delete on it).
My initial thinking was to have a User act two ways, as a better or a responder. many_to_many associations in rails may be the right option, with an extra column on the bet table to note who owns the model. But, then you are repeating one of the User's id's twice.
Thanks in advance!
I think this is what you want:
class User
has_many( :bets, :foreign_key => 'bettor_id', :class_name => 'Bet' )
has_many( :responses, :foreign_key => 'responder_id', :class_name => 'Bet' )
end
class Bet
belongs_to( :bettor, :class_name => 'User' )
belongs_to( :responder, :class_name => 'User' )
end
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.
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