# Cómo escribir en masa con transacción y bulkInsert

> Insertar miles de filas rápido — transacción, statement reutilizado con reset() o bulkInsert nativo.

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

# Cómo escribir en masa con transacción y bulkInsert

**Propósito.** Insertar/actualizar muchas filas con alto rendimiento. Sin transacción, cada INSERT se convierte en un commit en disco (lento). Hay tres niveles: (1) envolver todo en `transaction(...)`, (2) reutilizar un `SqliteStmt` con `reset()`, (3) usar `bulkInsert`, que hace el loop bind/step/reset en código nativo (más rápido).

## APIs usadas
- `SqliteDb.transaction(Runnable body)` — ejecuta `body` dentro de `BEGIN`/`COMMIT`; hace **`ROLLBACK` automático** si `body` lanza `RuntimeException` (y la relanza). Forma recomendada para atomicidad.
- `SqliteDb.begin()` / `commit()` / `rollback()` — control manual de la transacción (equivalen a `exec("BEGIN"/"COMMIT"/"ROLLBACK")`). Prefiere `transaction(...)` para no olvidar el rollback.
- `SqliteDb.prepare(String sql)` — prepara un `SqliteStmt` reutilizable (compila una vez). **Debes cerrarlo** (`try-with-resources`).
- `SqliteStmt.bindInt/bindLong/bindDouble/bindString/bindBlob/bindNull(idx, v)` y `bindAll(Object...)` — bind **1-based**; retornan `this` (fluido). `step()` ejecuta; `reset()` prepara para la próxima ejecución (mantiene los binds; usa `clearBindings()` si quieres reiniciarlos).
- `SqliteDb.bulkInsert(String sql, List<Object[]> rows)` — prepara `sql` una vez y lo ejecuta para cada `Object[]` **dentro de una única transacción**, en loop nativo. Retorna el nº de filas procesadas. Cada `Object[]` tiene un elemento por parámetro `?`; tipos aceptados: `Long/Integer`, `Double/Float`, `String`, `byte[]`, `null`. **Los tipos de columna se infieren de la PRIMERA fila** — mantén las filas homogéneas. Alias: `bulkUpdate`, `bulkDelete`.

> **¿Por qué reutilizar el statement?** `prepare` compila el SQL; repetir `prepare` en cada fila recompila. Reutilizar con `reset()` compila una vez y solo re-vincula — mucho más rápido en loops grandes.

## Ejemplo completo (copiar, pegar y ejecutar)

```java
package com.exemplo;

import java.util.*;                          // List, ArrayList

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

/** 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 t(a INTEGER, b TEXT)");

         // Opción A — transacción + statement reutilizado (reset entre ejecuciones)
         db.transaction(() ->
         {
            try (SqliteStmt st = db.prepare("INSERT INTO t(a, b) VALUES(?, ?)"))
            {
               for (int i = 0; i < 10_000; i++)
               {
                  st.bindInt(1, i)            // idx 1-based
                    .bindString(2, "row" + i)
                    .step();
                  st.reset();                 // listo para la siguiente fila
               }
            }
         }); // commit automático; rollback automático si lanza

         // Opción B — bulkInsert nativo (el más rápido para grandes volúmenes)
         List<Object[]> rows = new ArrayList<>();
         for (int i = 0; i < 10_000; i++)
            rows.add(new Object[]{ i, "row" + i });   // tipos inferidos de la 1ª fila
         int n = db.bulkInsert("INSERT INTO t(a, b) VALUES(?, ?)", rows);
         System.out.println(n + " linhas inseridas");
      }
   }
}
```

> 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: transaction, prepare + bind (1-based) + step + reset, bulkInsert
import dev.crossfly.sqlite.*;
import java.util.*;

try (SqliteDb db = SqliteDb.open("app.db"))
{
   db.exec("CREATE TABLE IF NOT EXISTS t(a INTEGER, b TEXT)");

   // Opción A — transacción + statement reutilizado (reset entre ejecuciones)
   db.transaction(() ->
   {
      try (SqliteStmt st = db.prepare("INSERT INTO t(a, b) VALUES(?, ?)"))
      {
         for (int i = 0; i < 10_000; i++)
         {
            st.bindInt(1, i)            // idx 1-based
              .bindString(2, "row" + i)
              .step();
            st.reset();                 // listo para la siguiente fila
         }
      }
   }); // commit automático; rollback automático si lanza

   // Opción B — bulkInsert nativo (el más rápido para grandes volúmenes)
   List<Object[]> rows = new ArrayList<>();
   for (int i = 0; i < 10_000; i++)
      rows.add(new Object[]{ i, "row" + i });   // tipos inferidos de la 1ª fila
   int n = db.bulkInsert("INSERT INTO t(a, b) VALUES(?, ?)", rows);
   System.out.println(n + " linhas inseridas");
}
```

## Profundiza
- [Trabajo en background](/docs/data-and-services/background-work) · [Rendimiento y listas grandes](/docs/best-practices/performance-e-listas-grandes)
- Recetas hermanas: [CRUD parametrizado](/docs/cookbook/data/crud) · [Uso fuera de la paint thread](/docs/cookbook/data/banco-fora-da-paint-thread)
