Enforcing a Uniqueness Constraint in a Nested Form
I'm trying not to fight the defaults here and 开发者_如何转开发use Rails built-in support for nested attributes (from http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes). I'm labeling Things with Tags, and all works swell, and for each Thing I have a form with a nested field that creates a new Tag by a name. Trouble is, I need to make sure that each Tag has a unique name. Instead of creating a new Tag, if a user enters the name of one that already exists, I need to create the associate with that pre-existing Tag. How do I do this?
There's probably a better way to do this but this is about the best I can come up with for now.
In a has_many(:through) association accepts_nested_arguments_for uses an assignment to the virtual attribute #{association}_attributes
to work its magic. It expects an array of hashes, where each hash contains attribute keys and their values. Any hashes with an id will be updated (or deleted if there is a :_delete
key with the value of true). Any hashes missing an id will be used to create new items of that association. So the key is to intercept the call to tags_associations=
and check any of the hashes that are missing ids for an existing tag with the same name value, and replace it with an entry that tags_attributes will use to make the association to the existing tag. N.B. for has_one and belongs_to relationships tag_attributes will expect a single hash. The code will be similar, but much simpler.
class Thing < ActiveRecord::Base
has_many :tags, :through => :taggings
has_many :taggings
accepts_nested_attributes_for :tags
def tags_attributes_with_recycling=(attributes)
existing_attributes = attributes.reject{|attribute| attribute[:id].nil?}
new_attributes = attributes - existing_attributes
new_and_recycled_attributes = new_attributes.map { |attribute|
tag_id = Tag.find_by_name(attribute[:name]).id
tag_id ? {:id => tag_id) : attribute
}
tags_attributes_without_recycling= (existing_attributes + new_and_recycled_attributes)
end
alias_method_chain :tags_attributes=, :recycling
end
It's untested, so no guarantees. But it should at least put you on track for a solution.
In your Thing class, define a tags_attributes= method to override the default and use Tag.find_or_create
def tags_attributes=(attributes)
attributes.each do |attr|
Tag.find_or_create_by_name(attr[:name])
end
end
Not sure what the attributes hash will look like but you get the idea.
精彩评论