Is there an easy or at least elegant way to prevent duplicate entries in polymorphic has_many through associations?
I've got two models, stories and links that can be tagged. I'm making a conscious decision to not use a plugin here. I want to actually understand everything that's going on and not be dependent on someone else's code that I don't fully grasp.
To see what my question is getting at, if I run the following in the console (assuming the story and tag objects exist in the database already)
s = Story.find_by_id(1)
t = Tag.find_by_id(1)
s.tags << t
s.tags << t
My taggings join table will have two entries added to it, each with the same exact data (tag_id = 1, taggable_id = 1, taggable_type = "Story"). That just doesn't seem very proper to me. So in an attempt to prevent this from happening I added the following to my Tagging model:
before_validation :validate_uniqueness
def validate_uniqueness
taggings = Tagging.find(:all, :conditions => { :tag_id => self.tag_id, :taggable_id => self.taggable_id, :taggable_type => self.taggable_type })
if !taggings.empty?
return false
end
return true
end
And it works almost as intended, but if I attempt to add a duplicate tag to a story or link I get an ActiveRecord::RecordInvalid: Validation failed exception. It seems that when you add an association to a list it calls the save! (rather than save sans !) method which raises exceptions if something goes wrong rather than just returning false. That isn't quite what I want to happen. I suppose I can surround any attempts to add new tags with a try/catch but that goes against the idea that you shouldn't expect your exceptions and this is something I fully expect to happen.
Is there a better way of doing this that won't raise exceptions when all I want to do is just silently not save the object to the database because a duplicate exists?
You could do it a couple of ways.
Define a custom add_tags method that loads all the existing tags then checks for and only adds the new ones.
Example:
def add_tags *new_tags
new_tags = new_tags.first if tags[0].kind_of? Enumerable #deal with Array as first argument
new_tags.delete_if do |new_tag|
self.tags.any? {|tag| tag.name == new_tag.name}
end
self.tags += new_tags
end
You could also use a before_save filter to ensure that the list of tags doesn't have any duplicates. This would incur a little more overhead because it would happen on EVERY save.
You can set the uniq option when defining has_many relation. Rails API docs says:
:uniq
If true, duplicates will be omitted from the collection. Useful in conjunction with :through.
(taken from: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001833 under "Supported options" subheading)
I believe this works...
class Tagging < ActiveRecord::Base
validate :validate_uniqueness
def validate_uniqueness
taggings = Tagging.find(:all, :conditions => {
:tag_id => self.tag_id,
:taggable_id => self.taggable_id,
:taggable_type => self.taggable_type })
errors.add_to_base("Your error message") unless taggings.empty?
end
end
Let me know if you get any errors or something with that :]
Related
I am adding a feature to an old app that was not made by me, this along with being relatively new to RoR is leading to some confusion for me.
I have models called reponse, activity_point, and report
response has two parents, it belongs_to activity_point and report.
I am trying to access activity_points for a do block like so:
report.responses.activity_points.activity.each do |a|
Obviously that isn't working. I am getting the error message:
undefined method `activity_points' for []:ActiveRecord::Relation
Thanks to anyone who can help me with this little problem.
Or you can add something like this to your Report model
has_many :responses
has_many :activity_points, :through => :responses
has_many :activities, :through => :activity_points
then you can do this
report.activities.each do |a|
Another way to do this kind of thing, add a method to Report and joins from the other side (to get activity objects)
def activities
Activity.joins(:activity_points => :responses).where('responses.report_id = ?', id)
end
The point of doing all this, you don't want to create Ruby objects if you don't need to. Nested loops are also a potential problem with unique items and sorting.
Each response have several activity_points so you should iterate through responses. Also each activity_point has several activities, so:
report.responses.each do |r|
r.activity_points.each do |ap|
ap.activity.each do |a|
# Do your thing
end
end
end
First, when you write report.responses, this will return an ActiveRecord array. Since activity_points is an undefined method for arrays, you can't call it. So to call this method there is two conditions:
You have to tell your app which element of the array will call the method. For instance, report.responses.first.activity_points or report.responses.second.activity_points ...
Response model has to have a has_many: activity_points to call this method.
You could still also use a loop, but that will take multiple DB calls. Therefore, my solution involves direct database call for efficiency.
Activity.includes(activity_point: {responses: :report}).where(reports: {id: report.id}).each do |a|
#...
#...
end
Forgive me if this is a basic question; I'm learning Rails (using 3.2) as I go.
My Event model has_many images. Each image has an is_primary boolean field. Event should have a cover_image method, which returns the image with is_primary set to true, or the first image otherwise. This is my code:
def cover_image
imgs = self.images
imgs.each { |i| return i if i.is_primary }
# If no primary
return imgs.first
end
I can't help feeling like there's a better way to do it, one that doesn't involve looping through all the elements just to find one.
You could do this with a scope quite easily:
Image model
scope :primary, where(:is_primary => true)
Event model
def cover_image
images.primary.first
end
This is a really basic example that should get you started at least, you'll want to build on it to handle a missing primary image gracefully, for example.
For completeness, you don't have to do this as a scope, if you'd prefer you can use where statements directly. Scopes are just really nice for staying DRY:
Event model
def cover_image
images.where(:is_primary => true).first
end
Update: This may be something that just isn't doable. See this
TLDR: How do you conditionally load an association (say, only load the association for the current user) while also including records that don't have that association at all?
Rails 3.1, here's roughly the model I'm working with.
class User
has_many :subscriptions
has_many :collections, :through => :subscriptions
end
class Collection
has_many :things
end
class Thing
has_many :user_thing_states, :dependent => :destroy
belongs_to :collection
end
class Subscription
belongs_to :user
belongs_to :collection
end
class UserThingState
belongs_to :user
belongs_to :thing
end
There exist many collections which have many things. Users subscribe to many collections and thereby they subscribe to many things. Users have a state with respect to things, but not necessarily, and are still subscribed to things even if they don't happen to have a state for them. When a user subscribes to a collection and its associated things, a state is not generated for every single thing (which could be in the hundreds). Instead, states are generated when a user first interacts with a given thing. Now, the problem: I want to select all of the user's subscribed things while loading the user's state for each thing where the state exists.
Conceptually this isn't that hard. For reference, the SQL that would get me the data needed for this is:
SELECT things.*, user_thing_states.* FROM things
# Next line gets me all things subscribed to
INNER JOIN subscriptions as subs ON things.collection_id = subs.collection_id AND subs.user_id = :user_id
# Next line pulls in the state data for the user
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I just don't know how to piece it together in rails. What happens in the Thing class? Thing.includes(:user_thing_states) would load all states for all users and that looks like the only tool. I need something like this but am not sure how (or if it's possible):
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
def state
# There should be only one user_thing_state if the include is correct, state method to access it.
self.user_thing_states.first
end
end
I need something like:
Thing.includes(:user_question_states, **where 'user_question_state.user_id => :user_id**).by_collections(user.collections)
Then I can do
things = User.things_subscribed_to
things.first.some_state_property # the property of the state loaded for the current user.
You don't need to do anything.
class User
has_many :user_thing_states
has_many :things, :through => :user_thing_states
end
# All Users w/ Things eager loaded through States association
User.all.includes(:things)
# Lookup specific user, Load all States w/ Things (if they exist for that user)
user = User.find_by_login 'bob'
user.user_thing_states.all(:include => :things)
Using includes() for this already loads up the associated object if they exist.
There's no need to do any filtering or add extra behavior for the Users who don't have an associated object.
Just ran into this issue ourselves, and my coworker pointed out that Rails 6 seems to include support for this now: https://github.com/rails/rails/pull/32655
*Nope, didn't solve it :( Here's a treatment of the specific issue I seem to have hit.
Think I've got it, easier than expected:
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
scope :with_user_state, lambda { |user|
includes(:user_thing_states).where('user_thing_states.user_id = :user_id
OR user_thing_states.user_id IS NULL',
{:user_id => user.id}) }
def state
self.user_thing_states.first
end
end
So:
Thing.with_user_state(current_user).all
Will load all Things and each thing will have only one user_question_state accessible via state, and won't exclude Things with no state.
Answering my own question twice... bit awkward but anyway.
Rails doesn't seem to let you specify additional conditions for an includes() statement. If it did, my previous answer would work - you could put an additional condition on the includes() statement that would let the where conditions work correctly. To solve this we'd need to get includes() to use something like the following SQL (Getting the 'AND' condition is the problem):
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I'm resorting to this for now which is a bit awful.
class User
...
def subscribed_things
self.subscribed_things_with_state + self.subscribed_things_with_no_state
end
def subscribed_things_with_state
self.things.includes(:user_thing_states).by_subscribed_collections(self).all
end
def subscribed_things_with_no_state
Thing.with_no_state().by_subscribed_collections(self).all
end
end
I have the following one to many associations. Document has many Sections and Section has many Items.
class Document < ActiveRecord::Base
has_many :document_sections, :dependent => :destroy, :autosave => true
has_many :document_items, :through => :document_sections
end
class DocumentSection < ActiveRecord::Base
belongs_to :document
has_many :document_items, :dependent => :destroy, :autosave => true
end
class DocumentItem < ActiveRecord::Base
belongs_to :document_section
end
Here is the params hash:
-
Parameters: {"commit"=>"Submit Document", "authenticity_token"=>"4nx2B0pJkvavDmkEQ305ABHy+h5R4bZTrmHUv1setnc=", "id"=>"10184", "document"=>{"section"=>{"10254"=>{"seqnum"=>"3", "item"=>{"10259"=>{"comments"=>"tada"}}}}, "comment"=>"blah"}}
I have the following update method...
# PUT /documents/1
# PUT /documents/1.xml
def update
#document = Document.find(params[:id])
# This is header comment
#document.comment = params[:document][:comment]
params[:document][:section].each do |k,v|
document_section = #document.document_sections.find_by_id(k)
if document_section
v[:item].each do |key, value|
document_item = document_section.feedback_items.find_by_id(key)
if document_item
# This is item comments
document_item.comments = value[:comments]
end
end
end
end
#document.save
end
When I save the document it only updates the document header comments. It does not save the document_item comments. Shouldn't the autosave option also update the associations.
In the log only the following DML is registered:
UPDATE documents SET updated_at = TO_DATE('2010-03-09 08:35:59','YYYY-MM-DD HH24:MI:SS'), comment = 'blah' WHERE id = 10184
How do I save the associations by saving the document.
I think I see what the problem is. I'm pretty sure that you cannot do the following:
# Triggers a database call
document_section = #document.document_sections.find_by_id(k)
And expect ActiveRecord to keep the association for autosaves. Instead, you should save the loaded records individually. Which of course would not be atomic.
I believe for autosave to work like you are thinking, you want to do something like this:
# untested...
#document.document_sections.collect { |s| s.id == k }.foo = "bar"
Notice that here I'm actually modifying a fake param foo in the array, instead of calling find_by_id, which will re-query the database and return a new object.
A third option you have is that you could of course, do what you had originally planned, but handle all the transactions yourself, or use nested transactions, etc, to get the atmoic saves. This would be necessary if your data was too large for array manipulation to work since autosave by it's natures triggers a load of all associated data into memory.
It all depends on your application.
Some clarifications on the underlying problem:
If you run the find_by_id method, you are asking ActiveRecord to return to you a new set of objects that match that query. The fact that you executed that method from an instance (document_sections) is really just another way of saying:
DocumentSection.find_by_id(k)
Calling it from an object instance I think is just some syntactic niceness that rails is adding on the top of things, but in my mind it doesn't make a lot of sense; I think it could be handy in some application, I'm not sure.
On the other side, collect is a Ruby Array method that offers a way to "slice" an array using a block. Basically a fancy foreach loop. :) By interacting with the document_sections array directly, you are changing the same objects already loaded into the containing object (#document), which will then be committed when you save with the special autosave flag set.
HTH! Glad you are back and running. :)
I am trying to accomplish the atypical library learning a language application. I have been able to create Books, People, and BookCheckOuts. I've got the relationships working pretty well but am having issues with wrapping my head around how to handle books that have never been checked out.
I've created two properties on my book class CheckedOut (returns a boolean) and LastCheckedOutTo (returns a person). I am pretty much at peace with CheckedOut and feel confident that I am using the right RoR mechanism for determining if a book is presently checked out and returning a boolean in either case. I am not as confident about LastCheckedOutTo as my implementation seems like a kludge.
Am I going about this correctly? Is there a better way?
Book Class in its Entirety
class Book < ActiveRecord::Base
has_many :book_check_outs
has_many :people, :through => :book_check_outs
def checked_out
if (book_check_outs.find(:first, :conditions => "return_date is null"))
true
else
false
end
end
def last_checked_out_to
if (book_check_outs.count > 0)
book_check_outs.find(:first,
:order => "out_date desc").person
else
Person.new()
end
end
end
Perhaps:
class Book < ActiveRecord::Base
has_many :book_loans
has_many :borrowers, :class_name => 'Person', :through => :book_loans
def loaned?
book_loans.exists?(:return_date => nil)
end
# I would be reluctant to return a new Person object
# just because it was not checked out by anyone, instead you could return nil
# OR exception out.
def current_borrower
book_loans.first(:order => "out_date desc").person
end
end
# you can use a helper to keep your presentation clean
module BookHelper
def borrower_name(book)
if borrower = book.borrower
borrower.name
else
"not checked out"
end
end
end
There are actually lots of ways to do this. Here are some ideas:
You can add order and another has_many since you really care about the return date:
has_many :book_check_outs, :order => "out_date asc"
has_many :current_book_check_outs, :conditions=>'return_date is null'
Then you get:
def checked_out?
current_book_check_outs.any?
end
def last_checked_out_to
if (book_check_outs.count > 0)
book_check_outs.last.person
else
Person.new()
end
end
But I'm a little confused about how I'd use last_checked_out_to. I think I'd prefer it to return nil if it has no last person.
You should check out named scopes, as those help build these dynamic queries modularly. They would work quite well here.
Although you're not using persons (people?) in this code, I'd rework the terminology a little bit so it reads better. book.persons doesn't really seem right for what it's telling us. What do librarians call them? book.checker_outters or something?
Regarding def checked_out i'd
rename it to checked_out? since
there is unwritten (or maybe
written) ruby convention that any
method returning true or falls
end up with question-mark.
The second method is pretty much ok,
but it won't go well for heavy-dute
websites. I'd suggest denormalizing
this part and adding
last_checked_out_to_id attribute to
books table and updating it after
each checkout process. Other way
would be
book_check_outs.last.person for
existing person and
book_check_outs.people.build for new one.
It may well be overkill for this particular example but alternatively you might want to explore the option of implementing a state machine i.e. aasm.