# 使用 SQLite 持久化

> SqliteDb / SqliteStmt / SqliteCursor —— CRUD、事务、批量写入与加密。

Source: https://crossfly.dev/zh/docs/data-and-services/storage-sqlite — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# 使用 SQLite 持久化

在 CrossFly 中持久化结构化数据的**规范**方式是 `dev.crossfly.sqlite` 模块 ——
它是对原生库 **cfsqlite**（SQLite3MultipleCiphers）的一层薄封装。它取代了旧的
`dev.crossfly.sql`（JDBC）。

> **在绘制线程之外运行数据库**，并用 `Component.onUI(...)` 回到 UI（见
> [后台工作](/docs/data-and-services/background-work)）。`SqliteDb`、`SqliteStmt` 和 `SqliteCursor` 都是
> `AutoCloseable` → 请使用 **try-with-resources**。

## 打开 / 关闭
```java
try (SqliteDb db = SqliteDb.open("app.db")) {
   db.exec("CREATE TABLE IF NOT EXISTS cliente(id INTEGER PRIMARY KEY, nome TEXT, idade INTEGER)");
   // … 使用 db …
}   // 自动关闭
```
工厂方法：`SqliteDb.open(String)`、`open(String, int flags)`、`open(String, byte[] key)`（加密）、
`openMemory()`。

## 参数化 CRUD
```java
// INSERT/UPDATE/DELETE → execute(sql, params...) 返回受影响的行数
db.execute("INSERT INTO cliente(nome, idade) VALUES(?, ?)", "Ana", 30);
db.execute("UPDATE cliente SET idade=? WHERE id=?", 31, 1);

// SELECT → query(sql, params...) 返回一个游标
try (SqliteCursor c = db.query("SELECT id, nome, idade FROM cliente WHERE idade > ?", 18)) {
   while (c.next()) {
      long id    = c.getLong(0);      // ⚠️ 游标的 getter 是从 0 开始的
      String nome= c.getString(1);
      int idade  = c.getInt(2);
   }
}

// 单一值：
long total = db.queryLong("SELECT COUNT(*) FROM cliente", 0);
String nome = db.queryString("SELECT nome FROM cliente WHERE id=?", 1);
```

## ⚠️ 索引的坑：绑定从 1 开始，getter 从 0 开始
| 操作 | 起始基准 | 示例 |
|---|---|---|
| 参数**绑定**（`SqliteStmt`） | **从 1 开始** | `stmt.bindString(1, nome)` |
| 列**getter**（`SqliteCursor`） | **从 0 开始** | `cursor.getString(0)` |

这是该模块最常见的错误 —— `?` 从 **1** 开始，结果集的列从 **0** 开始。

## 事务与批量写入
```java
db.transaction(() -> {                      // 自动 begin/commit（异常时 rollback）
   db.execute("INSERT INTO cliente(nome) VALUES(?)", "A");
   db.execute("INSERT INTO cliente(nome) VALUES(?)", "B");
});

// 手动的 begin/commit/rollback 也存在：
db.begin(); /* … */ db.commit();

// 批量写入（内部复用 statement）：
List<Object[]> linhas = List.of(new Object[]{"A", 1}, new Object[]{"B", 2});
db.bulkInsert("INSERT INTO cliente(nome, idade) VALUES(?, ?)", linhas);
```

## 可复用的预编译 statement
```java
try (SqliteStmt st = db.prepare("INSERT INTO cliente(nome, idade) VALUES(?, ?)")) {
   for (Cliente c : lista) {
      st.bindAll(c.nome, c.idade);   // 或 bindString(1, ...).bindInt(2, ...)
      st.step();
      st.reset();                    // 复用 statement
   }
}
```

## 可选加密
```java
byte[] chave = obterChave();                       // 你的密钥 (byte[])
try (SqliteDb db = SqliteDb.open("secreto.db", chave)) {   // 默认使用 ChaCha20-Poly1305 加密
   // … 加密的数据库 …
   db.rekey(novaChave);                            // 更换密钥（或传入另一个以移除）
}
```
不提供密钥 = 普通 SQLite，**零开销**。支持的加密算法包括 `CHACHA20`（默认）、
`SQLCIPHER`、`AES256CBC` 等（`Sqlite.Cipher`）。

## 键/值表（取代旧的轻量持久化）
轻量持久化（`Settings.appSettings`）已被**移除**。对于标志/主题/上次登录，一张
键/值表即可解决：
```java
db.exec("CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT)");
db.execute("INSERT OR REPLACE INTO kv(k, v) VALUES(?, ?)", "tema", "escuro");
String tema = db.queryString("SELECT v FROM kv WHERE k=?", "tema");
```
见[文件与偏好设置](/docs/data-and-services/files-and-preferences)。

## 最佳实践
- 始终使用 **try-with-resources**（db、stmt、cursor）—— 避免泄漏原生句柄。
- 批量写入 → `transaction(...)`/`bulkInsert(...)`（快数个数量级）。
- 数据库操作放在**绘制线程之外**；结果用 `onUI(...)` 返回。

## 常见错误
- 弄反起始基准：`?` 是**从 1 开始**，cursor 的列是**从 0 开始**。
- 在绘制线程上运行查询，导致 UI 卡死。
- 忘记关闭 cursor/statement（泄漏原生资源）。

## 另见
- [状态管理](/docs/data-and-services/state-management) · [后台工作](/docs/data-and-services/background-work)
