Cómo usar SQLite con Node.js

Aprenda a crear una base de datos SQLite y acceda a ella desde Node.js para crear tablas, insertar datos y leer datos.

Primeros pasos

Debido a que el código que estamos escribiendo para este tutorial es "independiente" (lo que significa que no es parte de una aplicación o proyecto más grande), vamos a crear un proyecto de Node.js desde cero. Si aún no tiene Node.js instalado en su computadora, lea este tutorial primero y luego regrese aquí.

Una vez que haya instalado Node.js en su computadora, desde la carpeta de proyectos en su computadora (por ejemplo, ~/projects ), crea una nueva carpeta para nuestro trabajo:

Terminal

mkdir app

A continuación, cd en ese directorio y crea un index.js archivo (aquí es donde escribiremos nuestro código para el tutorial):

Terminal

cd app && touch index.js

A continuación, queremos instalar dos dependencias adicionales, sqlite3 y lorem-ipsum :

Terminal

npm i sqlite3 lorem-ipsum

El primero nos dará acceso a un controlador Node.js para SQLite (lo que usaremos para conectarnos a la base de datos en nuestro código), mientras que el segundo nos ayudará a generar algunos datos de prueba para insertarlos en nuestra base de datos.

Un último paso:en el package.json archivo que se creó para usted, asegúrese de agregar el campo "type": "module" como propiedad. Esto habilitará la compatibilidad con ESModules y nos permitirá usar el import declaraciones que se muestran en el siguiente código.

Con eso en su lugar, estamos listos para comenzar.

Una breve introducción a SQLite

Cuando la mayoría de las personas piensan en una base de datos, piensan en algo que escribe y recupera datos del disco (como PostgreSQL) o directamente de la memoria (como Redis). Sin embargo, para que estas bases de datos funcionen, necesitan un servidor de base de datos :un proceso de ejecución prolongada que maneja las conexiones entrantes.

Para una aplicación completa, este tipo de bases de datos son útiles porque ofrecen un amplio conjunto de funciones y le permiten administrar cantidades significativas de datos.

Sin embargo, en algunos casos, este tipo de bases de datos son problemáticas, es decir, cuando intenta mantener una huella lo más ligera posible o limitar la cantidad de procesos "caros" (en términos de CPU y memoria) que se ejecutan junto con su aplicación. Para combatir esto, tenemos una forma diferente de base de datos conocida como base de datos integrada . Estas son bases de datos que no requieren un servidor para funcionar, lo que significa que pueden ejecutarse en entornos con recursos limitados (por ejemplo, una Raspberry Pi).

La opción más popular para este tipo de base de datos es SQLite. Una base de datos basada en SQL que opera como un solo archivo usando un formato especial para almacenar sus datos. Una "base de datos" en SQLite es solo un archivo como posts.db o users.db . Cuando usa un controlador para interactuar con SQLite, lee y escribe en este archivo. Para simplificar las cosas, SQLite ofrece un conjunto limitado de tipos de datos (solo cinco:NULL , INTEGER , REAL(FLOAT) , TEXT y BLOB ).

Agregar una nueva base de datos y tablas

Para comenzar con nuestro código, queremos asegurarnos de que tenemos dos cosas disponibles:

  1. Una base de datos para escribir datos a .
  2. Una tabla dentro de esa base de datos para organizar nuestros datos.

Comencemos conectando una conexión a una base de datos (si no existe, nuestro controlador la creará por nosotros) y luego agréguele una tabla.

/index.js

import sqlite3 from 'sqlite3';

const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');

Sólo unas pocas líneas. Primero, arriba, necesitamos importar sqlite3 del paquete que instalamos anteriormente a través de NPM. Recuerde:este es el controlador paquete (lo que usamos para comunicarnos con la base de datos) y no SQLite en sí. A continuación, creamos una nueva variable SQLite3 (usamos este caso porque esperamos recibir una clase de JavaScript a cambio; este caso es un patrón común para indicar eso) que se asigna a una llamada a sqlite3.verbose() . Esto nos está dando una copia de la clase que usaremos para iniciar el controlador en verbose mode, lo que significa que incluirá el seguimiento completo de la pila para cualquier error que encuentre (útil para la depuración).

A continuación, con nuestra clase, creamos una variable más db lo que nos da acceso a nuestra instancia/conexión real a nuestra base de datos llamando a new SQLite3.Database('posts.db') . Aquí, posts.db es el nombre de la base de datos a la que queremos conectarnos. En el caso de que esta base de datos (un archivo raíz de nuestro proyecto) no exista, el controlador la creará por nosotros.

Crear un envoltorio de promesa

Antes de comenzar a crear nuestros datos, para facilitar nuestro trabajo, vamos a escribir rápidamente una función contenedora que nos brinde una versión prometida del sqlite3 conductor. Queremos hacer esto porque, de manera predeterminada, el paquete usa un patrón de devolución de llamada (lo que puede generar un código desordenado).

/index.js

import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';

const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');

const query = (command, method = 'all') => {
  return new Promise((resolve, reject) => {
    db[method](command, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
};

Justo debajo de nuestro código de configuración, aquí hemos agregado una nueva función query (el nombre es arbitrario) que toma dos argumentos:command cuál es la instrucción SQL que queremos ejecutar y method cual es el sqlite3 conductor método que queremos llamar.

Dentro de esa función, devolvemos una nueva promesa de JavaScript que envuelve una llamada a db[method] donde db es la conexión/instancia que acabamos de configurar arriba y [method] estamos usando la notación de paréntesis de JavaScript para decir "llame al método con el mismo nombre que el valor actual de nuestro method variable". Por ejemplo, si no pasar cualquier cosa por method , por defecto pasamos all lo que significa que aquí estaríamos ejecutando db.all() . Si pasamos el método como get , estaríamos haciendo db.get() .

Como esperamos que ese método sea una función, lo llamamos db[method]() , pasando nuestro SQL command como primer argumento y luego pasar una función de devolución de llamada recibiendo un error o un result como segundo argumento.

Dentro de esa función, si tenemos un error queremos llamar al reject() método de nuestra Promesa pasando el error que ocurrió y si todo está bien, queremos llamar al resolve() de nuestra Promesa, pasando el result recibimos.

Con esto, estamos listos para comenzar a ejecutar comandos en nuestra base de datos.

Insertar datos en una tabla

Como su nombre lo indica, SQLite es solo una base de datos SQL. Excepto por ciertas limitaciones, si está familiarizado con la sintaxis SQL básica utilizada por other bases de datos (por ejemplo, PostgreSQL o MySQL), se sentirá como en casa. Primero, para realmente poner datos en nuestra base de datos, necesitamos una tabla dentro de esa base de datos para existir. Para hacerlo, vamos a usar el query() función que acabamos de conectar.

/index.js

import sqlite3 from 'sqlite3';

const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');

const query = (command, method = 'all') => { ... };

db.serialize(async () => {
  await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
});

En la parte inferior de nuestro archivo, hemos realizado una llamada a una nueva función db.serialize() que a su vez recibe una función. Esta función le dice al sqlite3 controlador que queremos serializar nuestras llamadas a la base de datos, lo que significa que cada comando SQL que ejecutamos dentro de la función que le pasamos se ejecuta y se completa antes se permite ejecutar el siguiente comando SQL.

Por ahora, solo tenemos un solo comando dentro. Para ejecutarlo, hacemos uso de nuestro query() función que acabamos de conectar, con el prefijo await palabra clave (es por eso que tenemos el async palabra clave que precede a la función que pasamos a db.serialize() —sin eso, nuestro await declaración arrojaría un error).

A él le pasamos el comando SQL que queremos ejecutar como primer argumento y luego el method queremos ejecutar en nuestro controlador de base de datos como segundo argumento:run . Si observamos de cerca el comando, nuestro objetivo aquí es crear una nueva tabla llamada posts en nuestra base de datos si aún no existe . Para esa tabla, estamos definiendo cinco columnas:

  1. date que es una cadena de fecha ISO-8601 como 2022-04-29T00:00:00.000Z .
  2. title que es el título de nuestra publicación como una cadena.
  3. author que es el nombre del autor de la publicación como una cadena.
  4. content cuál es el contenido de nuestra publicación como una cadena.
  5. tags que es una lista de etiquetas separadas por comas para nuestra publicación como una cadena.

Con esto, cuando ejecutamos nuestro index.js archivo (desde nuestra terminal, en la raíz del proyecto, podemos ejecutar node index.js para ejecutar el código), si el posts la tabla no existe en posts.db , SQLite lo creará con las columnas especificadas.

/index.js

import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';

const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');

const query = (command, method = 'all') => { ... };

const createPostsIfEmpty = async () => {
  const existingPosts = await query('SELECT * FROM posts');

  if (existingPosts?.length === 0) {
    const lorem = new LoremIpsum();

    for (let i = 0; i < 1000; i += 1) {
      const tags = [...Array(3)].map(() => lorem.generateWords(1));
      await query(`INSERT INTO posts VALUES ("${new Date().toISOString()}", "${lorem.generateWords(10)}", "Ryan Glover", "${lorem.generateParagraphs(5)}", "${tags}")`, 'run');
    }
  }
};

db.serialize(async () => {
  await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
  await createPostsIfEmpty();
});

A continuación, con nuestra tabla, queremos crear algunos datos de prueba. Para hacerlo, vamos a agregar otra función arriba nuestra llamada al db.serialize() llamado createPostsIfEmpty() .

Como su nombre lo indica, nuestro objetivo será verificar si nuestro posts la tabla está vacía, y si lo está, inserte algunos datos de prueba para que los podamos leer.

Tal como vimos anteriormente, la función que estamos definiendo aquí deberá tener el prefijo async para que podamos usar con seguridad el await palabra clave sin desencadenar un error de JavaScript.

Dentro de esa función, lo primero que queremos hacer es comprobar si tenemos alguna publicación. Para hacerlo llamamos al await query() pasando la instrucción SQL SELECT * FROM posts que dice "seleccione todas las columnas del posts mesa". Tenga en cuenta que no pasar un segundo argumento a query() aquí, es decir, queremos usar el all predeterminado para nuestro controlador (esto devuelve all filas que coinciden con nuestra consulta como una matriz).

Si la matriz que recibimos de vuelta, aquí, almacenada en el existingPosts variable:tiene una longitud de 0 (lo que significa que la tabla está vacía), queremos insertar algunos datos.

Para hacerlo, arriba hemos importado el LoremIpsum clase del lorem-ipsum paquete que instalamos anteriormente. Como sugiere el nombre, este paquete nos ayudará a generar algunos datos falsos sobre la marcha.

Para usarlo, primero, necesitamos crear una instancia llamando a new LoremIpsum() , que hemos almacenado en una variable lorem aquí. A continuación, para crear nuestros datos, usaremos un JavaScript for bucle que creará 1000 publicaciones en nuestro posts mesa.

Dentro de ese for loop, primero, creamos una variable tags que generará una matriz de 3 cadenas donde cada cadena es el resultado de llamar a lorem.generateWords(1) . Para hacer eso, usamos un pequeño truco con Array(3) diciendo "crear una matriz de 3 elementos" que serán indefinidos elementos y luego use el ... operador de propagación para desempaquetarlos en otra matriz (técnicamente innecesario, pero nos da certeza de nuestro .map() la llamada se ejecuta en un valor de matriz real). A continuación, usamos un .map() para iterar sobre la matriz de undefined valores y para cada uno, devolver una cadena a través de lorem.generateWords() .

Con esto, nuevamente, usamos nuestro query() función para ejecutar un comando SQL, esta vez haciendo un INSERT en nuestro posts mesa. Como segundo argumento, pasamos run como método para indicar que solo queremos ejecutar este comando y no espere un valor de retorno.

Eso es todo para obtener datos en la tabla. Ahora, para nuestro último paso, aprendamos cómo volver a leer los datos que acabamos de insertar.

Lectura de datos

Solo una línea para hacer esto. Retrocede en nuestro db.serialize() función, ahora, deberíamos tener algunos datos que podemos consultar:

/index.js

import sqlite3 from 'sqlite3';
import { LoremIpsum } from 'lorem-ipsum';

const SQLite3 = sqlite3.verbose();
const db = new SQLite3.Database('posts.db');

const query = (command, method = 'all') => {
  return new Promise((resolve, reject) => {
    db[method](command, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
};

const createPostsIfEmpty = async () => {
  const existingPosts = await query('SELECT * FROM posts');

  if (existingPosts?.length === 0) {
    const lorem = new LoremIpsum();

    for (let i = 0; i < 1000; i += 1) {
      const tags = [...Array(3)].map(() => lorem.generateWords(1));
      await query(`INSERT INTO posts VALUES ("${new Date().toISOString()}", "${lorem.generateWords(10)}", "Ryan Glover", "${lorem.generateParagraphs(5)}", "${tags}")`, 'run');
    }
  }
};

db.serialize(async () => {
  await query("CREATE TABLE IF NOT EXISTS posts (date text, title text, author text, content text, tags text)", 'run');
  await createPostsIfEmpty();

  const existingPosts = await query('SELECT rowid as id, date, title, author, content, tags FROM posts');
  console.log(existingPosts);
});

Abajo en la parte inferior, estamos usando query() una última vez para realizar un SELECT comando, esta vez pasando los campos específicos que queremos recuperar (lo más destacado aquí es leer rowid as id donde rowid es el ID predeterminado que SQLite agrega para nosotros pero no especificamos en la creación de nuestra tabla). Porque usamos por defecto el all método, esperamos que esto devuelva las 1000 filas completas que insertamos en posts .

Si salimos existingPosts , ¡tenemos una base de datos SQLite en funcionamiento!

Terminando

En este tutorial, aprendimos cómo conectar una base de datos SQLite. Aprendimos cómo crear un archivo de base de datos sobre la marcha y cómo crear una tabla donde pudiéramos insertar datos. A continuación, aprendimos cómo insertar datos y luego consultar esos datos. Para mantener nuestro código limpio, también aprendimos a escribir una función contenedora que devolvía una Promesa de JavaScript, lo que nos permitía escribir llamadas asincrónicas a nuestra base de datos con facilidad.