UPSERT / UPDATE ATAU INSERT SQLite

103

Saya perlu melakukan UPSERT / INSERT ATAU UPDATE terhadap Database SQLite.

Ada perintah INSERT OR REPLACE yang dalam banyak kasus dapat berguna. Tetapi jika Anda ingin mempertahankan id Anda dengan autoincrement di tempatnya karena kunci asing, itu tidak berfungsi karena menghapus baris, membuat yang baru dan akibatnya baris baru ini memiliki ID baru.

Ini akan menjadi tabelnya:

pemain - (kunci utama pada id, nama_pengguna unik)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |
bgusach.dll
sumber

Jawaban:

52

Ini adalah jawaban yang terlambat. Mulai dari SQLIte 3.24.0, dirilis pada 4 Juni 2018, akhirnya ada dukungan untuk klausa UPSERT setelah sintaks PostgreSQL.

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

Catatan: Bagi mereka yang harus menggunakan versi SQLite sebelum 3.24.0, harap referensi jawaban ini di bawah ini (diposting oleh saya, @MarqueIV).

Namun jika Anda memiliki opsi untuk meningkatkan, Anda sangat disarankan untuk melakukannya karena tidak seperti solusi saya, solusi yang diposting di sini mencapai perilaku yang diinginkan dalam satu pernyataan. Selain itu, Anda mendapatkan semua fitur, peningkatan, dan perbaikan bug yang biasanya datang dengan rilis yang lebih baru.

prapin
sumber
Untuk saat ini, belum ada rilis ini di repositori Ubuntu.
bl79
Mengapa saya tidak bisa menggunakan ini di android? Saya sudah mencoba db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Memberi saya kesalahan sintaks pada kata "on"
Bastian Voigt
1
@BastianVoigt Karena pustaka SQLite3 yang diinstal pada berbagai versi Android lebih lama dari 3.24.0. Lihat: developer.android.com/reference/android/database/sqlite/… Sayangnya, jika Anda memerlukan fitur baru SQLite3 (atau pustaka sistem lainnya) di Android atau iOS, Anda perlu menggabungkan versi SQLite tertentu di aplikasi daripada mengandalkan sistem yang diinstal.
prapin
Daripada UPSERT, bukankah ini lebih merupakan INDATE karena mencoba memasukkan terlebih dahulu? ;)
Mark A. Donohoe
@BastianVoigt, silakan lihat jawaban saya di bawah ini (ditautkan dalam pertanyaan di atas) yang untuk versi sebelum 3.24.0.
Mark A. Donohoe
106

Gaya Tanya Jawab

Nah, setelah meneliti dan berjuang dengan masalah selama berjam-jam, saya menemukan bahwa ada dua cara untuk melakukannya, tergantung pada struktur tabel Anda dan jika Anda mengaktifkan pembatasan kunci asing untuk menjaga integritas. Saya ingin membagikan ini dalam format yang bersih untuk menghemat waktu kepada orang-orang yang mungkin berada dalam situasi saya.


Opsi 1: Anda mampu menghapus baris tersebut

Dengan kata lain, Anda tidak memiliki kunci asing, atau jika Anda memilikinya, mesin SQLite Anda dikonfigurasi sehingga tidak ada pengecualian integritas. Cara untuk pergi adalah INSERT OR REPLACE . Jika Anda mencoba memasukkan / memperbarui pemain yang ID-nya sudah ada, mesin SQLite akan menghapus baris itu dan memasukkan data yang Anda berikan. Sekarang pertanyaannya muncul: apa yang harus dilakukan agar ID lama tetap terkait?

Katakanlah kita ingin UPSERT dengan data user_name = 'steven' dan age = 32.

Lihat kode ini:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

Triknya sedang menyatu. Ia mengembalikan id pengguna 'steven' jika ada, dan sebaliknya, ia mengembalikan id baru yang baru.


Opsi 2: Anda tidak mampu menghapus baris tersebut

Setelah berkutat dengan solusi sebelumnya, saya menyadari bahwa dalam kasus saya itu bisa menghancurkan data, karena ID ini berfungsi sebagai kunci asing untuk tabel lain. Selain itu, saya membuat tabel dengan klausul ON DELETE CASCADE , yang berarti akan menghapus data secara diam-diam. Berbahaya.

Jadi, saya pertama kali memikirkan klausa IF, tetapi SQLite hanya memiliki CASE . Dan KASUS ini tidak dapat digunakan (atau setidaknya saya tidak mengelolanya) untuk melakukan satu kueri UPDATE jika SUDAH (pilih id dari pemain di mana user_name = 'steven'), dan MASUKKAN jika tidak. Tidak pergi.

Dan kemudian, akhirnya saya menggunakan kekerasan, dengan sukses. Logikanya adalah, untuk setiap UPSERT yang ingin Anda lakukan, pertama-tama lakukan INSERT ATAU IGNORE untuk memastikan ada baris dengan pengguna kami, lalu jalankan kueri UPDATE dengan data yang sama persis dengan yang Anda coba masukkan.

Data yang sama seperti sebelumnya: user_name = 'steven' dan age = 32.

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

Dan itu saja!

EDIT

Seperti yang telah dikomentari Andy, mencoba memasukkan terlebih dahulu dan kemudian memperbarui dapat menyebabkan pemicu pengaktifan lebih sering dari yang diharapkan. Menurut saya, ini bukan masalah keamanan data, tetapi memang benar bahwa mengaktifkan peristiwa yang tidak perlu tidak masuk akal. Oleh karena itu, solusi yang lebih baik adalah:

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 
bgusach.dll
sumber
10
Ditto ... opsi 2 bagus. Kecuali, saya melakukannya dengan cara lain: coba pembaruan, periksa apakah rowAffected> 0, jika tidak, lakukan penyisipan.
Tom Spencer
Itu juga pendekatan yang cukup bagus, satu-satunya kelemahan kecil adalah Anda tidak hanya memiliki satu SQL untuk "upsert".
bgusach
2
Anda tidak perlu menyetel ulang nama_user dalam pernyataan pembaruan di contoh kode terakhir. Itu cukup untuk mengatur usia.
Serg Stetsuk
72

Berikut adalah pendekatan yang tidak memerlukan 'abaikan' kekerasan yang hanya akan berfungsi jika ada pelanggaran kunci. Cara ini bekerja berdasarkan kondisi apa pun yang Anda tentukan di pembaruan.

Coba ini...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

Bagaimana itu bekerja

'Saus ajaib' di sini digunakan Changes()dalam Whereklausa. Changes()mewakili jumlah baris yang dipengaruhi oleh operasi terakhir, yang dalam hal ini adalah pembaruan.

Dalam contoh di atas, jika tidak ada perubahan dari pembaruan (misalnya catatan tidak ada) maka Changes()= 0 sehingga Whereklausa dalam Insertpernyataan mengevaluasi menjadi benar dan baris baru dimasukkan dengan data yang ditentukan.

Jika Update memang memperbarui baris yang ada, maka Changes()= 1 (atau lebih akurat, bukan nol jika lebih dari satu baris diperbarui), sehingga klausa 'Di mana' dalam Insertsekarang mengevaluasi ke salah dan dengan demikian tidak ada penyisipan yang akan dilakukan.

Keindahan dari hal ini adalah tidak diperlukan kekerasan fisik, atau penghapusan yang tidak perlu, lalu memasukkan kembali data yang dapat mengakibatkan mengacaukan kunci hilir dalam hubungan kunci asing.

Selain itu, karena ini hanya Whereklausa standar , dapat didasarkan pada apa pun yang Anda tentukan, bukan hanya pelanggaran kunci. Demikian juga, Anda dapat menggunakan Changes()kombinasi dengan apa pun yang Anda inginkan / butuhkan di mana pun ekspresi diizinkan.

Mark A. Donohoe
sumber
1
Ini bekerja dengan baik untuk saya. Saya belum pernah melihat solusi ini di tempat lain di samping semua contoh INSERT OR REPLACE, ini jauh lebih fleksibel untuk kasus penggunaan saya.
csab
@MarqueIV dan bagaimana jika ada dua item yang harus diperbarui atau disisipkan? misalnya, cemara telah diperbarui, dan yang kedua tidak ada. dalam kasus seperti itu Changes() = 0akan mengembalikan false dan dua baris akan melakukan INSERT OR REPLACE
Andriy Antonov
Biasanya UPSERT seharusnya bertindak pada satu catatan. Jika Anda mengatakan Anda tahu pasti itu bekerja pada lebih dari satu rekaman, maka ubah pemeriksaan hitungan yang sesuai.
Mark A. Donohoe
Hal buruknya adalah jika baris tersebut ada, metode pembaruan harus dijalankan terlepas dari apakah baris tersebut telah berubah atau tidak.
Jimi
1
Mengapa itu hal yang buruk? Dan jika datanya tidak berubah, mengapa Anda menelepon UPSERT? Namun demikian, ada baiknya pembaruan terjadi, pengaturan Changes=1atau INSERTpernyataan tersebut akan salah aktif, yang tidak Anda inginkan.
Mark A. Donohoe
25

Masalah dengan semua jawaban yang disajikan itu sama sekali tidak memperhitungkan pemicu (dan mungkin efek samping lainnya). Solusi seperti

INSERT OR IGNORE ...
UPDATE ...

mengarah ke kedua pemicu dieksekusi (untuk menyisipkan dan kemudian untuk pembaruan) ketika baris tidak ada.

Solusi yang tepat adalah

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

dalam hal ini hanya satu pernyataan yang dieksekusi (ketika ada baris atau tidak).

Andy
sumber
1
Saya mengerti maksud Anda. Saya akan memperbarui pertanyaan saya. Omong-omong, saya tidak mengapa UPDATE OR IGNOREperlu, karena pembaruan tidak akan macet jika tidak ada baris yang ditemukan.
bgusach
1
keterbacaan? Saya bisa melihat apa yang dilakukan kode Andy dalam sekejap. Masalahmu, aku harus belajar sebentar untuk mencari tahu.
Brandan
6

Untuk memiliki UPSERT murni tanpa lubang (untuk pemrogram) yang tidak menyampaikan kunci unik dan lainnya:

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

SELECT perubahan () akan mengembalikan jumlah pembaruan yang dilakukan dalam penyelidikan terakhir. Kemudian periksa apakah nilai kembalian dari perubahan () adalah 0, jika demikian jalankan:

INSERT INTO players (user_name, age) VALUES ('gil', 32); 
Gilco
sumber
Ini setara dengan apa yang @fiznool usulkan dalam komentarnya (meskipun saya akan mencari solusinya). Tidak apa-apa dan sebenarnya berfungsi dengan baik, tetapi Anda tidak memiliki pernyataan SQL yang unik. UPSERT tidak berdasarkan PK atau kunci unik lainnya yang tidak masuk akal bagi saya.
bgusach
4

Anda juga dapat menambahkan klausa ON CONFLICT REPLACE ke batasan unik user_name Anda dan kemudian cukup SISIPKAN, serahkan pada SQLite untuk mencari tahu apa yang harus dilakukan jika terjadi konflik. Lihat: https://sqlite.org/lang_conflict.html .

Perhatikan juga kalimat terkait pemicu hapus: Saat strategi penyelesaian konflik REPLACE menghapus baris untuk memenuhi batasan, hapus pemicu yang diaktifkan jika dan hanya jika pemicu rekursif diaktifkan.

Maximilian Tyrtania
sumber
1

Opsi 1: Sisipkan -> Perbarui

Jika Anda ingin menghindari keduanya changes()=0dan INSERT OR IGNOREbahkan jika Anda tidak mampu menghapus baris - Anda dapat menggunakan logika ini;

Pertama, masukkan (jika tidak ada) lalu perbarui dengan memfilter dengan kunci unik.

Contoh

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

Mengenai Pemicu

Perhatian: Saya belum mengujinya untuk melihat pemicu mana yang dipanggil, tetapi saya menganggap yang berikut:

jika baris tidak ada

  • SEBELUM SISIPKAN
  • SISIPKAN menggunakan INSTEAD OF
  • SETELAH SISIPKAN
  • SEBELUM UPDATE
  • PERBARUI menggunakan INSTEAD OF
  • SETELAH UPDATE

jika baris memang ada

  • SEBELUM UPDATE
  • PERBARUI menggunakan INSTEAD OF
  • SETELAH UPDATE

Opsi 2: Sisipkan atau ganti - simpan ID Anda sendiri

dengan cara ini Anda dapat memiliki satu perintah SQL

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

Edit: opsi tambahan 2.

itsho
sumber