REPLACE Command trong SQLite - Cơ chế Upsert
Lệnh REPLACE (hoặc INSERT OR REPLACE) trong SQLite thực hiện: nếu vi phạm ràng buộc UNIQUE hoặc PRIMARY KEY → xóa hàng cũ → chèn hàng mới. Đây là cách "Insert or Update" nguyên tử (atomic).
Bước 1: Tạo dự án
Android Studio → File → New Project.
Bước 2: Giao diện (activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:padding="16dp">
<EditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Nhập tên" />
<EditText android:id="@+id/salary" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Nhập lương" android:inputType="numberDecimal" />
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="8dp">
<Button android:id="@+id/save" android:text="Lưu (REPLACE)" android:layout_width="wrap_content" android:layout_height="wrap_content" />
<Button android:id="@+id/refresh" android:text="Làm mới" android:layout_width="wrap_content" android:layout_height="wrap_content" />
</LinearLayout>
<ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" />
</LinearLayout>
Bước 3: MainActivity.java
package com.example.sqlitereplacecommand;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
Button btnSave, btnRefresh;
EditText edtName, edtSalary;
ListView listView;
DatabaseHelper dbHelper;
ArrayList dataList;
ArrayAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dbHelper = new DatabaseHelper(this);
dataList = new ArrayList<>(dbHelper.getAllContacts());
edtName = findViewById(R.id.name);
edtSalary = findViewById(R.id.salary);
listView = findViewById(R.id.listView);
btnSave = findViewById(R.id.save);
btnRefresh = findViewById(R.id.refresh);
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, dataList);
listView.setAdapter(adapter);
btnRefresh.setOnClickListener(v -> refreshList());
btnSave.setOnClickListener(v -> {
String name = edtName.getText().toString().trim();
String salary = edtSalary.getText().toString().trim();
if (name.isEmpty() || salary.isEmpty()) {
showError(name, salary);
return;
}
// REPLACE: Nếu name đã tồn tại (UNIQUE) -> xóa cũ, chèn mới
// Nếu chưa tồn tại -> chèn mới
if (dbHelper.insertOrReplace(name, salary)) {
Toast.makeText(this, "Đã lưu (Replace)", Toast.LENGTH_SHORT).show();
refreshList();
} else {
Toast.makeText(this, "Lưu thất bại", Toast.LENGTH_SHORT).show();
}
});
}
private void refreshList() {
dataList.clear();
dataList.addAll(dbHelper.getAllContacts());
adapter.notifyDataSetChanged();
}
private void showError(String name, String salary) {
if (name.isEmpty()) edtName.setError("Nhập tên");
if (salary.isEmpty()) edtSalary.setError("Nhập lương");
}
}
Bước 4: DatabaseHelper.java
package com.example.sqlitereplacecommand;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "salaryDatabase";
private static final String TABLE_NAME = "SalaryDetails";
private static final int DB_VERSION = 1;
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// QUAN TRỌNG: Cột name có UNIQUE constraint
// REPLACE dựa trên vi phạm UNIQUE hoặc PRIMARY KEY
db.execSQL("CREATE TABLE " + TABLE_NAME +
"(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name TEXT UNIQUE, " +
"salary TEXT)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldV, int newV) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
// db.replace() = INSERT OR REPLACE
public boolean insertOrReplace(String name, String salary) {
SQLiteDatabase db = getWritableDatabase();
ContentValues cv = new ContentValues();
cv.put("name", name);
cv.put("salary", salary);
long rowId = db.replace(TABLE_NAME, null, cv);
return rowId != -1;
}
public ArrayList getAllContacts() {
ArrayList list = new ArrayList<>();
SQLiteDatabase db = getReadableDatabase();
// Lọc: salary > '10' HOẶC name bắt đầu bằng 'Sa'
Cursor cursor = db.rawQuery(
"SELECT name FROM " + TABLE_NAME +
" WHERE salary > '10' OR name LIKE 'Sa%'", null);
if (cursor.moveToFirst()) {
do {
list.add(cursor.getString(cursor.getColumnIndex("name")));
} while (cursor.moveToNext());
}
cursor.close();
return list;
}
public boolean deleteAll() {
return getWritableDatabase().delete(TABLE_NAME, null, null) > 0;
}
}
Cơ chế hoạt động của REPLACE
-- Bảng: CREATE TABLE t (id PK, name UNIQUE, value TEXT)
-- Lần 1: INSERT
INSERT INTO t (name, value) VALUES ('A', '100'); -- OK, id=1
-- Lần 2: REPLACE cùng name 'A'
REPLACE INTO t (name, value) VALUES ('A', '200');
-- SQLite thấy name 'A' vi phạm UNIQUE
-- -> DELETE hàng id=1
-- -> INSERT hàng mới (id=2, name='A', value='200')
-- Kết quả: Chỉ còn 1 hàng, id đã thay đổi!
REPLACE vs INSERT OR IGNORE vs UPSERT hiện đại
| Lệnh | Hành vi khi trùng UNIQUE/PK | Trường hợp dùng |
|---|---|---|
| INSERT | Lỗi (throw exception) | Chắc chắn chưa tồn tại |
| INSERT OR IGNORE | Bỏ qua, không làm gì | Chỉ insert nếu chưa có, không cập nhật |
| REPLACE / INSERT OR REPLACE | Xóa cũ → Insert mới | Muốn luôn có giá trị mới, không care ID |
| INSERT ... ON CONFLICT DO UPDATE | Cập nhật các cột chỉ định | Giữ ID, cập nhật một phần (SQLite 3.24.0+) |
UPSERT chuẩn SQL (SQLite 3.24.0+, API 26+)
-- Cú pháp hiện đại, khuyến nghị
INSERT INTO SalaryDetails (name, salary) VALUES (?, ?)
ON CONFLICT(name) DO UPDATE SET
salary = excluded.salary,
updated_at = CURRENT_TIMESTAMP;
-- excluded = giá trị mới đang cố insert
-- Giữ nguyên ID, chỉ cập nhật salary
Triển khai UPSERT hiện đại trong Android
public boolean upsertModern(String name, String salary) {
SQLiteDatabase db = getWritableDatabase();
// SQLiteDatabase không có helper cho UPSERT, dùng execSQL
String sql = "INSERT INTO " + TABLE_NAME + " (name, salary) VALUES (?, ?) " +
"ON CONFLICT(name) DO UPDATE SET salary = excluded.salary";
try {
db.execSQL(sql, new Object[]{name, salary});
return true;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
Khi nào dùng REPLACE?
- Bảng cấu hình key-value (Settings):
REPLACE INTO settings (key, value) VALUES ('theme', 'dark') - Cache dữ liệu: key là URL, value là JSON response.
- Dữ liệu tạm, không quan tâm ID, không có FK tham chiếu.
⚠️ Cảnh báo
- REPLACE = DELETE + INSERT → Trigger ON DELETE và ON INSERT đều chạy.
- ID thay đổi → Phá vỡ Foreign Key tham chiếu đến hàng này.
- Auto-increment gap: ID nhảy vọt (1, 5, 12...).
- Hiệu năng: Chậm hơn UPDATE do phải delete + insert + reindex.