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
Related
Is there a way to define a polymorphic association that pulls from another poly assoc, but also then reverts to a backup poly assoc if the first one is empty? Maybe like this?
class Reservation < ActiveRecord::Base
has_many :trip_itinerary_entries, :through => :trips, :source => :itinerary_entries
has_many :template_itinerary_entries, :through => :templates, :source => :itinerary_entries
has_many :entries, :from => :trip_itinerary_entries, :backup => :template_itinerary_entries
Thanks!
I would set up the two polymorphic associations (in my model and the required columns in my DB) and then create a method for getting both of the items (you don't want to override them method otherwise you would't be able to write new relationships).
def get_entries
if entries.any?
entries
else
template_itinerary_entries
end
end
This way you can still use entries when you need to add items.
reserve = Reservation.new
reserve.entries << Entry.create()
But you could use the get_entries method to get either of the items.
reserve.get_entries
=> [template_itinerary_entries] # if there are no entries.
I am trying to add a condition to a has many through association without luck. This is the association in my video model:
has_many :voted_users, :through => :video_votes, :source => :user
I want to only get the voted_users whose video_votes have a value equal to 1 for that video. How would I do this?
I would suggest creating a model method within the video model class
Something like:
def users_with_one_vote
self.voted_users, :conditions => ['value = ?', 1]
end
Then in the controller use video.users_with_one_vote
Then testing is easier too.
Any chance you can change that column name from 'value'. Might give some issues (reserved?).
I'd do this in 2 stages:
First, I'd define the has_many :through relationship between the models without any conditions.
Second, I'd add a 'scope' that defines a where condition.
Specifically, I'd do something like:
class User < ActiveRecord::Base
has_many :video_votes
has_many :votes, :through=>:video_votes
def self.voted_users
self.video_votes.voted
end
end
class VideoVote
def self.voted
where("value = ?", 1)
end
end
class Video
has_many :video_votes
has_many :users, :through=>:video_votes
end
Then you could get the users that have voted using:
VideoVote.voted.collect(&:user).uniq
which I believe would return an array of all users who had voted. This isn't the exact code you'd use -- they're just snippets -- but the idea is the same.
Would
has_many :voted_users, :through => :video_votes, :source => :user, :conditions => ['users.votes = ?', 1]
Do the trick?
I found that defining this method in my model works:
def upvoted_users
self.voted_users.where("value = 1")
end
and then calling #video.upvoted_users does the trick.
The best way to do this without messing with the relations is by crafting a more complex query. Relations is not the best thing to use for this particular problem. Please understand that relations is more a "way of data definition" then a way of "bussiness rules definition".
Bussiness logic or bussiness rules must be defined on a more specifically layer.
My suggestion for your problem is to create a method to search for users who voted on your video only once. something like:
class Video < ActiveRecord::Base
def voted_once()
User.joins(:video_votes).where("video_votes.value == 1 AND video_votes.video_id == ?", this.id)
end
Rails is magical for many things, but complex queries still have to be done in a "SQL" way of thinking. Don't let the illusional object oriented metaphor blind you
As long as we are throwing around ideas, how about using association extensions.
class VideoVote
scope :upvotes, where(:value => 1)
end
class Video
has_many :voted_users, :through => :video_votes, :source => :user do
def upvoted
scoped & VideoVote.upvotes
end
end
end
Then you feel good about making a call with absolutely no arguments AND you technically didn't add another method to your Video model (it's on the association, right?)
#video.voted_users.upvoted
I am using Rails 3 and wanted to get the classes a student has access to based upon the model below
class Student
has_many :students_levels
has_many :levels, :through => :students_levels
end
class Class
has_many :classes_levels
has_many :levels, :through => :classes_levels
end
class Level
has_many :students_levels
has_many :classes_levels
end
class StudentsLevel
belongs_to :students
belongs_to :levels
end
class ClassesLevel
belongs_to :classes
belongs_to :levels
end
I came up with the query below but didn't think it seemed like the best Rails way to do things and wanted to get additional suggestions. Thx
Class.where(:id => (ClassesLevel.where(:level_id => Student.find(1).levels)))
I want to add this as an instance method to Student and was thinking there would be a better way doing something with has many through.
I quite did not understand the whole logic behind your class structure. Why you are not connecting students directly into a class? And how a class can have many levels. I mean if you have Math1 and Math2, those are different classes, right? Or do you have Math1,2,3?
Well, anyway, here's the solution if you want to use current assosiations, I hope it suites your needs:
Class Student
...
def available_classes
Class.find(:all,
:include => {:levels => {:students_levels => :student}},
:conditions => ["students.id = ?", self.id])
end
And sorry, this is still in Rails 2.x format...
Consider a class:
class Link < ActiveRecord::Base
has_many :link_votes, :as => :vote_subject, :class_name => 'Vote'
has_many :spam_votes, :as => :vote_subject, :class_name => 'Vote'
end
The problem is, when I'm adding a new vote with #link.link_votes << Vote.new the vote_subject_type is 'Link', while I wish it could be 'link_votes' or something like that. Is this an AR limitation or is there a way to workaround this thing?
I've actually found one related answer, but I'm not quite sure about what it says: Polymorphic Association with multiple associations on the same model
Sounds like you want to use Single Table Inheritance - this will allow you to have two different types of Votes. This will add a 'type' column to the votes table that you will then access as a LinkVote or SpamVote
class SpamVote << Vote
...
end
Along those lines.
class Link < ActiveRecord::Base
has_many :link_votes, :as => :vote_subject
has_many :spam_votes, :as => :vote_subject
end
In the votes table you'd see columns like:
id, type, vote_subject_type, vote_subject_id, etc.
Do more research on STI and I bet you'll find your answer.
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