validates_uniqueness_of in destroyed nested model rails
Asked Answered
S

5

21

I have a Project model which accepts nested attributes for Task.

class Project < ActiveRecord::Base  
  has_many :tasks
  accepts_nested_attributes_for :tasks, :allow_destroy => :true
end

class Task < ActiveRecord::Base  
  validates_uniqueness_of :name
end

Uniqueness validation in Task model gives problem while updating Project.

In edit of project i delete a task T1 and then add a new task with same name T1, uniqueness validation restricts the saving of Project.

params hash look something like

task_attributes => { {"id" =>
"1","name" => "T1", "_destroy" =>
"1"},{"name" => "T1"}}

Validation on task is done before destroying the old task. Hence validation fails.Any idea how to validate such that it doesn't consider task to be destroyed?

Setsukosett answered 5/5, 2010 at 10:12 Comment(7)
Just Curious Why dont you update your Old Task Instead of Deleting Old & creating new task with the same name.Venule
You mean i need to go through old tasks and check if there is any old task with same name as new task but that is marked to be destroyed and then just update that old task?Setsukosett
Arun ... is this just a test case (adding a task with the same name as another task you're deleting) or are you doing this on every edit ie Deleting tasks and recreating them.Dereliction
trustfundbaby - actually i didnt have that test case in mind. By mistake i deleted a task. So i HAD to add a new task with the same name as deleted task. I ended up with the above scenario.Setsukosett
I'm confused, if you already deleted the task why are the parameters for the deletion of the task being passed when you go to submit the edit? Are you deleting the task via js and then sending the params for the actual deletion afterwards?Dereliction
I am following accepts_nested_attributes method for adding many tasks for a project model.While editing a project, many tasks can be added or deleted. On click of delete it wont be deleted until we submit the edit form but an attribute _destroy will be set to true. After submit all the tasks with _destroy set to true will be deleted.Setsukosett
See also, stackoverflow.com/questions/14534665Showiness
J
17

Andrew France created a patch in this thread, where the validation is done in memory.

class Author
  has_many :books

  # Could easily be made a validation-style class method of course
  validate :validate_unique_books

  def validate_unique_books
    validate_uniqueness_of_in_memory(
      books, [:title, :isbn], 'Duplicate book.')
  end
end

module ActiveRecord
  class Base
    # Validate that the the objects in +collection+ are unique
    # when compared against all their non-blank +attrs+. If not
    # add +message+ to the base errors.
    def validate_uniqueness_of_in_memory(collection, attrs, message)
      hashes = collection.inject({}) do |hash, record|
        key = attrs.map {|a| record.send(a).to_s }.join
        if key.blank? || record.marked_for_destruction?
          key = record.object_id
        end
        hash[key] = record unless hash[key]
        hash
      end
      if collection.length > hashes.length
        self.errors.add_to_base(message)
      end
    end
  end
end
Joslin answered 21/5, 2010 at 14:54 Comment(4)
add_to_base has been deprecated and is unavailable in 3.1. Use self.errors.add(:base, message)Bodega
Heh, I received an email from someone saying they found my workaround via StackOverflow. Nice to know it has helped people. Maybe I should update it for Rails 3 as that implementation would be considered very bad nowadays!Spall
Thanks to Andrew, saved my lot of time. I didn't find any other way to achieve this.Timotheus
Brian, try this one: techbrownbags.wordpress.com/2014/02/05/…Macedonian
P
3

As I understand it, Reiner's approach about validating in memory would not be practical in my case, as I have a lot of "books", 500K and growing. That would be a big hit if you want to bring all into memory.

The solution I came up with is to:

Place the uniqueness condition in the database (which I've found is always a good idea, as in my experience Rails does not always do a good job here) by adding the following to your migration file in db/migrate/:

  add_index :tasks [ :project_id, :name ], :unique => true

In the controller, place the save or update_attributes inside a transaction, and rescue the Database exception. E.g.,

 def update
   @project = Project.find(params[:id])
   begin
     transaction do       
       if @project.update_attributes(params[:project])
          redirect_to(project_path(@project))
       else
         render(:action => :edit)
       end
     end
   rescue
     ... we have an exception; make sure is a DB uniqueness violation
     ... go down params[:project] to see which item is the problem
     ... and add error to base
     render( :action => :edit )
   end
 end

end

Pet answered 4/8, 2013 at 20:34 Comment(1)
Excellent approach! This answer provides the best solution, imo. Robust, fast, fail-safe, and reliable. Database indexes can't lie. ;)Alber
B
2

Rainer Blessing's answer is good. But it's better when we can mark which tasks are duplicated.

class Project < ActiveRecord::Base
  has_many :tasks, inverse_of: :project

  accepts_nested_attributes_for :tasks, :allow_destroy => :true
end

class Task < ActiveRecord::Base
  belongs_to :project

  validates_each :name do |record, attr, value|
    record.errors.add attr, :taken if record.project.tasks.map(&:name).count(value) > 1
  end
end
Bummalo answered 31/8, 2016 at 10:29 Comment(0)
S
1

For Rails 4.0.1, this issue is marked as being fixed by this pull request, https://github.com/rails/rails/pull/10417

If you have a table with a unique field index, and you mark a record for destruction, and you build a new record with the same value as the unique field, then when you call save, a database level unique index error will be thrown.

Personally this still doesn't work for me, so I don't think it's completely fixed yet.

Showiness answered 4/12, 2013 at 18:34 Comment(0)
V
-4

Ref this

Why don't you use :scope

class Task < ActiveRecord::Base
  validates_uniqueness_of :name, :scope=>'project_id' 
end

this will create unique Task for each project.

Venule answered 5/5, 2010 at 12:37 Comment(3)
scope => 'project_id' will work for different project ids, but in the above case project_id is same. I need to destroy a task then add a new task with same name for a project.Setsukosett
will you please paste your code here so that one could help you.Venule
In controller def update @project = Project.find(params[:id]) @project.update_attributes(params[:project]) end Model code is same as i have mentioned above.I have done exactly similar to as described by Ryan Bates in railscasts episode 197Setsukosett

© 2022 - 2024 — McMap. All rights reserved.