Kodokon kodokon.com

Persistencia con SQLite: la capa de acceso a datos

Usa better-sqlite3 con sentencias preparadas, transacciones y un repositorio dedicado para una persistencia rápida y segura.

9 min · 3 preguntas

Abrir esta lección en Kodokon

SQLite se ejecuta dentro de tu proceso: sin servidor, sin red, con una latencia por consulta del orden de un microsegundo. Eso es lo que hace que la API síncrona de better-sqlite3 no solo sea aceptable, sino a menudo más rápida que un driver asíncrono: no tiene sentido pagar una promesa y una vuelta del event loop en una lectura más corta que un tick. La contrapartida es real en las escrituras: una consulta lenta bloquea el loop. Mantén tus consultas indexadas y cortas, y SQLite manejará decenas de miles de lecturas por segundo.

BASH
npm install better-sqlite3
Módulo nativo: se compila en el momento de la instalación si hace falta.

Al abrir la base de datos, dos ajustes marcan la diferencia. El modo WAL (write-ahead logging) permite leer durante una escritura en lugar de bloquear todo el archivo. Y el esquema se aplica de forma idempotente con CREATE TABLE IF NOT EXISTS, más que suficiente antes de introducir una herramienta de migraciones de verdad.

JAVASCRIPT
import Database from 'better-sqlite3';

export function createDb(path = 'app.db') {
  const db = new Database(path);
  db.pragma('journal_mode = WAL');
  db.exec(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
  )`);
  return db;
}
connection.js: apertura, ajustes, esquema idempotente.

La capa de acceso a datos, el repositorio, es el único módulo que habla SQL. Las consultas se preparan una vez en la construcción y luego se reutilizan en cada llamada: el motor no vuelve a analizar el texto SQL, y los valores pasan por marcadores de posición ?, nunca por concatenación. Es exactamente la interfaz findAll, findByEmail, insert que espera el servicio de la lección anterior.

JAVASCRIPT
export function createUserRepository(db) {
  const insertStmt = db.prepare(
    'INSERT INTO users (email, name) VALUES (?, ?)',
  );
  const byEmailStmt = db.prepare(
    'SELECT * FROM users WHERE email = ?',
  );
  const allStmt = db.prepare(
    'SELECT * FROM users ORDER BY id',
  );
  return {
    insert(user) {
      const info = insertStmt.run(user.email, user.name);
      return { id: info.lastInsertRowid, ...user };
    },
    findByEmail(email) {
      return byEmailStmt.get(email);
    },
    findAll() {
      return allStmt.all();
    },
  };
}
Preparado una vez, ejecutado mil veces: run, get, all.
JAVASCRIPT
import { createDb } from './connection.js';

const db = createDb();
const insertStmt = db.prepare(
  'INSERT INTO users (email, name) VALUES (?, ?)',
);
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertStmt.run(user.email, user.name);
  }
});
insertMany([
  { email: 'ada@example.com', name: 'Ada' },
  { email: 'linus@example.com', name: 'Linus' },
]);
db.transaction: se confirma todo, o se revierte todo.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué la API síncrona de better-sqlite3 no es una contradicción en Node?
    • Porque Node ejecuta los módulos nativos en un hilo aparte
    • Porque SQLite está en el proceso: la consulta cuesta menos que la orquestación asíncrona que evitaría
    • Porque las escrituras se ponen en cola automáticamente
    • Porque el modo WAL hace que cada consulta sea no bloqueante
  2. ¿Qué garantizan los marcadores de posición ? en una sentencia preparada?
    • Los valores se pasan aparte del SQL y nunca pueden ejecutarse como código
    • Los valores se cifran antes de almacenarse
    • La consulta se cachea automáticamente en disco
  3. ¿Qué doble ventaja aporta db.transaction para mil inserciones?
    • Compresión de datos y deduplicación automática
    • Atomicidad (todo o nada) y una única sincronización a disco en lugar de mil
    • Validación del esquema y creación de los índices que falten