Bạn muốn cập nhật tên hiển thị theo email và chọn INSERT OR REPLACE cho gọn. Request chỉ gửi email cùng tên mới, nhưng ghi chú cũ trong database biến thành giá trị mặc định. REPLACE khi gặp xung đột khóa duy nhất có cơ chế khác một UPDATE chỉ sửa các cột được chỉ định.
Bài đối chiếu tài liệu SQLite ngày 06/11/2026 và chạy bằng Python 3.9.6, SQLite 3.54.0 trong bộ nhớ. Đây là fixture độc lập, chưa chạy trên database ứng dụng, ORM hoặc PostgreSQL. Cú pháp UPSERT dùng ở đây có trong SQLite từ 3.24.0; cần kiểm tra SQLite thật sự được đóng gói trên thiết bị hoặc server của bạn.
Xác định cột nào cần được giữ lại
Trong demo có email là khóa duy nhất, display_name là cột muốn đổi, note là thông tin phải giữ nếu request không đề cập. Khi xung đột UNIQUE hoặc PRIMARY KEY, REPLACE loại hàng gây xung đột trước khi tiếp tục ghi hàng mới. Cột không nằm trong INSERT sẽ theo quy tắc của lần chèn, không tự lấy giá trị từ hàng cũ.
Với UPSERT, ON CONFLICT chỉ rõ khóa gây xung đột và DO UPDATE chỉ rõ các cột cần sửa. Bạn có thể giữ note bằng cách không đưa note vào tập gán của UPDATE.
1. Tái hiện bằng hai database giống nhau
Chạy nguyên đoạn Python sau. Mỗi cách bắt đầu từ một database mới, nên phép so sánh không chịu ảnh hưởng lần ghi trước:
import sqlite3
def make_db():
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE demo_profiles (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
note TEXT NOT NULL DEFAULT 'new row'
);
INSERT INTO demo_profiles VALUES
(10, '[email protected]', 'Old name', 'keep this note');
""")
return db
replace_db = make_db()
replace_db.execute("""
INSERT OR REPLACE INTO demo_profiles(email, display_name)
VALUES (?, ?)
""", ("[email protected]", "New name"))
replaced = replace_db.execute("""
SELECT display_name, note FROM demo_profiles
""").fetchone()
assert replaced == ("New name", "new row")
upsert_db = make_db()
upsert_db.execute("""
INSERT INTO demo_profiles(email, display_name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE
SET display_name = excluded.display_name
""", ("[email protected]", "New name"))
updated = upsert_db.execute("""
SELECT id, display_name, note FROM demo_profiles
""").fetchone()
assert updated == (10, "New name", "keep this note")
print("REPLACE:", replaced)
print("UPSERT:", updated)
Hai tên hiển thị đều được đổi, nhưng note khác nhau. Fixture chủ động kiểm tra cả id ở nhánh UPSERT để xác nhận hàng cũ được cập nhật trong ca này. Không dựa vào một giá trị id mới cụ thể ở nhánh REPLACE để suy luận quy tắc cấp khóa cho mọi schema.
excluded.display_name tham chiếu giá trị mà INSERT định ghi. Nó không phải alias của hàng cũ. Khi thêm nhiều cột vào UPDATE, hãy xác định rõ cột lấy từ request và cột lấy từ giá trị đang lưu.
2. Nhánh không xung đột vẫn là INSERT
Nối tiếp file để kiểm tra email chưa có. Các database chỉ tồn tại trong bộ nhớ:
upsert_db.execute("""
INSERT INTO demo_profiles(email, display_name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE
SET display_name = excluded.display_name
""", ("[email protected]", "Another dev"))
new_note = upsert_db.execute("""
SELECT note FROM demo_profiles WHERE email = ?
""", ("[email protected]",)).fetchone()
assert new_note == ("new row",)
replace_db.close()
upsert_db.close()
Giữ note khi UPDATE không có nghĩa mọi hàng mới đều có note cũ. Ở nhánh INSERT, note vẫn cần giá trị request, default hoặc một quy tắc nghiệp vụ khác đã xác định.
Kiểm tra trước khi đổi câu lệnh trong ứng dụng
Liệt kê các cột request có thể bỏ qua và định nghĩa “bỏ qua” khác null như thế nào. Thêm test bắt đầu từ hàng có đủ dữ liệu, chỉ cập nhật một trường rồi đọc lại tất cả trường cần bảo toàn. Test chỉ kiểm tra display_name sẽ bỏ sót lỗi note trong ví dụ.
Đọc conflict target và các ràng buộc còn lại của bảng. UPSERT xử lý xung đột duy nhất được mô tả, không biến mọi lỗi NOT NULL, CHECK hoặc khóa ngoại thành UPDATE. Nếu schema có nhiều UNIQUE, thêm fixture cho từng đường xung đột thay vì mặc định ON CONFLICT(email) bao phủ tất cả.
Giới hạn cần xem riêng
Ví dụ chưa thêm trigger, bảng con hoặc kiểm tra hiệu ứng khóa ngoại của thao tác thay hàng. Đó là lý do cần rà schema thật trước khi dùng REPLACE cho dữ liệu liên kết. Bài không khẳng định kết quả số lượng hook hay trigger vì không thử chúng trong fixture.
Khi request chỉ muốn sửa một phần hàng, UPDATE hoặc một UPSERT có tập gán rõ ràng thường diễn đạt yêu cầu dễ kiểm tra hơn. Chọn câu lệnh theo dữ liệu phải giữ, rồi xác nhận cả nhánh tạo mới và nhánh cập nhật bằng test đọc lại.

