Rails preventing duplicates in polymorphic has_many :through associations
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 :]
精彩评论