Persistência com SQLite
A forma canônica de persistir dados estruturados no CrossFly é o módulo dev.crossfly.sqlite —
uma camada fina sobre a lib nativa cfsqlite (SQLite3MultipleCiphers). Substitui o antigo
dev.crossfly.sql (JDBC).
Rode o banco fora da paint thread e volte à UI com
Component.onUI(...)(ver Trabalho em background).SqliteDb,SqliteStmteSqliteCursorsão todosAutoCloseable→ use try-with-resources.
Abrir / fechar
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 …
} // fecha sozinho
Fábricas: SqliteDb.open(String), open(String, int flags), open(String, byte[] key) (cifrado),
openMemory().
CRUD parametrizado
// INSERT/UPDATE/DELETE → execute(sql, params...) retorna nº de linhas afetadas
db.execute("INSERT INTO cliente(nome, idade) VALUES(?, ?)", "Ana", 30);
db.execute("UPDATE cliente SET idade=? WHERE id=?", 31, 1);
// SELECT → query(sql, params...) devolve um cursor
try (SqliteCursor c = db.query("SELECT id, nome, idade FROM cliente WHERE idade > ?", 18)) {
while (c.next()) {
long id = c.getLong(0); // ⚠️ getters do cursor são 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);
⚠️ A pegadinha de índices: bind 1-based, getters 0-based
| Operação | Base | Exemplo |
|---|---|---|
bind de parâmetro (SqliteStmt) |
1-based | stmt.bindString(1, nome) |
getter de coluna (SqliteCursor) |
0-based | cursor.getString(0) |
É o erro mais comum do módulo — os ? começam em 1, as colunas do resultado em 0.
Transações e escrita em massa
db.transaction(() -> { // begin/commit automático (rollback em exceção)
db.execute("INSERT INTO cliente(nome) VALUES(?)", "A");
db.execute("INSERT INTO cliente(nome) VALUES(?)", "B");
});
// begin/commit/rollback manuais também existem:
db.begin(); /* … */ db.commit();
// escrita em massa (statement reusado 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 reusável
try (SqliteStmt st = db.prepare("INSERT INTO cliente(nome, idade) VALUES(?, ?)")) {
for (Cliente c : lista) {
st.bindAll(c.nome, c.idade); // ou bindString(1, ...).bindInt(2, ...)
st.step();
st.reset(); // reaproveita o statement
}
}
Criptografia opcional
byte[] chave = obterChave(); // sua chave (byte[])
try (SqliteDb db = SqliteDb.open("secreto.db", chave)) { // cifra ChaCha20-Poly1305 por padrão
// … banco cifrado …
db.rekey(novaChave); // troca a chave (ou remove passando outra)
}
Sem chave = SQLite normal, zero overhead. Cifras suportadas incluem CHACHA20 (padrão),
SQLCIPHER, AES256CBC, etc. (Sqlite.Cipher).
Tabela chave/valor (substitui a antiga persistência leve)
A persistência leve (Settings.appSettings) foi removida. Para flags/tema/último login, uma
tabela chave/valor resolve:
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");
Boas práticas
- Sempre try-with-resources (db, stmt, cursor) — evita vazar handles nativos.
- Escrita em lote →
transaction(...)/bulkInsert(...)(ordens de magnitude mais rápido). - Banco fora da paint thread; resultados de volta com
onUI(...).
Erros comuns
- Trocar as bases:
?é 1-based, coluna do cursor é 0-based. - Rodar consultas na paint thread e travar a UI.
- Esquecer de fechar cursor/statement (vaza recurso nativo).