# 持久化数据

> SQLite（SqliteDb/SqliteCursor）置于仓储之后 —— bind 从 1 开始，getter 从 0 开始。

Source: https://crossfly.dev/zh/docs/learn/persisting-data — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# 9. 持久化数据

> **你将学到：** 用 SQLite 真正地写入数据，并置于仓储之后。
> **前置要求：** [加载数据](/docs/learn/loading-data)。

## 完整示例（复制、粘贴即可运行）
一个 SQLite 仓储（外观）将 SQL 隔离起来，**在 paint thread 之外**通过 `Thread` 调用，
结果通过 `Component.onUI` 显示。bind 从 1 开始，cursor 从 0 开始，使用 try-with-resources。原样即可编译。
```java
package com.exemplo;

import java.util.*;
import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.sqlite.*;

public class MainWindow extends Window
{
   private final TarefaRepositorySQLite repo = new TarefaRepositorySQLite();
   private final Component listaBox = new Component();

   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      Button add = new Button("Inserir e recarregar");
      add.onAction(c -> new Thread(() -> {               // I/O 在 paint thread 之外
         repo.inserir("Tarefa " + System.currentTimeMillis());
         var dados = repo.listar();
         Component.onUI(() -> renderizar(dados));         // 返回 paint thread
      }).start());
      add(LAY_AUTO, add);

      listaBox.setLayout(new VerticalLayout());
      add(LAY_FILL, listaBox);

      new Thread(() -> {                                  // 后台初始加载
         var dados = repo.listar();
         Component.onUI(() -> renderizar(dados));
      }).start();
   }

   private void renderizar(List<Tarefa> dados)
   {
      listaBox.removeAll();
      for (Tarefa t : dados) listaBox.add(LAY_AUTO, new Label(t.id + " · " + t.titulo));
      listaBox.setValidLayout(false);
      Component.repaint();
   }
}

class Tarefa
{
   final int id; final String titulo;
   Tarefa(int id, String titulo) { this.id = id; this.titulo = titulo; }
}

class TarefaRepositorySQLite
{
   private static final String DB = "tarefas.db";

   public TarefaRepositorySQLite()
   {
      try (SqliteDb db = SqliteDb.open(DB)) {
         db.exec("CREATE TABLE IF NOT EXISTS tarefa(id INTEGER PRIMARY KEY, titulo TEXT, feito INTEGER)");
      } catch (Exception e) { Debug.handle(e); }
   }

   public List<Tarefa> listar()
   {
      List<Tarefa> out = new ArrayList<>();
      try (SqliteDb db = SqliteDb.open(DB);
           SqliteCursor c = db.query("SELECT id, titulo FROM tarefa ORDER BY id")) {
         while (c.next())                                 // getter 从 0 开始
            out.add(new Tarefa(c.getInt(0), c.getString(1)));
      } catch (Exception e) { Debug.handle(e); }
      return out;
   }

   public void inserir(String titulo)
   {
      try (SqliteDb db = SqliteDb.open(DB)) {
         db.execute("INSERT INTO tarefa(titulo, feito) VALUES(?, ?)", titulo, 0);   // bind 从 1 开始
      } catch (Exception e) { Debug.handle(e); }
   }
}
```
> 下面的代码片段只展示**核心部分**（放入 `onShow()` 内部的内容）。要运行它们，请放进上面的 `MainWindow` 骨架中。

## `dev.crossfly.sqlite` 模块
`SqliteDb`/`SqliteStmt`/`SqliteCursor` —— 全都是 `AutoCloseable`（使用 try-with-resources），并且
**在 paint thread 之外**运行。

## 仓储（外观）
把 SQL 隔离到一个类中；UI 永远不直接说 SQL：
```java
public class TarefaRepositorySQLite {
   private static final String DB = "tarefas.db";

   public TarefaRepositorySQLite() {
      try (SqliteDb db = SqliteDb.open(DB)) {
         db.exec("CREATE TABLE IF NOT EXISTS tarefa(id INTEGER PRIMARY KEY, titulo TEXT, feito INTEGER)");
      } catch (Exception e) { Debug.handle(e); }
   }

   public List<Tarefa> listar() {
      List<Tarefa> out = new ArrayList<>();
      try (SqliteDb db = SqliteDb.open(DB);
           SqliteCursor c = db.query("SELECT id, titulo FROM tarefa ORDER BY id")) {
         while (c.next())                                   // getter 从 0 开始
            out.add(new Tarefa(c.getInt(0), c.getString(1)));
      } catch (Exception e) { Debug.handle(e); }
      return out;
   }

   public void inserir(String titulo) {
      try (SqliteDb db = SqliteDb.open(DB)) {
         db.execute("INSERT INTO tarefa(titulo, feito) VALUES(?, ?)", titulo, 0);   // bind 从 1 开始
      } catch (Exception e) { Debug.handle(e); }
   }
}
```

## ⚠️ 两条黄金法则
| 操作 | 起始基数 |
|---|---|
| 参数 **bind**（`?`） | **从 1 开始** |
| 列 **getter**（cursor） | **从 0 开始** |

批量写入时，使用 `transaction(...)`/`bulkInsert(...)`。对于敏感数据，用密钥打开
（`SqliteDb.open(file, key)`，参见[安全](/docs/best-practices/seguranca)）。

## 本步骤构建的内容
**`TarefaRepositorySQLite`** —— 应用现在**能在重启后存活**了。

## 回顾
通过仓储使用 SQLite，try-with-resources，在 paint thread 之外；**bind 从 1 开始，cursor 从 0 开始**；
批量使用事务/bulk。

## 下一步
→ [让它响应式](/docs/learn/making-it-responsive)

## 另请参阅
- [SQLite 持久化](/docs/data-and-services/storage-sqlite) · 权威来源：第 23 章 —— SQLite
