Niels B.
Niels B.

Reputation: 6310

has_many / belongs_to validation

Consider the following code

class CreateFoosAndBars < ActiveRecord::Migration
  def change
    create_table :foos do |t|
      t.string :name
      t.timestamps
    end
    create_table :bars do |t|
      t.integer :foo_id
      t.string :name
      t.timestamps
    end
  end
end

class Foo < ActiveRecord::Base
  has_many :bars
end

class Bar < ActiveRecord::Base
  belongs_to :foo
  validates_presence_of :foo
end

So, foos can have any number of bars and bars cannot exist without a foo.

This code should save, but it doesn't:

irb(main):020:0> foo = Foo.new
=> #<Foo id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):021:0> foo.bars.build
=> #<Bar id: nil, foo_id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):022:0> foo.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
=> false
irb(main):023:0> foo.errors
=> #<ActiveModel::Errors:0x007fb325b35e68 @base=#<Foo id: nil, name: nil, created_at: nil, updated_at: nil>, @messages={:bars=>["er ikke gyldig"]}>
irb(main):024:0> 

I am using Rails 4.0.3 for this project.

I can't seem to grasp how to transactionally initialize, validate and save a foo with a number of bars. What am I missing here?

Upvotes: 0

Views: 425

Answers (4)

Niels B.
Niels B.

Reputation: 6310

I have discovered the solution.

If I do this

foo = Foo.new
bar = foo.bars.build
bar.foo
=> nil

So build is lying. It's not building a bar associated with foo as you would expect. build only works when the foo is persisted.

However, if I add inverse_of: :foo to my Foo models association, like this:

class Foo < ActiveRecord::Base
  has_many :bars, inverse_of: :foo
end

It suddenly oddly works. Calling foo on the built bar instance suddenly returns an object instead of nil.

irb(main):002:0> bar = foo.bars.build
=> #<Bar id: nil, foo_id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):003:0> bar.foo
=> #<Foo id: nil, name: nil, created_at: nil, updated_at: nil>
irb(main):005:0> bar.foo.object_id
=> 70208007135620
irb(main):006:0> foo.object_id
=> 70208007135620

I do not know why this is neccessary and not the default behavior.

Update

This issue only applies to Rails versions older than 4.1.0.

Upvotes: 0

hjing
hjing

Reputation: 4982

Your code should work, I copy pasted your example into a new rails app:

class CreateFoosAndBars < ActiveRecord::Migration
  def change
    create_table :foos do |t|
      t.string :name
      t.timestamps
    end
    create_table :bars do |t|
      t.integer :foo_id
      t.string :name
      t.timestamps
    end
  end
end

class Foo < ActiveRecord::Base
  has_many :bars
end

class Bar < ActiveRecord::Base
  belongs_to :foo
  validates_presence_of :foo
end

And these were the results:

2.1.1 :001 > Foo.count
   (0.1ms)  SELECT COUNT(*) FROM "foos"
 => 0
2.1.1 :002 > Bar.count
   (0.1ms)  SELECT COUNT(*) FROM "bars"
 => 0
2.1.1 :003 > foo = Foo.new
 => #<Foo id: nil, name: nil, created_at: nil, updated_at: nil>
2.1.1 :004 > foo.bars.build
 => #<Bar id: nil, foo_id: nil, name: nil, created_at: nil, updated_at: nil>
2.1.1 :005 > foo.save
   (0.3ms)  SAVEPOINT active_record_1
  SQL (0.5ms)  INSERT INTO "foos" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2014-06-21 16:24:15.334535"], ["updated_at", "2014-06-21 16:24:15.334535"]]
  SQL (0.2ms)  INSERT INTO "bars" ("created_at", "foo_id", "updated_at") VALUES (?, ?, ?)  [["created_at", "2014-06-21 16:24:15.342524"], ["foo_id", 1], ["updated_at", "2014-06-21 16:24:15.342524"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
 => true
2.1.1 :006 > foo.errors
 => #<ActiveModel::Errors:0x007ff37b9514a8 @base=#<Foo id: 1, name: nil, created_at: "2014-06-21 16:24:15", updated_at: "2014-06-21 16:24:15">, @messages={}>
2.1.1 :007 > Foo.count
   (0.2ms)  SELECT COUNT(*) FROM "foos"
 => 1
2.1.1 :008 > Bar.count
   (0.3ms)  SELECT COUNT(*) FROM "bars"
 => 1

This is using Rails 4.1.0.

In versions of Rails prior to 4.1.0, you would have to specify the following relationship:

class Foo
  has_many :bars, inverse_of: :foo
end

However Rails 4.1.0 now heuristically detects inverse associations as documented here.

Upvotes: 3

James Mason
James Mason

Reputation: 4306

Try adding the inverse_of option to both of your associations. Maybe Rails is getting confused somewhere and can't match them up.

Upvotes: 0

ShivamD
ShivamD

Reputation: 971

foo = Foo.build
bar = foo.bars.build
bar.save

The reason being you are creating bars without saving foo. Hence foo is invalid. You must save foo before that, so it can get an id.

Update:

You have to use inverse_of in your in your bar model.

belongs_to :foo, inverse_of :bar

Foo model

has_many :bards, inverse_of :foo

Reference: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Upvotes: 0

Related Questions