Anurag
Anurag

Reputation: 141859

Rails validation for a has_many association

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.

Upvotes: 4

Views: 7276

Answers (2)

Milan Novota
Milan Novota

Reputation: 15596

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.

Upvotes: 5

nas
nas

Reputation: 3696

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

Upvotes: 1

Related Questions