Я столкнулся с дилеммой.
Я создал модель со столбцом wave_order
с индексом базы данных уникальности с wavable
. wavable
является полиморфным (но на самом деле нас это не волнует).
# migration
create_table :invitation_waves do |t|
t.string :name, null: false
t.references :wavable, polymorphic: true, index: true
t.integer :wave_order, null: false
t.timestamps
end
add_index :invitation_waves, [:wavable_id, :wavable_type, :wave_order], unique: true
# This first time, it's working
InvitationWave.upsert_all([{id: 1, name: "Wave1", wave_order: 0, wavable_id: my_object.id, wavable_class: my_object.class.name}, {id: 2, name: "Wave2", wave_order: 1, wavable_id: my_object.id, wavable_class: my_object.class.name}], unique_by: :id, update_only: :wave_order, record_timestamps: true)
# The second time, it's not because of the db constraint.
InvitationWave.upsert_all([{id: 1, name: "Wave1", wave_order: 1, wavable_id: my_object.id, wavable_class: my_object.class.name}, {id: 2, name: "Wave2", wave_order: 2, wavable_id: my_object.id, wavable_class: my_object.class.name}], unique_by: :id, update_only: :wave_order, record_timestamps: true)
# => ActiveRecord::RecordNotUnique:
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "idx_on_wavable_id_wavable_type_wave_order_39aa14a122"
DETAIL: Key (wavable_id, wavable_type, wave_order)=(1, MyObject, 1) already exists.
Есть ли другое решение, кроме этих 3:
А как насчет добавления DEFERRABLE INITIALLY IMMEDIATE
для ограничения уникальности?
В этом случае PostgreSQL проверяет уникальность перед фиксацией.
def up
create_table :invitation_waves do |t|
t.string :name, null: false
t.references :wavable, polymorphic: true, index: true
t.integer :wave_order, null: false
t.timestamps
end
execute <<~SQL.squish
ALTER TABLE invitation_waves
ADD CONSTRAINT idx_on_wavable_id_wavable_type_wave_order
UNIQUE (wavable_id, wavable_type, wave_order)
DEFERRABLE INITIALLY IMMEDIATE
SQL
end
def down
drop_table :invitation_waves
end
В новых версиях Rails вы можете применить метод миграции unique_constraint
create_table :invitation_waves do |t|
t.string :name, null: false
t.references :wavable, polymorphic: true, index: true t.integer :wave_order, null: false
t.timestamps
t.unique_constraint %i[wavable_id wavable_type wave_order], defferable: :immediate
end
add_unique_constraint :invitation_waves, %i[wavable_id wavable_type wave_order], defferable: :immediate
Независимо от того, как вы добавите это ограничение, оно будет работать таким образом.
# First query (populate records)
InvitationWave.upsert_all(
[
{id: 1, name: "Wave1", wave_order: 0, wavable_id: 1, wavable_type: "MyObject"},
{id: 2, name: "Wave2", wave_order: 1, wavable_id: 1, wavable_type: "MyObject"}
],
unique_by: :id, update_only: :wave_order, record_timestamps: true
)
# Second query (update records) that failed without DEFERRABLE, but now is ok
InvitationWave.upsert_all(
[
{id: 1, name: "Wave1", wave_order: 1, wavable_id: 1, wavable_type: "MyObject"},
{id: 2, name: "Wave2", wave_order: 2, wavable_id: 1, wavable_type: "MyObject"}
],
unique_by: :id, update_only: :wave_order, record_timestamps: true
)
Оба запроса будут успешными, поскольку ограничение будет проверено в конце оператора (непосредственно перед фиксацией).
Но когда вы попытаетесь выполнить третий запрос,
# Will fail, because there is other record with same wave_order
InvitationWave.find(2).update!(wave_order: 1)
он потерпит неудачу из-за ограничения UNIQUE
Просто помните, что вам нужно изменить формат дампа схемы с Ruby на SQL, иначе этот индекс просто потеряется при переводе.
Лучший ответ на свете. Вы решаете мою проблему с помощью
deferrable: :immediate
. Большое спасибо!