views:

707

answers:

2

I am having trouble with validations on a has_many relationship where the children exist, but the parent doesn't. However, when creating/saving the parent object, I want to ensure that specific children (with certain attributes) have already been saved.

There is a Parent object that has_many Child objects. The Child objects are persisted into the database first, and thus don't have any reference to the parent. The association structure is:

Parent
  - has_many :children 

Child
  - someProperty: string
  - belongs_to: parent

For example, there are three child objects:

#1 {someProperty: "bookmark", parent: nil}
#2 {someProperty: "history", parent: nil }
#2 {someProperty: "window", parent: nil }

A parent is valid only if it contains child objects with someProperty history and window.

I am setting up the parent inside the controller as:

p = Parent.new(params[:data])
for type in %w[bookmark_id history_id window_id]
    if !params[type].blank?
        p.children << Child.find(params[type])
    end
end
// save the parent object p now
p.save!

When the children are assigned to the parent with <<, they are not saved immediately as the parent's id does not exist. And for the parent to be saved, it must have at least those 2 children. How could I solve this problem? Any input is welcome.

+1  A: 

First thing, if you want the children to be saved without the parent id then there is no point in doing this

 p = Parent.new(params[:data])
 for type in %w[bookmark_id history_id window_id]
   if !params[type].blank?
     p.children << Child.find(params[type])
   end
 end

the whole purpose of

 p.children << some_child

is to attach the parent id to the child object which you are not doing here because the parent doesn't exist yet.

The other thing is if you just want to make sure that the parent has a child object and if you are creating child and parent together then you can use transaction block around the parent and child creation which will make sure that the parent has child, like

 transaction do
   p = create_parent
   p.children << child1
   p.children << child2
 end

So, within the transaction, if at any stage code fails then it will rollback the whole db transaction , i.e you will either have one parent with 2 children or nothing, if that's the end state you are looking for.

EDIT: Since you can't create a parent unless it has 2 children, in that case, instead of

p = Parent.new(params[:data])
 for type in %w[bookmark_id history_id window_id]
   if !params[type].blank?
     p.children << Child.find(params[type])
   end
 end

do

 children = []
 for type in %w[bookmark_id history_id window_id]
   if !params[type].blank?
     children << Child.find(params[type])
   end
 end

 if children.size >= 2
   p = Parent.create!(params[:data])
   children.each {|child| p.children << child}
 end

Does that make sense

nas
thanks for the answer. I don't specifically want to save the children without the parent id, but just that the children are always created first in the process. i won't have any information about the parent at that point. the parent's info will come in a later HTTP request. the child objects are disposable, and i don't care if they don't have a parent as a cron job will clean them up on a regular basis. i could technically use a session, but that will definitely kill the server as the child objects are really heavyweight, so i opted for storing em in the db directly.
Anurag
Right I understand what you mean now, I will edit my answer then
nas
+1  A: 

Not sure why you need to do such a thing, but anyway, how about doing this?

class Parent < ActiveRecord::Base

  CHILDREN_TYPES = %w[bookmark_id history_id window_id]
  CHILDREN_TYPES.each{ |c| attr_accessor c }

  has_many :children

  before_validation :assign_children
  validate :ensure_has_proper_children

private

  def assign_children
    CHILDREN_TYPES.each do |t|
      children << Child.find(send(t)) unless send(t).blank?
    end
  end

  def ensure_has_proper_children
    # Test if the potential children meet the criteria and add errors to :base if they don't
  end
end

Controller:

...
p = Parent.new(params[:data])
p.save!
...

As you can see, I moved all the logic to model at the first place. Then, there is a two-step process for saving children. First, we assign children to the parent and then we validate if they meet the required criteria (insert your logic there).

Sorry for being short. I'll answer any further questions if necessary.

Milan Novota
I like this approach as it keeps the logic within the model. What does the send(t) function do here? Is it being called on the parent object?
Anurag
Yes, since we define the children types dynamically as accessors (attributes) of the parent object, we need to access them dynamically as well. That's why we need to call the send method on the parent object.
Milan Novota