Hindari pelanggaran unik dalam transaksi atom


15

Apakah mungkin membuat transaksi atom dalam PostgreSQL?

Pertimbangkan saya memiliki kategori tabel dengan baris ini:

id|name
--|---------
1 |'tablets'
2 |'phones'

Dan nama kolom memiliki kendala unik.

Jika saya mencoba:

BEGIN;
update "category" set name = 'phones' where id = 1;
update "category" set name = 'tablets' where id = 2;
COMMIT;

Saya mendapatkan:

ERROR:  duplicate key value violates unique constraint "category_name_key"
DETAIL:  Key (name)=(tablets) already exists.

Jawaban:


24

Selain apa yang disediakan @Craig (dan koreksi sebagian):

Efektif Postgres 9.4 , UNIQUE, PRIMARY KEYdan EXCLUDEkendala diperiksa segera setelah setiap baris saat didefinisikan NOT DEFERRABLE. Ini berbeda dari jenis NOT DEFERRABLEkendala lainnya (saat ini hanya REFERENCES(kunci asing)) yang diperiksa setelah setiap pernyataan . Kami mengerjakan semua ini berdasarkan pertanyaan terkait pada SO:

Hal ini tidak cukup untuk UNIQUE(atau PRIMARY KEYatau EXCLUDE) kendala untuk DEFERRABLEmembuat kode disajikan dengan beberapa pernyataan kerja.

Dan Anda tidak dapat menggunakan ALTER TABLE ... ALTER CONSTRAINTuntuk tujuan ini. Per dokumentasi:

ALTER CONSTRAINT

Formulir ini mengubah atribut dari kendala yang sebelumnya dibuat. Saat ini hanya batasan kunci asing yang dapat diubah .

Penekanan berani saya. Gunakan sebaliknya:

ALTER TABLE t
   DROP CONSTRAINT category_name_key
 , ADD  CONSTRAINT category_name_key UNIQUE(name) DEFERRABLE;

Lepaskan dan tambahkan kendala kembali dalam satu pernyataan sehingga tidak ada jendela waktu bagi siapa pun untuk menyelinap di baris yang menyinggung. Untuk tabel besar akan tergoda untuk melestarikan entah bagaimana indeks unik yang mendasarinya, karena itu mahal untuk menghapus dan membuatnya kembali. Sayangnya, itu sepertinya tidak mungkin dilakukan dengan alat standar (jika Anda memiliki solusi untuk itu, beri tahu kami!):

Untuk satu pernyataan membuat batasan ditangguhkan sudah cukup:

UPDATE category c
SET    name = c_old.name
FROM   category c_old
WHERE  c.id     IN (1,2)
AND    c_old.id IN (1,2)
AND    c.id <> c_old.id;

Permintaan dengan CTE juga adalah pernyataan tunggal :

WITH x AS (
    UPDATE category SET name = 'phones' WHERE id = 1
    )
UPDATE category SET name = 'tablets' WHERE id = 2;

Namun , untuk kode Anda dengan beberapa pernyataan, Anda (tambahan) harus benar-benar menunda kendala - atau mendefinisikannya sebagai salah INITIALLY DEFERREDsatu yang biasanya lebih mahal daripada yang di atas. Tetapi mungkin tidak mudah untuk mengemas semuanya menjadi satu pernyataan.

BEGIN;
SET CONSTRAINTS category_name_key DEFERRED;
UPDATE category SET name = 'phones'  WHERE id = 1;
UPDATE category SET name = 'tablets' WHERE id = 2;
COMMIT;

Waspadai batasan sehubungan dengan FOREIGN KEYkendala. Per dokumentasi:

Kolom yang direferensikan harus berupa kolom dari batasan kunci primer atau unik yang tidak dapat ditangguhkan dalam tabel yang direferensikan.

Jadi, Anda tidak dapat memiliki keduanya sekaligus.


13

Seperti yang saya pahami, masalah Anda di sini adalah bahwa kendala diperiksa setelah setiap pernyataan, tetapi Anda ingin memeriksa pada akhir transaksi, sehingga membandingkan kondisi sebelum ke kondisi setelah, mengabaikan kondisi perantara.

Jika demikian, itu mungkin dengan kendala yang ditangguhkan .

Lihat SET CONSTRAINTSdan DEFERRABLEkendala sebagaimana didokumentasikan dalam CREATE TABLE.

Perhatikan bahwa kendala yang ditangguhkan memiliki biaya - sistem harus menyimpan daftar mereka untuk diperiksa pada waktu yang ditentukan, jadi mereka tidak baik untuk transaksi yang membuat set perubahan besar. Mereka juga lebih lambat untuk memeriksa.

Jadi saya pikir Anda mungkin ingin:

ALTER TABLE mytable ALTER CONSTRAINT category_name_key DEFERRABLE;

Perhatikan bahwa tampaknya ada batasan pada ALTER TABLEpengaturan batasan DEFERRABLE; Anda mungkin harus sebaliknya DROPdan kembali ADDkendala.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.