# Persistencia con SQLite

> SqliteDb / SqliteStmt / SqliteCursor — CRUD, transacciones, escritura masiva y cifrado.

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

# Persistencia con SQLite

La forma **canónica** de persistir datos estructurados en CrossFly es el módulo `dev.crossfly.sqlite` —
una capa fina sobre la lib nativa **cfsqlite** (SQLite3MultipleCiphers). Sustituye al antiguo
`dev.crossfly.sql` (JDBC).

> **Ejecuta la base de datos fuera del paint thread** y vuelve a la UI con `Component.onUI(...)` (ver
> [Trabajo en segundo plano](/docs/data-and-services/background-work)). `SqliteDb`, `SqliteStmt` y `SqliteCursor` son todos
> `AutoCloseable` → usa **try-with-resources**.

## Abrir / cerrar
```java
try (SqliteDb db = SqliteDb.open("app.db")) {
   db.exec("CREATE TABLE IF NOT EXISTS cliente(id INTEGER PRIMARY KEY, nome TEXT, idade INTEGER)");
   // … usar db …
}   // se cierra por sí solo
```
Fábricas: `SqliteDb.open(String)`, `open(String, int flags)`, `open(String, byte[] key)` (cifrado),
`openMemory()`.

## CRUD parametrizado
```java
// INSERT/UPDATE/DELETE → execute(sql, params...) retorna nº de filas afectadas
db.execute("INSERT INTO cliente(nome, idade) VALUES(?, ?)", "Ana", 30);
db.execute("UPDATE cliente SET idade=? WHERE id=?", 31, 1);

// SELECT → query(sql, params...) devuelve un cursor
try (SqliteCursor c = db.query("SELECT id, nome, idade FROM cliente WHERE idade > ?", 18)) {
   while (c.next()) {
      long id    = c.getLong(0);      // ⚠️ los getters del cursor son 0-based
      String nome= c.getString(1);
      int idade  = c.getInt(2);
   }
}

// valor único:
long total = db.queryLong("SELECT COUNT(*) FROM cliente", 0);
String nome = db.queryString("SELECT nome FROM cliente WHERE id=?", 1);
```

## ⚠️ El truco de los índices: bind 1-based, getters 0-based
| Operación | Base | Ejemplo |
|---|---|---|
| **bind** de parámetro (`SqliteStmt`) | **1-based** | `stmt.bindString(1, nome)` |
| **getter** de columna (`SqliteCursor`) | **0-based** | `cursor.getString(0)` |

Es el error más común del módulo — los `?` empiezan en **1**, las columnas del resultado en **0**.

## Transacciones y escritura masiva
```java
db.transaction(() -> {                      // begin/commit automático (rollback en excepción)
   db.execute("INSERT INTO cliente(nome) VALUES(?)", "A");
   db.execute("INSERT INTO cliente(nome) VALUES(?)", "B");
});

// begin/commit/rollback manuales también existen:
db.begin(); /* … */ db.commit();

// escritura en masa (statement reutilizado internamente):
List<Object[]> linhas = List.of(new Object[]{"A", 1}, new Object[]{"B", 2});
db.bulkInsert("INSERT INTO cliente(nome, idade) VALUES(?, ?)", linhas);
```

## Statement preparado reutilizable
```java
try (SqliteStmt st = db.prepare("INSERT INTO cliente(nome, idade) VALUES(?, ?)")) {
   for (Cliente c : lista) {
      st.bindAll(c.nome, c.idade);   // o bindString(1, ...).bindInt(2, ...)
      st.step();
      st.reset();                    // reutiliza el statement
   }
}
```

## Cifrado opcional
```java
byte[] chave = obterChave();                       // tu clave (byte[])
try (SqliteDb db = SqliteDb.open("secreto.db", chave)) {   // cifra ChaCha20-Poly1305 por defecto
   // … base de datos cifrada …
   db.rekey(novaChave);                            // cambia la clave (o la elimina pasando otra)
}
```
Sin clave = SQLite normal, **cero overhead**. Las cifras soportadas incluyen `CHACHA20` (por defecto),
`SQLCIPHER`, `AES256CBC`, etc. (`Sqlite.Cipher`).

## Tabla clave/valor (sustituye la antigua persistencia ligera)
La persistencia ligera (`Settings.appSettings`) fue **eliminada**. Para flags/tema/último login, una
tabla clave/valor lo resuelve:
```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");
```
Ver [Archivos y preferencias](/docs/data-and-services/files-and-preferences).

## Buenas prácticas
- Usa siempre **try-with-resources** (db, stmt, cursor) — evita filtrar handles nativos.
- Escritura por lotes → `transaction(...)`/`bulkInsert(...)` (órdenes de magnitud más rápido).
- Base de datos **fuera del paint thread**; resultados de vuelta con `onUI(...)`.

## Errores comunes
- Cambiar las bases: `?` es **1-based**, la columna del cursor es **0-based**.
- Ejecutar consultas en el paint thread y bloquear la UI.
- Olvidar cerrar cursor/statement (filtra recurso nativo).

## Véase también
- [Gestión de estado](/docs/data-and-services/state-management) · [Trabajo en segundo plano](/docs/data-and-services/background-work)
