Attila O.
Attila O.

Reputation: 16615

How to create an ActiveRecord object with a many-to-many relationship

I have created a blank rails app (rails new cheese_shop), with two models and a join table. I am trying to create a cheese shop and specifying which cheeses it contains, at creation time, like this:

cheeses = [
  Cheese.create!(name: "Bree"),
  Cheese.create!(name: "Kačkavalj"),
]
Shop.create! name: "The Whistling Cheese", cheeses: cheeses

However, I'm getting this error:

SQLite3::ConstraintException: NOT NULL constraint failed: stocks.shop_id: INSERT INTO "stocks" ("cheese_id", "created_at", "updated_at") VALUES (?, ?, ?)

Apparently, the shop ID is not inserted to the stocks table when I create the shop. Is it possible to fix this, without having to do it in two steps (i.e. without first creating the Shop, and then adding the cheeses?)

Here are my models:

class Cheese < ActiveRecord::Base
  has_many :shops, through: :stocks
  has_many :stocks
end

class Shop < ActiveRecord::Base
  has_many :cheeses, through: :stocks
  has_many :stocks
end

class Stock < ActiveRecord::Base
  belongs_to :shop
  belongs_to :cheese
end

My migrations look like this:

class CreateTables < ActiveRecord::Migration
  def change
    create_table :cheeses do |t|
      t.string :name, null: false

      t.timestamps null: false
    end

    create_table :shops do |t|
      t.string :name, null: false

      t.timestamps null: false
    end

    create_table :stocks do |t|
      t.integer :shop_id,   null: false
      t.integer :cheese_id, null: false

      t.integer :amount
      t.float :price
    end
  end
end

Upvotes: 1

Views: 120

Answers (2)

Attila O.
Attila O.

Reputation: 16615

It turns out Rails creates the associations in two steps, first leaving out the Shop ID, then setting the Shop IDs with an UPDATE, all in one transaction. So The NOT NULL constraints are causing the problem.

Changing this:

  t.integer :shop_id,   null: false
  t.integer :cheese_id, null: false

…to this:

  t.integer :shop_id
  t.integer :cheese_id, null: false

…solves the problem, although I'm unhappy with this since now I cannot rely on the database to ensure the integrity of my data.

Upvotes: 0

Marko Krstic
Marko Krstic

Reputation: 1447

maybe you should try to use nested attributes:

class Shop < ActiveRecord::Base
    has_many :cheeses, through: :stocks
    has_many :stocks

    accepts_nested_attributes_for :stocks
end

and then you will be able to do something like:

cheese = Cheese.create!(name: "Bree")
params = { attrib: { name: "The Whistling Cheese", stocks_attributes: { cheese_id: cheese.id} } }
Shop.create params[:attrib]

here is doc: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Upvotes: 1

Related Questions