views:

779

answers:

3

To be more specific, "How do I validate that a model requires at least x valid associated models to be created?". I've been trying to validate nested models that get created in the same form as the parent (and ultimately show immediate validations a la jQuery). As a popular example, lets assume the following models and schema.

class Project
  include DataMapper::Resource

  property :id,     Serial
  property :title,  String, :nullable => false

  has 2..n, :tasks
end

class Task
  include DataMapper::Resource

  property :id,         Serial
  property :project_id, Integer,  :key => true
  property :title,      String,   :nullable => false

  belongs_to :project
end

All the validations are done in the schema definitions, as you can see. The important one here is "has 2..n, :tasks". This validation actually works normally, given that the nested task attributes in the params hash will produce valid tasks. If they produce an invalid task, however, then the task won't get created and you'll end up with a Project that has less than 2 tasks, and thus an invalid project object.

As I understand it, this is because it can't figure out if the task attributes are valid or not until it attempts to save the tasks, and since - as far as I know - the tasks can't get saved before the project, the project is unaware if the tasks will be valid or not. Am I correct in assuming this?

Anyway, I was hoping there would be a quick answer, but it seems a lot less trivial then I'd hoped. If you've got any suggestions at all, that would be greatly appreciated.

A: 

SET CONSTRAINTS DEFERRED might be useful if your database engine supports that.

Otherwise, maybe write a stored procedure to do the inserts, and then say that its the resonsibility of the stored procedure to ensure that only correct, validated data is inserted.

ChrisW
Thanks Chris, these are good suggestions, although I'm not looking for a database-specific solution. I want the logic and validations to stay within the app itself. If worst comes to worst I'd just write an after-save hook to validate the records (ugly, I know, but might be the only sensible approach).
Mike Richards
A: 

There is a model method valid? that runs the validations on a model object before it is saved. So, the simple way to validate the associations would be to use validates_with_block' or 'validates_with_method to check the validations on the associations.

It would look something like this

validates_with_block do
  if @tasks.all?{|t|t.valid?}
    true
  else
    [false, "you have an invalid task"]
  end
end

Or you could look at dm-association-validator or dm-accepts-nested-attributes

Edit: for extra crazy. run validations on the tasks, then check to see if the only errors are ones related to the association.

validates_with_block do
  if @tasks.all?{|t|t.valid?;!t.errors.any?{|e|e[0]==:project}}
    true
  else
    [false, "you have an invalid task"]
  end
end
BaroqueBobcat
Unfortunately all tasks are going to be invalid at that point, since they're not saved yet (having ID=>nil, project_id=>nil, etc.), so the validation will always return false. This is where the problem is, since the parent AND children get created in the same form, at the same time.
Mike Richards
I also tried dm-accepts_nested_attributes with no luck. I had a chat with snusnu (the plugin author) in IRC, and we couldn't seem to figure it out. It's a known issue, but a grey area it seems.
Mike Richards
You could do something rather hacky and check the errors objects of the tasks to see if they only have errors related to the project.
BaroqueBobcat
+1  A: 

I actually found a nice solution here using transactions in DataMapper. Basically this transaction tries to save the parent object as well as all the child objects. As soon as one fails to save, then the transaction stops and nothing gets created. If all goes well, then the objects will save successfully.

class Project
  def make
    transaction do |trans|
      trans.rollback unless save
      tasks.each do |task|
        unless task.save
          trans.rollback
          break
        end
      end
    end
  end
end

This assures that everything is valid before it anything gets saved. I just needed to change my #save and #update methods to #make in my controller code.

Mike Richards
Do you think you could illustrate this with a practical example?
arbales