Validates presence of each other in two associated models - ruby-on-rails

I have the following two models:
class Parent < ActiveRecord::Base
has_one :child, dependent: :destroy
validates :child, presence: true
end
class Child < ActiveRecord::Base
belongs_to :parent
validates :parent, presence: true
end
I want to create Parent object.
If I do the following:
Parent.create! or Factory(:parent)
Exception raises: ActiveRecord::RecordInvalid: Validation failed: Child can't be blank
But I can't create Child object without Parent object for the same reason - I need to create Parent object first in order to pass presence validation.
As it appears I have some kind of infinite recursion here.
How to solve it?

UPDATE:
The code below works well in my environment ( Rails3.2.2, ruby 1.8.7)
# parent.rb
class Parent < ActiveRecord::Base
has_one :child
validates :child, :presence => true
end
# child.rb
class Child < ActiveRecord::Base
belongs_to :parent
validate :parent, :presence => true
end
# parent_test.rb
require 'test_helper'
class ParentTest < ActiveSupport::TestCase
test "should be saved" do
parent = Parent.new(:name => "111")
child = Child.new(:name => "222", :parent => parent)
parent.child = child
parent.save!
puts "after saved, parent: #{parent.inspect}"
puts "after saved, child: #{child.inspect}"
assert parent.id > 0
assert child.id > 0
end
end
run this test and got:
Started
after saved, parent: #<Parent id: 980190963, name: "111", created_at: "2012-04-05 23:19:31", updated_at: "2012-04-05 23:19:31">
after saved, child: #<Child id: 980190963, name: "222", parent_id: 980190963, created_at: "2012-04-05 23:19:31", updated_at: "2012-04-05 23:19:31">
.
Finished in 0.172716 seconds.
1 tests, 2 assertions, 0 failures, 0 errors
PREVIOUS ANSWER ================
try to initialize them separately, then add the association, at last save them.
parent = FactoryGirl.build(:parent)
child = FactoryGirl.build(:child, :parent => parent)
parent.child = child
parent.save
child.save # seems this line of code is redundant? I am not sure.
for more details of "build, create", see its official website: https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md

Related

Raise an exception when parent fails to save

I've run into a gotcha when trying to create a parent and child at the same time. I would think an exception should be raised when the associated parent fails to save, but it doesn't.
class Child < ApplicationRecord
belongs_to :parent
validates_presence_of :parent, :name
end
class Parent < ApplicationRecord
has_one :child
validates_presence_of :name
end
Notice the child saves and their is no visibility into the save issue with the parent:
parent = Parent.new
parent.valid? # => false
child = Child.create!(name: 'something', parent: parent) # => true
child.parent_id # => nil
child.reload.valid? # => false
An an invalid child has been created. Why is the create! method not calling create! on the parent as well so an exception is raised?
Its worth noting that When the same process is followed, but instead we start from the parent, we get the behavior I expect:
child = child.new
child.valid? # => false
parent = Parent.create!(name: 'something', child: child) # => ActiveRecord::Exception
parent.valid? # => false
I know some work arounds (e.g. validates_associated :parent on the child), but I'm trying to understand why Rails is behaving the way it is.
Rails doesn't know you're trying to save them at the same time. If you want to save them both at once, try nested attributes.
I would expect this to do what you want:
class Child < ApplicationRecord
belongs_to :parent
validates_presence_of :parent
end
class Parent < ApplicationRecord
has_many :children
validates_presence_of :name
accepts_nested_attributes_for :children, allow_destroy: true
end
parent = Parent.new children: [child]
parent.save!
Why would the exception be raised? You are setting the parent and as far as the child is concerned, the parent exists (passing the validation) and it will not call the create! on the parent and IMO nor should it.
Your workaround is to validate on the parent_id instead of the parent or create the child via parent.children.create!. The latter will raise ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved if the parent is not persisted
change children to
class Child < ApplicationRecord
belongs_to :parent
validates_presence_of :parent, :id
end
It is as #AbM said you should put the constraint on some field, This will give you ActiveRecord::RecordInvalid: Validation failed: Id can't be blank exception.

Why does `validates_associated` exist?

This section of railsguide says:
You should use this helper when your model has associations with other models and they also need to be validated.
So I thought validation of associated models wouldn't be run without validates_associated.
But actually, It was run without it.
There are two models, School and Student.
class School < ActiveRecord::Base
has_many :students
validates :name, presence: true
end
class Student < ActiveRecord::Base
belongs_to :school
validates :name, presence: true
end
On rails console,
school = School.new
=> #<School id: nil, name: nil, created_at: nil, updated_at: nil>
school.students << Student.new
=> #<ActiveRecord::Associations::CollectionProxy [#<Student id: nil, name: nil, school_id: nil, created_at: nil, updated_at: nil>]>
school.name = "test shcool"
=> "test shcool"
school.save
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> false
school.errors.full_messages
=> ["Students is invalid"]
If with validates_associated like below:
class School < ActiveRecord::Base
has_many :students
validates :name, presence: true
validates_associated :students
end
class Student < ActiveRecord::Base
belongs_to :school
validates :name, presence: true
end
On rails console, I ran the exact same commands as above. But the last command school.errors.full_messages returned different result. (It is strange that there are duplicate error messages.)
school.errors.full_messages
=> ["Students is invalid", "Students is invalid"]
My questions are
Is this a RailsGuide's mistake?
Why does validates_associated exist?
Or do I have any mistaken idea?
My environment is
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin14.0]
Rails 4.2.0
it checks to see if the associated objects are valid before saving

How to prevent duplicate records in a Join Table

I'm quite new to Ruby and Rails so please bear with me.
I have two models Player, and Reward joined via a has_many through relationship as below. My Player model has an attribute points. As a player accrues points they get rewards. What I want to do is put a method on the Player model that will run before update and give the appropriate reward(s) for the points they have like below.
However I want to do it in such a way that if the Player already has the reward it won't be duplicated, nor cause an error.
class Player < ActiveRecord::Base
has_many :earned_rewards, -> { extending FirstOrBuild }
has_many :rewards, :through => :earned_rewards
before_update :assign_rewards, :if => :points_changed?
def assign_rewards
case self.points
when 1000
self.rewards << Reward.find_by(:name => "Bronze")
when 2000
self.rewards << Reward.find_by(:name => "Silver")
end
end
class Reward < ActiveRecord::Base
has_many :earned_rewards
has_many :players, :through => :earned_rewards
end
class EarnedReward < ActiveRecord::Base
belongs_to :player
belongs_to :reward
validates_uniqueness_of :reward_id, :scope => [:reward_id, :player_id]
end
module FirstOrBuild
def first_or_build(attributes = nil, options = {}, &block)
first || scoping{ proxy_association.build(attributes, &block) }
end
end
You should validate it in db also
Add follwing in migrate file-
add_index :earnedrewards, [:reward_id, :player_id], unique: true
EDIT:
I've realised that my previous answer wouldn't work, as the new Reward is not associated to the parent Player model.
In order to correctly associate the two, you need to use build.
See https://stackoverflow.com/a/18724458/4073431
In short, we only want to build if it doesn't already exist, so we call first || build
Specifically:
class Player < ActiveRecord::Base
has_many :earned_rewards
has_many :rewards, -> { extending FirstOrBuild }, :through => :earned_rewards
before_update :assign_rewards, :if => :points_changed?
def assign_rewards
case self.points
when 1000...2000
self.rewards.where(:name => "Bronze").first_or_build
when 2000...3000
self.rewards.where(:name => "Silver").first_or_build
end
end
class Reward < ActiveRecord::Base
has_many :earned_rewards
has_many :players, :through => :earned_rewards
end
class EarnedReward < ActiveRecord::Base
belongs_to :player
belongs_to :reward
validates_uniqueness_of :reward_id, :scope => [:reward_id, :player_id]
end
module FirstOrBuild
def first_or_build(attributes = nil, options = {}, &block)
first || scoping{ proxy_association.build(attributes, &block) }
end
end
When you build an association, it adds it to the parent so that when the parent is saved, the child is also saved. E.g.
pry(main)> company.customers.where(:fname => "Bob")
Customer Load (0.1ms) SELECT "customers".* FROM "customers"
=> [] # No customer named Bob
pry(main)> company.customers.where(:fname => "Bob").first_or_build
=> #<Customer id: nil, fname: "Bob"> # returns you an unsaved Customer
pry(main)> company.save
=> true
pry(main)> company.reload.customers
=> [#<Customer id: 1035, fname: "Bob">] # Bob gets created when the company gets saved
pry(main)> company.customers.where(:fname => "Bob").first_or_build
=> #<Customer id: 1035, fname: "Bob"> # Calling first_or_build again will return the first Customer with name Bob
Since our code is running in a before_update hook, the Player will be saved as well as any newly built Rewards as well.

In Rails, is there an easy way create child objects at the same time as their parents?

Note: This question was spawned from another question I had regarding the use of accepts_nested_attributes_for. You may reference that question for additional context, if needed.
I believe this question is best explained with a simple example:
class Foo < ActiveRecord::Base
has_many :bars, inverse_of: :foo
end
class Bar < ActiveRecord::Base
validates :foo_id, presence: true
belongs_to :foo, inverse_of: :bars
end
f = Foo.new()
=> #<Foo id: nil, created_at: nil, updated_at: nil>
b = f.bars.build()
=> #<Bar id: nil, foo_id: nil, created_at: nil, updated_at: nil>
f.save!
=> ActiveRecord::RecordInvalid: Validation failed: Bars foo can't be blank
Is there an easy way to fix this problem? I know that I could save f first and then build b, but my situation is a little more complex than this example (see the question I referenced above) and I'd prefer to avoid that if possible.
The child records get created at the same time as parent, this is why your validation is failing, your child is not yet persisted. to make it work i would write a custom validation like this
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars, :allow_destroy => true
end
class Bar < ActiveRecord::Base
belongs_to :foo
validates_presence_of :bar
end
You can use a callback to create the object (Maybe before_save?). See here.

nested mass assignment with mongoid

with a has_one/belongs_to relationship, i cannot seem to update nested records via mass assignment.
models:
class ProductVariation
include Mongoid::Document
has_one :shipping_profile, :inverse_of => :variation
field :quantity
attr_accessible :shipping_profile_attributes
accepts_nested_attributes_for :shipping_profile
end
class ShippingProfile
include Mongoid::Document
belongs_to :variation, :class_name => "ProductVariation"
field :weight, :type => Float
attr_accessible :weight
end
controller:
#variation = ProductVariation.find(params[:id])
#variation.update_attributes(params[:product_variation])
post request:
Parameters:{
"product_variation"=>{
"quantity"=>"13",
"shipping_profile_attributes"=>{
"weight"=>"66",
"id"=>"4dae758ce1607c1d18000074"
}
},
"id"=>"4dae758ce1607c1d18000073"
}
mongo query:
MONGODB app_development['product_variations'].update({"_id"=>BSON::ObjectId('4dae758ce1607c1d18000073')}, {"$set"=>{"quantity"=>13, "updated_at"=>2011-04-28 06:59:17 UTC}})
and i dont even get a mongo update query if the product_variation doesnt have any changed attributes... what am i missing here?
Working models and a unit test are below to demonstrate that you can update child parameters and save to the database
through the parent as intended via accepts_nested_attributes_for and the autosave: true option for relations.
The Mongoid documentation says that an error will be raised for an attempt to set a protected field via mass assignment,
but this is out of date.
Instead, messages like the following are printed to the log file.
WARNING: Can't mass-assign protected attributes: id
You should carefully look for for these messages in the appropriate log file to diagnose your problem.
This will help you notice that you have a nested id field for the shipping profile in your parameters,
and this seems to cause the weight to be rejected as well, probably along with all child parameters.
After adding "attr_accessible :id" to the ShippingProfile model, the weight now gets assigned.
You also need to add "attr_accessible :quantity" (and I've added :id for the unit test) to the ProductVariation model
The next issue is that you need "autosave: true" appended to the has_one relation
in order to have the child updated through the parent,
otherwise you will have to save the child manually.
You might also be interested in sanitize_for_mass_assignment, which can be used to launder out ids.
include ActiveModel::MassAssignmentSecurity
p sanitize_for_mass_assignment(params['product_variation'], :default)
The unit test should make the whole subject clear, I'll leave the controller work to you.
Hope that this is clear and that it helps.
class ProductVariation
include Mongoid::Document
has_one :shipping_profile, :inverse_of => :variation, autosave: true
field :quantity
accepts_nested_attributes_for :shipping_profile
attr_accessible :id
attr_accessible :quantity
attr_accessible :shipping_profile_attributes
end
class ShippingProfile
include Mongoid::Document
belongs_to :variation, :class_name => "ProductVariation"
field :weight, :type => Float
attr_accessible :id
attr_accessible :weight
end
test/unit/product_varitation_test.rb
require 'test_helper'
class ProductVariationTest < ActiveSupport::TestCase
def setup
ProductVariation.delete_all
ShippingProfile.delete_all
end
test "mass assignment" do
params = {
"product_variation"=>{
"quantity"=>"13",
"shipping_profile_attributes"=>{
"weight"=>"66",
"id"=>"4dae758ce1607c1d18000074"
}
},
"id"=>"4dae758ce1607c1d18000073"
}
product_variation_id = params['id']
shipping_profile_id = params['product_variation']['shipping_profile_attributes']['id']
product_variation = ProductVariation.create("id" => product_variation_id)
shipping_profile = ShippingProfile.create("id" => shipping_profile_id)
product_variation.shipping_profile = shipping_profile
assert_equal(1, ProductVariation.count)
assert_equal(1, ShippingProfile.count)
product_variation.update_attributes(params['product_variation'])
assert_equal('13', ProductVariation.find(product_variation_id)['quantity'])
assert_equal(66.0, ShippingProfile.find(shipping_profile_id)['weight'])
p ProductVariation.find(product_variation_id)
p ShippingProfile.find(shipping_profile_id)
end
end
test output
Run options: --name=test_mass_assignment
# Running tests:
#<ProductVariation _id: 4dae758ce1607c1d18000073, _type: nil, quantity: "13">
#<ShippingProfile _id: 4dae758ce1607c1d18000074, _type: nil, variation_id: BSON::ObjectId('4dae758ce1607c1d18000073'), weight: 66.0>
.
Finished tests in 0.014682s, 68.1106 tests/s, 272.4424 assertions/s.
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
Process finished with exit code 0

Resources