Implementando TypeORM en NestJS

Implementando TypeORM en NestJS

NestJS es un framework que facilita (y mucho) el desarrollo con Node.js, ya que proporciona funcionalidades que, utilizando la arquitectura MSC, nos entregan fragmentos de código que requerirían un tiempo considerable, el cual puede ser reutilizado en aspectos más específicos del proyecto.

En mis últimas publicaciones (puedes consultar los artículos anteriores haciendo clic aquí), aprendimos bastante sobre NestJS, en lo que respecta a su estructura y cómo utilizarlo para realizar consultas mediante métodos HTTP (GET, POST, PATCH y DELETE). Hoy, daremos un paso más con el framework, usándolo junto con un ORM: TypeORM.

¿Vamos allá?

¿Qué es NestJS?

Hablando un poco más sobre NestJS, es un framework backend para Node.js que ofrece una colección de clases, funciones, tipos y patrones para facilitar el desarrollo de aplicaciones. Está escrito en TypeScript, un lenguaje derivado de JavaScript que añade tipado estático, proporcionando un código más seguro y menos propenso a errores. Con NestJS, es posible crear aplicaciones eficientes, escalables y confiables.

Sin más preámbulos, vamos a crear nuestro proyecto NestJS. En el directorio donde quieras que se ubique tu proyecto, abre un terminal y ejecuta los siguientes comandos:

nest new zoo-functions
cd zoo-functions
npm i class-validator class-transformer

Al ejecutar la línea de código anterior, el propio terminal te preguntará qué gestor de paquetes estamos utilizando. Nosotros usamos npm, pero puedes usar otros gestores si te sientes más cómodo. Si todo sale bien, al concluir la instalación y abrir el directorio del proyecto, verás una estructura similar a la de la imagen a continuación:

Figura 1 - Estructura básica de NestJS después de la creación del proyecto

Nuestro objetivo es continuar utilizando el proyecto que trabajamos en los artículos anteriores sobre NestJS, donde creamos un sistema de gestión de animales para un zoológico, donde es posible leer, insertar, actualizar y eliminar animales del registro. Las consultas se realizarán vía TypeORM (veremos más sobre esto más adelante), pero por ahora vamos a crear los archivos base del proyecto, basados en las estructuras de módulo, servicio y controlador. Para esto, ejecuta el siguiente comando:

nest g module animals
nest g service animals
nest g controller animals

NOTA - Si tienes dudas sobre la estructura mencionada, te recomendamos leer el artículo “Usando NestJS”, publicado antes de este. Ahí explicamos la función de cada estructura.

Una vez ejecutados los comandos anteriores, el proyecto debería tener una estructura parecida a esta:

Figura 2 - Estructura básica del directorio “animals”

Como ya creamos una estructura de consultas usando los métodos GET, POST, PATCH y DELETE, los aprovecharemos de los artículos anteriores, con la excepción de que nuestras consultas en la capa de servicio serán totalmente diferentes. Por ahora, mantendremos las consultas en la capa de servicio retornando valores nulos, los cuales reemplazaremos más adelante en este artículo con consultas usando TypeORM:

Figura 3 - Implementación de la capa Controller

Figura 4 - Implementación de la capa Service (sin consultas aún, ya que las realizaremos a lo largo de este artículo)

Figura 5 - DTO creada para la validación de los datos de la Solicitud que están llegando

¿Qué es un ORM?

El ORM (Object-Relational Mapping) es una técnica que permite mapear entidades de una base de datos a objetos directamente en el código. El objetivo es simplificar la interacción con la base de datos, proporcionando una capa de código más sencilla. Con un ORM, no es necesario escribir consultas SQL directamente, ya que la biblioteca se encarga de esa parte. Solo necesitas pasar el objeto JavaScript al ORM, y este inserta los datos en la base de datos.

Existen varios tipos de ORM, y utilizaremos TypeORM. Para instalarlo, ejecutaremos el siguiente comando en un terminal inicializado en la raíz de tu proyecto:

npm i typeorm --database mysql2 @nestjs/typeorm

NOTA - Como se puede ver en la línea de comando mencionada anteriormente, utilizaremos la base de datos MySQL para las consultas que se realizarán a través de TypeORM, por lo que es importante que MySQL esté instalado en tu máquina, o al menos ejecutándose de forma virtual. Una vez inicializado MySQL, estaremos listos para comenzar con las primeras implementaciones utilizando TypeORM. Como estamos usando la base de datos “zoo-functions” de los proyectos anteriores, es importante que esta base de datos esté creada en tu MySQL, aunque esté vacía. Ejecuta el siguiente comando para crearla, ya sea en Workbench (si lo usas) o en MySQL desde tu terminal:

CREATE SCHEMA `zoo-functions`;

Creando una Tabla

El primer paso para crear una tabla utilizando TypeORM es crear una "Migration" para ello. Una "Migration" es básicamente una forma de versionar el esquema de nuestra base de datos, donde cada una actúa como un historial de cambios realizados, como por ejemplo crear una tabla, modificar una columna, eliminar una relación, etc.

Cada "Migration" contiene dos conjuntos de código, conocidos como "Up" y "Down". La parte "Up" contiene las instrucciones para realizar los cambios en la base de datos, mientras que la parte "Down" contiene las instrucciones para revertir esos cambios. Esto significa que las "Migrations" permiten avanzar o retroceder el estado de la base de datos a cualquier estado anterior.

Como mencionamos antes, nuestra "Migration" creará una tabla en la base de datos Zoo-functions. Para hacerlo, simplemente ejecuta el siguiente comando en un terminal abierto en la carpeta raíz del proyecto:

npx typeorm migration:create src/migrations/createTables

Notarás que en la carpeta “src” de nuestro proyecto se creó un archivo con una secuencia de números, seguido de “-createTables.ts”. Este es el archivo que manipularemos para crear la tabla.

Figura 6 - Estructura inicial de la Migration cuando se crea

Como se explicó sobre las "Migrations", tenemos una función "up" y otra "down" para ejecutar los cambios y revertirlos en caso de ser necesario. En la función "up", crearemos la tabla y sus columnas utilizando la función "createTable", extraída de "queryRunner". Esta recibe como parámetros un "new Table", que a su vez recibe un objeto con el nombre de la tabla y las columnas existentes, junto con sus especificaciones. Veamos:

Figura 7 - Creación de la tabla mediante la función up - Parte 1

Figura 8 - Creación de la tabla mediante la función up - Parte 2

En las figuras 7 y 8, se puede observar que las columnas de la tabla se crean mediante una clave "columns" que recibe como valor una lista, donde cada ítem es una columna de la tabla. Cada ítem es un objeto donde podemos definir varias especificaciones de cada columna, siendo obligatorios al menos su nombre y su tipo. Estas son algunas de las otras claves importantes:

  • isPrimary - define cuáles columnas serán la clave primaria de la tabla;
  • generationStrategy - define cómo se generará el valor de un campo (en este caso, como se ve en la figura 8, lo usamos para el ID, con el propósito de realizar un incremento cada vez que se cree un nuevo elemento en la tabla);
  • isNullable - define si el campo puede o no recibir valores nulos;
  • length - define el límite del tamaño del campo de tipo texto.

Por último, en la función "down", utilizamos la función "dropTable", para el caso de que necesitemos revertir los cambios realizados:

Figura 9 - Implementación de la reversión de la creación de la tabla mediante la función down

Entidad

Creamos una entidad para definir la estructura de la tabla en nuestro código. A diferencia de la "Migration", que solo crea las columnas de la tabla, nuestra aplicación utilizará la entidad creada para validar si cada consulta cumple con los estándares de la estructura esperada de la tabla. Para esto, creamos un archivo con la extensión “.ts”, precedido por “.entity” (por convención). Luego, creamos una clase precedida por el decorador @Entity, que recibe como parámetro el nombre de la tabla.

Después, creamos cada una de las columnas usando los decoradores @Column(), si es una columna sin especificaciones, o @PrimaryGeneratedColumn(“uuid”) en el caso de una columna que será una clave primaria generada por un conjunto de caracteres aleatorios:

Figura 10 - Creación de la entidad Animals

Una vez que hemos terminado la implementación de la entidad, necesitamos agregarla al módulo al que pertenecerá. En imports, añadimos la siguiente expresión (importada de “@nestjs/typeorm”:

Figura 11 - Importando la entidad en el Módulo AnimalsModule

Conectando con la Base de Datos

¿Te diste cuenta de que aún no hemos hecho la conexión entre TypeORM y nuestra base de datos? Esto se debe a que preferí que creáramos la migración primero, para luego realizar esta acción, ya que la importación de la misma sería necesaria.

Para realizar esta conexión, utilizamos la función “forRoot”, ubicada en TypeOrmModule, importada de “@nestjs/typeorm”. Esta función recibe como parámetro la información principal tanto de la conexión a la base de datos como de las entidades y migraciones que utilizaremos:

Figura 12 - Definiendo la conexión de TypeORM con la base de datos

Después, crearemos un archivo llamado "DataSourceTable.ts", que usaremos para ejecutar la migración que creamos. Primero importamos DataSource, que viene del propio TypeORM. Este recibirá como parámetro un objeto con todas las especificaciones de la base de datos que estamos utilizando, de forma muy similar a los parámetros que pasamos en la función “forRoot”:

Figura 13 - Creando la conexión con la base de datos

Nuestra aplicación ya está lista para conectarse a la base de datos y ejecutar la migración. Primero, inicializa la aplicación con el siguiente comando:

npm run start:dev

Luego, ejecuta la migración con los comandos descritos a continuación:

npm run build
npx typeorm migration:run -d dist/migrations/DataSourceTable.js

¡Todo listo! Ya tenemos una tabla “animals” en la base de datos “zoo-functions” con todas las columnas que definimos en la migración.

Implementando la capa de Servicio

Para finalizar, basta con utilizar las funciones que TypeORM nos proporciona para realizar consultas. En lugar de escribir toda la consulta MySQL, todo será más fácil: primero, crearemos un constructor que recibirá un decorador @InjectRepository(), el cual recibe como parámetro la entidad “Animals” que creamos. Luego, creamos un atributo para esta entidad. Este atributo es el que usaremos para crear las consultas a través de TypeORM:

Figura 14 - Creación del constructor que recibirá la entidad Animals

Entre muchas funciones existentes, utilizaremos para las funciones getAnimals y insertAnimal la función “find” (que retorna todos los elementos de la tabla) y save (que guarda en la base de datos los datos recibidos):

Figura 15 - Implementación de las funciones getAnimals e insertAnimals

Finalmente, para las funciones updateAnimal y deleteAnimal, además de las funciones find (usada esta vez para buscar un dato específico en la tabla) y save ya explicadas, utilizamos la función preload para preparar la información, concatenando el ID que viene como parámetro con los demás campos de las columnas que vienen en el cuerpo (body). La función remove también se usa para eliminar un determinado elemento de la tabla:

Figura 16 - Implementación de las funciones updateAnimal y deleteAnimal

Consideraciones finales

En este artículo, aprendimos a utilizar TypeORM para realizar consultas de lectura, actualización, inserción y eliminación en una base de datos MySQL utilizando NestJS. En el próximo artículo, entenderemos cómo realizar relaciones entre tablas como One-to-One, One-to-Many, Many-to-One y Many-to-Many.

Si te pareció interesante este artículo, tienes alguna duda o simplemente quieres platicar sobre estos y otros temas, ¡puedes contactarme por correo a bruno.cabral.silva2018@gmail.com o en mi perfil de LinkedIn!

¡Te espero ansiosamente!

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.