# Cómo hacer CRUD parametrizado en SQLite

> Insertar, leer, actualizar y borrar con parámetros posicionales y cursor, sin concatenar SQL.

Source: https://crossfly.dev/es/docs/cookbook/data/crud — CrossFly documentation. Full docs as one file: https://crossfly.dev/llms-full.txt

# Cómo hacer CRUD parametrizado en SQLite

**Propósito.** Ejecutar INSERT/UPDATE/DELETE y SELECT con **parámetros posicionales** (`?`), evitando la concatenación de cadenas (que causa SQL injection y errores de comillas). Presta atención a las dos convenciones de índice de CrossFly.

> ⚠️ **Índices.** Al **vincular** parámetros, los índices son **1-based** (`?` nº 1, 2, ...). Al **leer** columnas del cursor, los índices son **0-based** (`getString(0)` = primera columna). Esto es intencional y refleja la API nativa de SQLite.

## APIs usadas
- `SqliteDb.execute(String sql, Object... params)` — prepara, hace `bindAll(params)` (bind **1-based**), ejecuta un único INSERT/UPDATE/DELETE y retorna el **nº de filas afectadas** (`int`). Los tipos se infieren: `Integer/Long/Short/Byte`→entero, `Float/Double`→real, `Boolean`→0/1, `byte[]`→BLOB, `null`→NULL, en otro caso `toString()`→TEXT.
- `SqliteDb.query(String sql, Object... params)` — retorna un `SqliteCursor` **abierto** (bind 1-based). El llamador **debe cerrarlo** (usa `try-with-resources`).
- `SqliteCursor.next()` — avanza a la siguiente fila; retorna `true` si hay fila.
- `SqliteCursor.getInt(col)/getLong/getDouble/getString/getBlob/isNull(col)` — getters tipados, columna **0-based**.
- `SqliteDb.queryLong(sql, defaultValue, params)` — atajo: 1ª columna de la 1ª fila como `long`, o `defaultValue` si no hay filas.
- `SqliteDb.queryString(sql, params)` — atajo: 1ª columna de la 1ª fila como `String`, o `null` si no hay filas.
- `SqliteDb.lastInsertRowid()` — rowid del último INSERT (`long`). `SqliteDb.changes()` — filas alteradas por la última instrucción.

## Ejemplo completo (copiar, pegar y ejecutar)

```java
package com.exemplo;

import dev.crossfly.ui.*;                     // Window
import dev.crossfly.ui.layout.*;             // VerticalLayout
import dev.crossfly.sqlite.*;                // SqliteDb, SqliteCursor

/** Pantalla principal ejecutable. Apunta el launcher (classname) y corre: gradlew :lwjgl3:run */
public class MainWindow extends Window
{
   @Override
   public void onShow() throws Exception
   {
      super.onShow();
      setLayout(new VerticalLayout());

      try (SqliteDb db = SqliteDb.open("app.db"))
      {
         db.exec("CREATE TABLE IF NOT EXISTS person(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

         // CREATE — parámetros posicionales (? nº 1 = name, ? nº 2 = age)
         int inserted = db.execute("INSERT INTO person(name, age) VALUES(?, ?)", "Alice", 30);
         long id = db.lastInsertRowid();            // rowid generado
         System.out.println(inserted + " linha(s), id=" + id);

         // READ — cursor: columnas 0-based
         try (SqliteCursor c = db.query("SELECT name, age FROM person WHERE age > ?", 18))
         {
            while (c.next())
               System.out.println(c.getString(0) + " = " + c.getInt(1));   // col 0 = name, col 1 = age
         }

         // READ escalar — atajos
         long total = db.queryLong("SELECT count(*) FROM person", 0);
         String nome = db.queryString("SELECT name FROM person WHERE id = ?", id);

         // UPDATE — devuelve nº de filas afectadas
         int updated = db.execute("UPDATE person SET age = ? WHERE id = ?", 31, id);

         // DELETE
         int deleted = db.execute("DELETE FROM person WHERE id = ?", id);
      }
   }
}
```

> El fragmento de abajo muestra **solo el núcleo**. Para ejecutarlo, colócalo en el esqueleto `MainWindow` de arriba.

## Fragmento mínimo
```java
// verificado en la fuente: execute (bind 1-based), query + cursor (getters 0-based), queryLong/queryString
import dev.crossfly.sqlite.*;

try (SqliteDb db = SqliteDb.open("app.db"))
{
   db.exec("CREATE TABLE IF NOT EXISTS person(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

   // CREATE — parámetros posicionales (? nº 1 = name, ? nº 2 = age)
   int inserted = db.execute("INSERT INTO person(name, age) VALUES(?, ?)", "Alice", 30);
   long id = db.lastInsertRowid();            // rowid generado
   System.out.println(inserted + " linha(s), id=" + id);

   // READ — cursor: columnas 0-based
   try (SqliteCursor c = db.query("SELECT name, age FROM person WHERE age > ?", 18))
   {
      while (c.next())
         System.out.println(c.getString(0) + " = " + c.getInt(1));   // col 0 = name, col 1 = age
   }

   // READ escalar — atajos
   long total = db.queryLong("SELECT count(*) FROM person", 0);
   String nome = db.queryString("SELECT name FROM person WHERE id = ?", id);

   // UPDATE — devuelve nº de filas afectadas
   int updated = db.execute("UPDATE person SET age = ? WHERE id = ?", 31, id);

   // DELETE
   int deleted = db.execute("DELETE FROM person WHERE id = ?", id);
}
```

## Profundiza
- [Guía de storage SQLite](/docs/data-and-services/storage-sqlite) · Guía de desarrollo
- Recetas hermanas: [Abrir/crear base de datos](/docs/cookbook/data/abrir-banco) · [Transacción y escritura masiva](/docs/cookbook/data/transacao-e-bulk) · [Uso fuera de la paint thread](/docs/cookbook/data/banco-fora-da-paint-thread)
