Usando PostgreSQL con Node.js y node-postgres

PostgreSQL es una base de datos relacional gratuita, de código abierto y muy popular. El módulo node-postgres es un módulo ampliamente utilizado que une a Node con él. En este artículo, desarrollaremos una funcionalidad CRUD simple para una base de datos PostgreSQL.

Introducción

En este artículo, discutiremos cómo integrar PostgreSQL con Node.js.

Para seguir mejor este artículo, le recomendamos que tenga experiencia previa en el uso de Node.js y sentencias SQL. Usaremos la sintaxis simple de javascript ES6 en este artículo.

Hay algunos clientes diferentes que puede usar para integrar PostgreSQL con Node.js. En este artículo, usaremos el módulo node-postgres. Es un módulo popular y maduro en comparación con otros clientes de PostgreSQL.

Además, también puede usar PostgreSQL con un ORM como Secuela. Pero no usaremos un módulo ORM de este tipo en este artículo. En su lugar, utilizaremos consultas de SQL simple, que luego puede crear para interacciones de base de datos más complejas.

PostgreSQL

postgresql es una base de datos SQL popular. Ha estado en desarrollo activo durante los últimos 30 años y se considera una de las bases de datos relacionales más avanzadas que existen. PostgreSQL también es fácil de aprender y configurar en comparación con otras bases de datos relacionales disponibles. Debido a su naturaleza gratuita y de código abierto, esta es una opción popular entre las nuevas empresas.

PostgreSQL es una base de datos multiplataforma que se ejecuta en todos los principales sistemas operativos. Sin embargo, la configuración y la creación de acceso/base de datos difieren ligeramente entre los sistemas operativos.

En este artículo, usaremos Ubuntu 18.04, que es una plataforma Linux popular e incluye PostgreSQL de forma predeterminada. Algunos pasos pueden ser un poco diferentes si está utilizando un sistema operativo diferente.

Configuración del proyecto

Comencemos con un proyecto Node.js en blanco simple con la configuración predeterminada:

1
$ npm init -y

Luego, usemos npm para instalar el módulo node-postgres, que se usará para conectarse e interactuar con Postgres:

1
$ npm install --save pg

Implementación de operaciones CRUD

Con nuestro proyecto iniciado, avancemos y configuremos la base de datos. Después de eso, escribiremos algunas funciones CRUD básicas.

Configuración de la base de datos

Como con todas las bases de datos relacionales, comenzaremos creando una y conectándonos a ella. Puede usar la CLI o un cliente basado en GUI para hacer esto. Dado que es muy sencillo de configurar a través de la CLI, haremos precisamente eso.

Para Ubuntu, el comando psql predeterminado ingresará a la CLI. PostgreSQL creará un usuario llamado postgres para acceder a la base de datos en plataformas basadas en Linux. Por lo tanto, podemos usar el siguiente comando para iniciar sesión como usuario postgres:

1
$ sudo -i -u postgres

Luego ingrese a la CLI ejecutando:

1
$ psql

Debería ver un shell de comandos similar a este:

comando postgresql psql

Para ver las bases de datos actualmente presentes, usemos el comando \list o \l:

comando de lista postgresql

Y ahora, vamos a crear el nuestro usando una consulta SQL:

1
CREATE DATABASE testdb;

Al ejecutar este comando, estamos creando una base de datos testdb y recibimos el resultado, confirmando nuestro comando:

1
CREATE DATABASE

Dado que la base de datos está creada, ahora podemos acceder a ella. Si bien PostgreSQL crea un usuario postgres predeterminado, la contraseña no se establece de manera predeterminada. Si desea configurar su contraseña (en lugar de dejarla en blanco), use el comando \password:

cambiando la contraseña de postgresql usando el cli

Con su contraseña configurada, estamos listos para usar la base de datos en nuestro proyecto.

Conexión a la base de datos {#conexiónalabasede datos}

Tiene dos opciones para conectarse a un servidor PostgreSQL con el módulo node-postgres. Una de las opciones es usar un solo cliente. El otro método es utilizar un grupo de conexiones. Sin embargo, si su aplicación usa la base de datos con mucha frecuencia, el grupo será una mejor opción que usar un solo cliente.

La conexión a la base de datos usando el módulo node-postgres se puede hacer de dos maneras: usando un cliente único y usando un grupo de conexiones.

Veremos cómo usar un grupo de conexiones para conectarse a la base de datos más adelante en este artículo. Por el momento, conectémonos a la base de datos usando un solo cliente por brevedad y simplicidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { Client } = require('pg');

const client = new Client({
    user: 'postgres',
    host: 'localhost',
    database: 'testdb',
    password: '1234abcd',
    port: 5432,
});

client.connect();

Aquí, hemos configurado manualmente las opciones. Sin embargo, puede conectarse a la base de datos sin pasar ninguno de estos:

1
2
3
4
const { Client } = require('pg');

const client = new Client();
client.connect();

Pero, de nuevo, Node necesita saber cómo conectarse a la base de datos, por lo que los proporcionaremos a través de variables de entorno:

1
2
3
4
5
PGUSER=dbuser
PGHOST=database.server.com
PGPASSWORD=secretpassword
PGDATABASE=mydb
PGPORT=3211

Si no los ha configurado usted mismo, el módulo utilizará los valores predeterminados:

1
2
3
4
5
PGHOST='localhost'
PGUSER=process.env.USER
PGDATABASE=process.env.USER
PGPASSWORD=null
PGPORT=5432

En Linux, process.env.USER contendrá el valor del usuario actual que está conectado.

Creación de tablas

Con la base de datos preparada para la inserción de datos, creemos algunas tablas para almacenar nuestros datos. Al igual que con todas las bases de datos basadas en SQL, usaremos la consulta CREATE TABLE:

1
2
3
4
5
6
CREATE TABLE [table_name] (
    [column1] [datatype],
    [column2] [datatype],
    [column3] [datatype],
   ....
);

Una tabla consta de columnas y cada columna tiene un tipo de datos. Por ejemplo, una columna firstName tendría varchar como tipo de datos, que representa una cadena de tamaño variable.

Si desea leer más sobre los tipos de datos admitidos, la Documentación de PostgreSQL los enumera muy bien.

Dicho esto, podemos usar esta consulta para crear una tabla en la base de datos:

1
2
3
4
5
6
7
8
const query = `
CREATE TABLE users (
    email varchar,
    firstName varchar,
    lastName varchar,
    age int
);
`;

Para ejecutar esta consulta en la base de datos, usamos la función query() del objeto cliente que configuramos antes:

1
2
3
4
5
6
7
8
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Table is successfully created');
    client.end();
});

Nota: No olvide finalizar() su conexión con el cliente después de ejecutar la consulta.

Ejecutar este código creará nuestra tabla e imprimirá:

1
Table is successfully created

Esto también se puede lograr usando promesas y async/await. Dado que una llamada a la base de datos puede fallar, tiene más sentido usar promesas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
client
    .query(query)
    .then(res => {
        console.log('Table is successfully created');
    })
    .catch(err => {
        console.error(err);
    })
    .finally(() => {
        client.end();
    });

Como puede ver en el ejemplo, podemos usar el bloque finalmente para cerrar la conexión con la base de datos. Entonces, incluso si la consulta arroja un err, la conexión se cerrará.

Alternativamente, también podemos usar la sintaxis async/await:

1
2
3
4
5
6
7
8
try {
    const res = await client.query(query);
    console.log('Table is successfully created');
} catch (err) {
    console.log(err.stack);
} finally {
    client.close();
}

Todos estos enfoques deberían producir el mismo resultado:

1
Table is successfully created

Para verificar esto, usemos la interfaz de línea de comandos psql para inspeccionar la base de datos. Abra una terminal, inicie el shell con psql y seleccione la base de datos usando el comando \c [database]. \c es la abreviatura de \connect:

1
\c testdb

Luego puede listar las tablas en la base de datos testdb ejecutando el comando \dt:

tablas de lista de postgresql

También puede consultar tablas específicas proporcionando sus nombres:

1
testdb=# \dt FOO

Esta consulta mostrará la tabla denominada FOO.

Crear/Insertar datos

Podemos usar la instrucción SQL INSERT INTO para insertar datos en una tabla:

1
2
INSERT INTO [table_name] ([column1], [column2], [column3], ...)
VALUES ([value1], [value2], [value3], ...);

Para concretar esta consulta, insertemos nuestros propios valores y construyamos una consulta:

1
2
3
4
const query = `
INSERT INTO users (email, firstName, lastName, age)
VALUES ('[correo electrónico protegido]', 'john', 'doe', 21)
`;

Y finalmente, ejecutemos la consulta contra la base de datos:

1
2
3
4
5
6
7
8
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Data insert successful');
    client.end();
});

Nota: Al igual que la última vez, esta función se puede escribir usando la sintaxis async/await. Estos ejemplos adicionales se omiten por brevedad.

Ejecutar este código insertará un usuario en nuestra base de datos e imprimirá:

1
Data insert successful

Para verificar esto, en nuestra base de datos testdb, ejecute la instrucción SELECT:

1
SELECT * from users;

Obviamente, podemos ver que el usuario se creó con éxito:

postgresql mostrando los datos de la tabla

Recuperación/Selección de datos

Para recuperar datos de la base de datos, se utiliza la sentencia SELECT:

1
2
3
SELECT [column1], [column2], ...
FROM [table_name]
WHERE [condition];

Puede seleccionar columnas específicas especificándolas o seleccionar todos los campos de una tabla usando el comodín *. Opcionalmente, puede ser creativo con más condicionales usando la instrucción WHERE.

Aquí seleccionamos todas las filas y todas las columnas de la base de datos usuarios:

1
2
3
4
const query = `
SELECT *
FROM users
`;

Ahora, para ejecutar esta consulta en la base de datos, usaremos el cliente nuevamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    for (let row of res.rows) {
        console.log(row);
    }
    client.end();
});

Ejecutar este código producirá:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
email: '[correo electrónico protegido]',
firstname: 'john',
lastname: 'doe',
age: 21
}
{
email: '[correo electrónico protegido]',
firstname: 'anna',
lastname: 'dias',
age: 35
}

Esta consulta devuelve todos los usuarios agregados a la base de datos. También puede filtrar los usuarios por sus campos.

Por ejemplo, si quisiéramos devolver todos los usuarios menores de 30 años, agregaríamos una cláusula WHERE:

1
2
3
4
5
const query = `
SELECT *
FROM users
WHERE age<30
`;

Y luego, lo ejecutaríamos contra la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    for (let row of res.rows) {
        console.log(row);
    }
    client.end();
});

Ejecutar este código producirá:

1
2
3
4
5
6
{
email: '[correo electrónico protegido]',
firstname: 'john',
lastname: 'doe',
age: 21
}

Actualización de datos

Para actualizar datos que ya existen, podemos usar la sentencia UPDATE:

1
2
3
UPDATE [table_name]
SET [column1] = [value1], [column2] = [value2], ...
WHERE [condition];

Puede establecer cada valor actualizado para cada columna con la palabra clave SET. Después de la cláusula WHERE, puede definir la condición de qué entradas deben actualizarse.

Completemos nuestra consulta:

1
2
3
4
5
const query = `
UPDATE users
SET age = 22
WHERE email = '[correo electrónico protegido]'
`;

Ahora, ejecutemos la consulta contra la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    if (err) {
        console.error(err);
        return;
    }
    console.log('Data update successful');
    client.end();
});

Ejecutar este fragmento de código actualizará las entradas que satisfagan la cláusula WHERE e imprimirá:

1
Data update successful

Para verificar, revisemos nuestra base de datos:

postgresql mostrando datos de tabla

Eliminación de datos

Finalmente, para eliminar datos, podemos usar la sentencia DELETE:

1
2
DELETE FROM [table_name]
WHERE [condition];

Tenga cuidado con esta declaración, ya que podría eliminar accidentalmente más de lo que busca.

Completemos nuestra consulta:

1
2
3
4
const query = `
DELETE FROM users
WHERE email = '[correo electrónico protegido]'
`;

Y finalmente, ejecútelo contra la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
client.query(query, (err, res) => {
    if (err) {
        console.error(err);
        return;
    }
    if (err) {
        console.error(err);
        return;
    }
    console.log('Data delete successful');
    client.end();
});

Ejecutar este código eliminará la entrada que cumpla con la cláusula WHERE e imprimirá:

1
Data delete successful

Para verificar, echemos un vistazo a la base de datos:

postgresql mostrando los datos de la tabla

Agrupación

Si su aplicación usa la base de datos con frecuencia, usar una única conexión de cliente a la base de datos probablemente ralentizará la aplicación cuando tenga muchas solicitudes de usuarios. La forma más fácil y conveniente de abordar ese problema es usar un grupo de conexiones.

Por lo general, cuando un nuevo cliente se conecta a la base de datos, el proceso de establecer una conexión y autenticarse demora entre 20 y 30 milisegundos. Esto es importante cuando ejecuta más consultas que generan segundos de retraso, lo que probablemente terminará siendo una experiencia insatisfactoria para el usuario final.

Además, el servidor PostgreSQL solo puede manejar una cantidad limitada de clientes en un momento dado, lo que dependerá de la memoria de su servidor. Entonces, si se realizan 100 consultas en un segundo, esta limitación podría bloquear su servidor.

Además, el cliente puede procesar solo una solicitud a la vez para una sola conexión, lo que ralentiza aún más las cosas.

En una situación como esta, puede usar el módulo pg-pool para resolver eso.

Creación de un grupo

Primero importe la clase Pool desde el módulo pg:

1
const { Pool } = require('pg');

Luego, vamos a crear un nuevo objeto de grupo:

1
2
3
4
5
6
7
const pool = new Pool({
    user: 'postgres',
    host: 'localhost',
    database: 'testdb',
    password: '1234abcd',
    port: 5432,
});

Si no configura el nombre de usuario, el host y otras propiedades, tendrá que definir las variables de entorno para estos en un archivo de configuración. Es más o menos lo mismo que cuando se configura un solo cliente.

A continuación, definamos un controlador de errores para el grupo. Si hay algún error al lanzar desde el grupo, se activará la devolución de llamada en este evento:

1
2
3
pool.on('error', (err, client) => {
    console.error('Error:', err);
});

Esto nos cubre en caso de un error de red.

Luego, usando el objeto pool, nos conectamos a la base de datos y usamos un cliente en ese grupo para ejecutar una consulta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const query = `
SELECT *
FROM users
`;

pool.connect((err, client, done) => {
    if (err) throw err;
    client.query(query, (err, res) => {
        done();
        if (err) {
            console.log(err.stack);
        } else {
            for (let row of res.rows) {
                console.log(row);
            }
        }
    });
});

Esto debería producir:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  email: '[correo electrónico protegido]',
  firstname: 'john',
  lastname: 'doe',
  age: 21
}
{
  email: '[correo electrónico protegido]',
  firstname: 'anna',
  lastname: 'dias',
  age: 35
}

Nuevamente, tiene más sentido usar promesas en este caso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
pool.connect()
    .then((client) => {
        client.query(query)
            .then(res => {
                for (let row of res.rows) {
                    console.log(row);
                }
            })
            .catch(err => {
                console.error(err);
            });
    })
    .catch(err => {
        console.error(err);
    });

O incluso la sintaxis async/await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(async () => {
    try {
        const client = await pool.connect();
        const res = await client.query(query);

        for (let row of res.rows) {
            console.log(row);
        }
    } catch (err) {
        console.error(err);
    }
})();

Usar el cursor para leer consultas grandes

Por lo general, los datos recibidos de una consulta se cargan directamente en la memoria. Cuanto mayor sea el conjunto de datos, mayor será el uso de la memoria.

Entonces, cuando intenta consultar un gran conjunto de datos que puede contener decenas de miles de registros, es muy ineficiente cargar todo en la memoria y, a menudo, es simplemente imposible. Un cursor puede ayudarlo en una situación como esta al recuperar un número limitado de registros a la vez.

En cierto sentido, usar un cursor es similar a transmitir datos, ya que accederá a ellos secuencialmente en bloques más pequeños. Para usar el cursor, primero tenemos que instalar el módulo pg-cursor:

1
$ npm install --save pg pg-cursor

Pasaremos un nuevo Cursor a la función query(). El cursor no recuperará ninguna información hasta que especifiquemos el límite usando el método read():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const { Pool } = require('pg');
const Cursor = require('pg-cursor');

const pool = new Pool({
    user: 'postgres',
    host: 'localhost',
    database: 'testdb',
    password: '1234abcd',
    port: 5432,
});

(async () => {
    const client = await pool.connect();
    const query = 'SELECT * FROM users';

    const cursor = await client.query(new Cursor(query));

    cursor.read(1, (err, rows) => {
        console.log('We got the first row set');
        console.log(rows);

        cursor.read(1, (err, rows) => {
            console.log('This is the next row set');
            console.log(rows);
        });
    });
})();

El método read() del cursor nos permite definir cuántas filas queremos recuperar de la instancia actual del cursor. En este ejemplo, por simplicidad, hemos limitado las filas para un registro. Entonces hemos leído otro conjunto de filas después de eso.

Si ha llegado al final de las filas en la base de datos, la matriz filas tendrá una longitud 0.

Conclusión

PostgreSQL es una base de datos relacional gratuita, de código abierto y muy popular. El módulo node-postgres es un módulo desarrollado y ampliamente utilizado que une Node.js con PostgreSQL.

En este artículo, configuramos una base de datos PostgreSQL y desarrollamos la funcionalidad CRUD básica a través de un script simple de Node.js. Luego, exploramos la compatibilidad con la agrupación y el uso de cursores para limitar los datos recuperados.

Como siempre, el código fuente está disponible en GitHub.