rails: Join table cannot update or destroy throw error of no primary key

Steps to reproduce

Have models as follow:

class Event < ApplicationRecord
  belongs_to :deal
  has_many :events_users
  has_many :users, through: :events_users
end

class Deal < ApplicationRecord
  belongs_to :team
  has_many :deals_users
  has_many :users, through: :deals_users
  has_many :events
end

class Team < ApplicationRecord
  has_many :deals
  has_many :team_users
  has_many :users, through: :team_users
end

class User < ApplicationRecord
  has_one :team, through: :team_user
  has_one :team_user
  has_and_belongs_to_many :deals
  has_and_belongs_to_many :events
end

class EventsUser < ApplicationRecord
  belongs_to :event
  belongs_to :user
  validates_uniqueness_of :event_id, scope: :user_id
  enum status: [:pending, :accepted, :maybe, :rejected]
end

class DealsUser < ApplicationRecord
  belongs_to :user
  belongs_to :deal
  validates_uniqueness_of :deal_id, scope: :user_id
end

class TeamUser < ApplicationRecord
  belongs_to :user
  belongs_to :team
  validates_uniqueness_of :team_id, scope: :user_id
  validates_uniqueness_of :user_id
end

Expected behavior

The join tables, EventsUser, DealsUser, TeamUser can be updated or deleted.

Actual behavior

It can only be created, the update will throw

ActiveRecord::UnknownPrimaryKey: Unknown primary key for table events_users in model EventsUser.
Can not validate uniqueness for persisted record without primary key.

and destroy will throw

NoMethodError: undefined method `to_sym' for nil:NilClass

These following are my testing procedures in rails c.

eu = EventsUser.first
eu.status = "maybe"
eu.save (throw update error)
eu.destroy (throw destroy error)
eu (it exists)
eu.status (return the status)

status is an enum in model, and defined in migration as such:

t.integer :status, default: 0

System configuration

Rails version: Rails 5.0.0.rc1 Ruby version: ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]

Reference

https://github.com/rails/rails/issues/21304

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 2
  • Comments: 26 (8 by maintainers)

Most upvoted comments

I’ve read the thread and I can’t wrap my head around the fact that create_join_table by default creates a join table with no primary_key and at the same time:

Per #25347 (comment):

Active Record doesn’t have built in support for composite primary keys

That means you can’t manipulate a model whose corresponding table doesn’t have a single-column primary key defined. That includes doing so through an association that uses said model.

How can both be true? Is using create_join_table something we should avoid? Or am I missing something here?

It works if you use dependent: :delete_all instead of :destroy

I’m in Rails 5 in a new project and encountering this same issue. I echo @GomaaK’s confusion. I don’t have a model for these join tables; there’s nowhere to even stick self.primary_key = if I wanted to. What is create_join_table good for if it can’t … be … used?

It works if I remove id: false. I can work around the problem by testing uniqueness in the model, but it leaves me with a useless id field in a join table.

Old thread but in case it helps… my understanding is that if you use create_join_table you should also be using has_and_belongs_to_many, not has_many through:. This creates a simple bare join table with no id, and you cannot work with those as objects directly.

The problem is still actual If we have models/Page.rb

has_many :items, dependent: destroy has_many :page_items, through: :items

Page.last.destroy => PG::Error: ERROR: zero-length delimited identifier at or near “”“”

After being tired of these errors I just put a primary key on all my join tables

Having this issue on Rails 6.0.

What are best practices here and are there plans to fix the AR behavior with join tables? Ideally join tables would not need a primary id for the dependent: :destroy option to be honored.

I’m switching to manually delete-ing dependencies – is this the best option?

Hi @batmanbury , Rails is looking for a primary key for deleting a record. Check this Rails take ID as default primary key and as it’s not present it gives a to_sym error for null class. For temporary basis, you can avoid this issue by mentioning primary_key in model as

self.primary_key = <column_name having uniq index>

or you need to add ID column in a table which will serve as a primary_key.

This solved my issue

Created a bug_report_template script for this issue; the behavior is not being reproduced by the script. Hopefully, it will help identify the difference that is causing the issue for @starkshaw and @ibussieres .

Script to reproduce the issue with join table update and delete

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
  raise e
end

gemfile(true) do
  source "https://rubygems.org"
  # Activate the gem you are reporting the issue against.
  gem "activerecord", "5.0.0"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# Ensure backward compatibility with Minitest 4
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :teams, force: true do |t|
    t.string :name
  end

  create_table :deals, force: true do |t|
    t.string :name
    t.integer :team_id
  end

  create_table :events, force: true do |t|
    t.string :name
    t.integer :deal_id
  end

  create_table :users, force: true do |t|
    t.string :first_name
    t.string :last_name
  end

  create_table :events_users, force: true do |t|
    t.integer :event_id
    t.integer :user_id
    t.integer :status, default: 0
  end

  create_table :deals_users, force: true do |t|
    t.integer :deal_id
    t.integer :user_id
    t.integer :status, default: 0
  end

  create_table :team_users, force: true do |t|
    t.integer :team_id
    t.integer :user_id
    t.integer :status, default: 0
  end
end

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

class Event < ApplicationRecord
  belongs_to :deal
  has_many :events_users
  has_many :users, through: :events_users
end

class Deal < ApplicationRecord
  belongs_to :team
  has_many :deals_users
  has_many :users, through: :deals_users
  has_many :events
end

class Team < ApplicationRecord
  has_many :deals
  has_many :team_users
  has_many :users, through: :team_users
end

class User < ApplicationRecord
  has_one :team, through: :team_user
  has_one :team_user
  has_and_belongs_to_many :deals
  has_and_belongs_to_many :events
end

class EventsUser < ApplicationRecord
  belongs_to :event
  belongs_to :user
  validates_uniqueness_of :event_id, scope: :user_id
  enum status: [:pending, :accepted, :maybe, :rejected]
end

class DealsUser < ApplicationRecord
  belongs_to :user
  belongs_to :deal
  validates_uniqueness_of :deal_id, scope: :user_id
end

class TeamUser < ApplicationRecord
  belongs_to :user
  belongs_to :team
  validates_uniqueness_of :team_id, scope: :user_id
  validates_uniqueness_of :user_id
end

User.create(first_name: "Almaz", last_name: "Ayana")
User.create(first_name: "Vivian", last_name: "Cheruiyot")

Team.create(name: "10000m")
Team.create(name: "100m")

Deal.create(name: "Half Off", team_id: 1)
Deal.create(name: "BOGO", team_id: 2)

Event.create(name: "Christmas", deal_id: 1)
Event.create(name: "Halloween", deal_id: 1)

EventsUser.create(user_id: 1, event_id: 1)
EventsUser.create(user_id: 2, event_id: 1)

TeamUser.create(user_id: 1, team_id: 1)
TeamUser.create(user_id: 2, team_id: 2)

DealsUser.create(user_id: 1, deal_id: 1)
DealsUser.create(user_id: 2, deal_id: 2)

class BugTest < Minitest::Test
  def test_join_table_update
    eu = EventsUser.first
    eu.status = "maybe"
    eu.save
    assert_equal "maybe", EventsUser.first.status
    eu.destroy
    assert_equal true, eu.destroyed?
  end
end