составной первичный ключ не обновляется после сохранения


Вот минимальный тестовый случай, который лежит в основе моего вопроса. Почему, несмотря на то, что user Правильно сохранен, атрибут user.id не обновляется? Попытка повторно найти запись в базе данных извлекает ее без проблем, и атрибут id установлен правильно.

AFAICT, это не вопрос попытки автоматического увеличения составного первичного ключа в sqlite. Та же проблема возникает и с комбинацией uuid/PostgreSQL. Схема имеет только id в качестве первичного ключа с [ :account_id, :id ] будучи отдельным, уникальным индексом.

#!/usr/bin/env ruby
gem "rails", "~> 5.0.2"
gem "composite_primary_keys"

require "active_record"
require "composite_primary_keys"

ActiveRecord::Base.establish_connection(
  adapter: "sqlite3",
  database: ":memory:"
)

ActiveRecord::Schema.define do
  create_table :accounts, force: true do |t|
  end

  create_table :users, force: true do |t|
    t.references :account
    t.index [ :account_id, :id ], unique: true
  end
end

class User < ActiveRecord::Base
  self.primary_keys = [ :account_id, :id ]
  belongs_to :account, inverse_of: :users
end

class Account < ActiveRecord::Base
  has_many :users, inverse_of: :account
end

account = Account.create!
puts "created account: #{account.inspect}"
user = account.users.build
puts "before user.save: #{user.inspect}"
user.save
puts "after user.save: #{user.inspect}"
puts "account.users.first: #{account.users.first.inspect}"

И результат выполнения этого скрипта:

~/src
frankjmattia@lappy-i686(ttys005)[4146] % ./cpk-test.rb
-- create_table(:accounts, {:force=>true})
   -> 0.0036s
-- create_table(:users, {:force=>true})
   -> 0.0009s
created account: #<Account id: 1>
before user.save: #<User id: nil, account_id: 1>
after user.save: #<User id: nil, account_id: 1>
account.users.first: #<User id: 1, account_id: 1>

Не должен user.id быть [1,1] после первого сохранения? Если это ошибка, кому я должен сообщить об этом?

2 3

2 ответа:

SQLite не поддерживает автоматическое приращение для составного первичного ключа. Вы можете найти соответствующие вопросы в SO: 1, 2.

Вот ответ @SingleNegationElimination из второй ссылки:

В sqlite вы получаете поведение автоинкремента только тогда, когда только одно целое число столбец-это первичный ключ. составные ключи предотвращают автоинкремент от вступает в силу.

Вы можете получить аналогичный результат, определив id в качестве единственного первичного ключа, но затем добавляя дополнительный уникальное ограничение на id, col3.

И composite_primary_keys сохраняют эту логику.

Также здесь существует трюк, чтобы сделать это: sqlite: многоколоночный первичный ключ с автоинкрементным столбцом

Как оказалось, ответ был прост. Rails обычно получает возвращенный первичный ключ из create и обновляет модель с его помощью. Составной ключ не перезагружается сам по себе, поэтому я должен это сделать. В основном используется логика из reload в крюке after_create для извлечения созданной записи и обновления атрибутов соответственно.

#!/usr/bin/env ruby
gem "rails", "5.0.2"
gem "composite_primary_keys", "9.0.6"

require "active_record"
require "composite_primary_keys"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
  create_table :accounts, force: true    
  create_table :users, force: true do |t|
    t.integer :account_id, null: false
    t.string :email, null: false
    t.index [ :account_id, :id ], unique: true
    t.index [ :account_id, :email ], unique: true
  end
end

class User < ActiveRecord::Base
  self.primary_keys = [ :account_id, :id ]
  belongs_to :account, inverse_of: :users

  after_create do
    self.class.connection.clear_query_cache
    fresh_person = self.class.unscoped {
      self.class.find_by!(account: account, email: email)
    }
    @attributes = fresh_person.instance_variable_get('@attributes')
    @new_record = false
    self
  end
end

class Account < ActiveRecord::Base
  has_many :users, inverse_of: :account
end

account = Account.create!
user = account.users.build(email: "#{SecureRandom.hex(4)}@example.com")
puts "before save user: #{user.inspect}"
user.save
puts "after save user: #{user.inspect}"

А теперь:

frankjmattia@lappy-i686(ttys003)[4108] % ./cpk-test.rb
-- create_table(:accounts, {:force=>true})
   -> 0.0045s
-- create_table(:users, {:force=>true})
   -> 0.0009s
before save user: #<User id: nil, account_id: 1, email: "a54c2385@example.com">
after save user: #<User id: 1, account_id: 1, email: "a54c2385@example.com">