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:
- Una base de datos para escribir datos a .
- 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:
date
que es una cadena de fecha ISO-8601 como2022-04-29T00:00:00.000Z
.title
que es el título de nuestra publicación como una cadena.author
que es el nombre del autor de la publicación como una cadena.content
cuál es el contenido de nuestra publicación como una cadena.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.