Skip to content

Latest commit

 

History

History
2402 lines (1718 loc) · 64.6 KB

File metadata and controls

2402 lines (1718 loc) · 64.6 KB

DESARROLLO WEB EN ENTORNO SERVIDOR

Tema 6: BASES DE DATOS

Utilización de técnicas de acceso a datos
BASES DE DATOS con ORM, PRISMA

PROYECTOS DE EJEMPLO



1. Introducción

En temas anteriores hemos trabajado un poco con bases de datos no relacionales, también llamadas noSQL. En concreto, en el tema 4, creamos una aplicación que proporcionaba una API REST hacia una base de datos documental como MongoDB. En adelante nos centraremos en las bases de datos relacionales.

En la primera parte de este tema trabajaremos con algunas de estas bases de datos relacionales de uso habitual mediante comandos SQL. En concreto usaremos las bases de datos MySQL/MariaDB, SQLite y Postgres.

En la segunda parte de este tema trabajaremos con las mismas bases de datos, pero haciendo uso de ORM (Object-Relational Mapping), que es una técnica que nos permite realizar un mapeo objeto-relacional y evitar así tener que trabajar con el SQL específico de cada base de datos. Usaremos el ORM Prisma.

En cualquier caso, siempre usaremos bases de datos del lado servidor.

Note

En el lado cliente también disponemos de almacenamiento gestionado por el navegador. Por ejemplo:

indexedDB

2. SQLite

SQLite es una biblioteca en proceso que implementa un motor de base de datos SQL transaccional , autónomo y sin configuración . El código de SQLite es de dominio público y, por lo tanto, se puede utilizar de forma gratuita para cualquier fin, comercial o privado.

SQLite es un motor de base de datos SQL integrado. A diferencia de la mayoría de las otras bases de datos SQL, SQLite no tiene un proceso de servidor separado. SQLite lee y escribe directamente en archivos de disco normales. Una base de datos SQL completa con múltiples tablas, índices, activadores y vistas está contenida en un único archivo de disco. El formato del archivo de la base de datos es multiplataforma: puede copiar libremente una base de datos entre sistemas de 32 y 64 bits o entre arquitecturas big-endian y little-endian .

Usaremos el driver sqlite3.

2.1. Proyecto

La estructura del proyecto es la siguiente:

Archivos de CRUD SQLite

El código fuente completo puede obtenerse desde el siguiente enlace:

Los archivos directamente relacionados con la Base de Datos, son:

  • src/database/db.js
  • src/lib/sqlite.js
  • src/lib/actions.js
// src/database/db.js
const sqlite3 = require("sqlite3").verbose();

const createTable = `CREATE TABLE IF NOT EXISTS articulos (
    id INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    descripcion TEXT,
    precio DECIMAL(10,2),
    createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`

// Connecting to or creating a new SQLite database file
const db = new sqlite3.Database(
  "./db.sqlite",
  sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
  (err) => { if (err) return console.error(err.message); console.log("Connected to the SQlite database."); }
);

// Serialize method ensures that database queries are executed sequentially
db.serialize(() => {
  // Create the items of table if it doesn't exist
  db.run(createTable,
    (err) => {
      if (err) {
        return console.error(err.message);
      }
      console.log("Created items table.");

    // ...
    }
  );
});
// src/lib/sqlite.js
import sqlite3 from "sqlite3";
import { open } from "sqlite";

let db = null;

if (!db) {
    // If the database instance is not initialized, open the database connection
    db = await open({
        filename: "./src/database/db.sqlite", // Specify the database file path
        driver: sqlite3.Database, // Specify the database driver (sqlite3 in this case)
    });
}

export default db;
'use server'
// src/lib/actions.js
import db from '@/lib/sqlite'


export async function getArticulos() {

    // ...
    const results = await db.all('select * from articulos');
    // ...

}

export async function createArticulo(formData) {

    // ...
    const query = 'insert into articulos(nombre,descripcion,precio) values (?, ?, ?)';
    const results = await db.run(query, [nombre, descripcion, precio]);
    // ...

}


export async function updateArticulo(formData) {

    // ...
    const query = 'update articulos set nombre = ?, descripcion = ?, precio = ? where id = ? ';
    const results = await db.run(query, [nombre, descripcion, precio, id]);
    // ...

}

export async function deleteArticulo(formData) {

    // ...
    const query = 'delete from articulos where id = ?';
    const results = await db.run(query, [id]);
    // ...

}

2.2. Otros aspectos

SQLite nos permite trabajar sin necesidad de instalar un SGBD, puesto que trabaja directamente con el archivo en disco. Por tanto, esta base de datos es muy adecuada cuando deseamos realizar pruebas sin la necesidad de instalar un sistema gestor de bases de datos.

3. MySQL

MySQL/MariaDB es un sistema gestor de bases de datos ampliamente usado hoy en día. Para trabajar con él los haremos con el driver mysql2 y usando un entorno de desarrollo local, es decir un servidor de base de datos en localhost:3306. El driver mysql2 es el más usado para trabajar con bases de datos de este tipo. Tiene varios millomes de descargas semanales según el sitio npmjs.com.

Este driver permite disponer de un pool (o grupo de conexiones) de conexiones, lo cual es muy útil cuando estamos trabajando en una aplicación web u otro software que realiza consultas frecuentes. Puedes consultar en https://sidorares.github.io/node-mysql2/docs#using-connection-pools.

3.1. Proyecto

La estructura del proyecto es la siguiente:

Archivos de CRUD MySQL

El código fuente completo puede obtenerse desde el siguiente enlace:

Los archivos directamente relacionados con la Base de Datos, son:

  • src/database/db.sql
  • src/lib/mysql.js
  • src/lib/actions.js
-- src/database/db.sql
CREATE TABLE articulos (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(200) NOT NULL,
    descripcion VARCHAR(200),
    precio DECIMAL(10,2),
    createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ALTER TABLE articulos ADD COLUMN imagen VARCHAR(200) AFTER descripcion;
// src/lib/mysql.js
import mysql from 'mysql2/promise';

// Para inicializar una conexión
export const db = await mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'root',
  port: 3306,
  database: 'test',
});

// Para inicializar un pool de conexiones
export const const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'root',
  port: 3306,
  database: 'test',

  waitForConnections: true,
  connectionLimit: 10,
  maxIdle: 10, // max idle connections, the default value is the same as `connectionLimit`
  idleTimeout: 60000, // idle connections timeout, in milliseconds, the default value 60000
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 0,
});
'use server'
// src/lib/actions.js
import { pool } from '@/lib/mysql'


export async function getArticulos() {

    const connection = await pool.getConnection();
    // ...
    const sql = 'select * from `articulos`';
    const [rows] = await connection.execute(sql);
    // ...
    connection.release();

}

export async function createArticulo(formData) {

    const connection = await pool.getConnection();
    // ...
    const sql = 'insert into `articulos` (`nombre`, `descripcion`, `precio`) values (?, ?, ?)'
    const values = [nombre, descripcion, precio];
  
    const [result, fields] = await connection.execute(sql, values)
    // ...
    connection.release();

}


export async function updateArticulo(formData) {

    const connection = await pool.getConnection();
    // ...
    const sql = 'update `articulos` set `nombre` = ?, `descripcion` = ?, `precio` = ? where `id` = ?'
    const values = [nombre, descripcion, precio, id];
  
    const [result, fields] = await connection.execute(sql, values)
    // ...
    connection.release();

}

export async function deleteArticulo(formData) {

    const connection = await pool.getConnection();
    // ...
    const sql = 'delete from articulos where id = ?'
    const values = [ id ]
  
    const [result, fields] = await connection.execute(sql, values);
    // ...
    connection.release();

}

El driver mysql2 permite 2 tipos de consultas:

  1. Consultas simples
  2. Sentencias preparadas

Si vamos a ejecutar muchas veces el mismo tipo de consulta, aunque sea con datos distintos, nos interesará usar sentencias preparadas, puesto que se almacenará en caché para un mejor rendimiento.

Para mayor información acerca de como realizar consultas preparadas, dispones de la siguiente documentación:

Por otro lado, hemos usado conexiones obtenidas de un pool, por lo cual necesitamos las sentencias:

import { pool } from '@/lib/mysql'
// ...  

const connection = await pool.getConnection();

// query

connection.release();

Si no hacemos uso del pool, bastaría con hacer

import { db } from '@/lib/mysql'
// ...

const [rows, fields] = await db.query(sql);

3.2. Otros aspectos

Debemos resaltar que el código anterior es interesante por los siguientes motivos:

  • No sólo es posible acceder a la base de datos desde server actions. También es posible hacerlo desde otros componentes del servidor como, por ejemplo, src/app/articulos/update/page.js y src/app/articulos/delete/page.js
  • El componente src/components/Articulo.js acepta propiedades, entre ellas el famoso children, que nos permite incrustar contenido JSX.
  • El componente src/components/Form.js también acepta propiedades pero, en este caso, lo interesante es observar como el contenido JSX difiere del HTML tradicional.

src/components/Articulo.js

Dentro del JSX, donde aparece {children}, podremos insertar botones a la hora de usar el componente Articulo.

// src/components/Articulo.js
function Articulo({ children, articulo }) {
    return (
        <div style={{ 'border': '1px solid lightgrey', 'padding': '50px' }}>
            <p><strong>{articulo.nombre}</strong></p>
            <p>{articulo.descripcion}</p>
            <p>{articulo.precio}</p>
            {children}
        </div>
    )
}

export default Articulo

src/components/Form.js

Cuando trabajamos con JSX es frecuente olvidar que no se trata de código HTML. Los siguientes atributos JSX son distintos a los usados en HTML:

Etiqueta atributo HTML atributo JSX
label for htmlFor
input autofocus autoFocus
input value defaultValue
input checked defaultChecked
// src/components/Form.js
function Form({ action, title, articulo, disabled }) {

    return (
        <form action={action} >
            <input type='hidden' name='id' value={articulo?.id} />
            <fieldset disabled={disabled}>
                <label htmlFor='nombre'>Nombre</label>
                <input type='text' id='nombre' name='nombre'
                    placeholder='Nombre'
                    defaultValue={articulo?.nombre} autoFocus ></input>
                <label htmlFor='descripcion'>Descripción</label>
                <input type='text' id='descripcion' name='descripcion'
                    placeholder='Descripción'
                    defaultValue={articulo?.descripcion} />
                <label htmlFor='precio'>Precio</label>
                <input type='number' id='precio' name='precio' min='0' step={0.01}
                    placeholder='precio'
                    defaultValue={articulo?.precio} />
            </fieldset>
            <button type='submit'>{title}</button>
        </form>
    )
}

export default Form

4. MySQL (Serverless)

MySQL/MariaDB es un sistema gestor de bases de datos ampliamente usado hoy en día. Para trabajar con él los haremos con el driver serverless-mysql y usando un entorno de desarrollo local, es decir un servidor de base de datos en localhost:3306.

4.1. Proyecto

La estructura del proyecto es la siguiente:

Archivos de CRUD MySQL

El código fuente completo puede obtenerse desde el siguiente enlace:

Los archivos directamente relacionados con la Base de Datos, son:

  • src/database/db.sql
  • src/lib/mysql.js
  • src/lib/actions.js
-- src/database/db.sql
CREATE TABLE articulos (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(200) NOT NULL,
    descripcion VARCHAR(200),
    precio DECIMAL(10,2),
    createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- ALTER TABLE articulos ADD COLUMN imagen VARCHAR(200) AFTER descripcion;
// src/lib/mysql.js
import mysql from 'serverless-mysql'

Podemos inicializar la conexión de esta manera
export const db = mysql({
    config: {
        host: 'localhost',
        user: 'root',
        password: 'root',
        port: 3306,
        database: 'crud'
    }
})

// También podemos inicializar la conexión de esta otra manera
// export const db = mysql('mysql://root:root@localhost:3306/crud')
'use server'
// src/lib/actions.js
import { db } from '@/lib/mysql'


export async function getArticulos() {

    // ...
    const results = await db.query('select * from articulos');
    // ...

}

export async function createArticulo(formData) {

    // ...
    const query = 'insert into articulos(nombre,descripcion,precio) values (?, ?, ?)';
    const results = await db.query(query, [nombre, descripcion, precio]);
    // ...

}


export async function updateArticulo(formData) {

    // ...
    const query = 'update articulos set ? where id = ? ';
    const results = await db.query(query, [{nombre, descripcion, precio}, id]);
    // ...

}

export async function deleteArticulo(formData) {

    // ...
    const query = 'delete from articulos where id = ?';
    const results = await db.query(query, [id]);
    // ...

}

4.2. Otros aspectos

El proyecto anterior, aunque simple, es muy adecuado desde un punto de vista didáctico, pues no sólo se muestra como trabajar con MySQL, sino que lo hace con un driver que permite acceso a base de datos serverless, lo cual es cada día más habitual. Por ejemplo, desde PlanetScale podemos leer lo siguiente:

PlanetScale is a MySQL-compatible serverless database that brings you scale, performance, and reliability — without sacrificing developer experience.

5. Postgres

Postgres es un sistema gestor de bases de datos que está ganando bastante aceptación últimamente. Esto es debido principalmente a numerosos factores:

  • Es software de código abierto.
  • Uso gratuito, lo que la convierte en una opción rentable para muchas organizaciones.
  • Sigue de forma bastante fidedigna el estándar SQL.
  • Ofrece amplia funcionalidad: transacciones, lenguaje procedimental, ...
  • Existe bastante documentación.
  • Cada vez existen más proveedores en la nube de este DBaaS.

Para trabajar con él los haremos con el driver pg y usando un entorno de desarrollo local, es decir un servidor de base de datos en localhost:5432.

5.1. Proyecto

La estructura del proyecto es la siguiente:

Archivos de CRUD MySQL

El código fuente completo puede obtenerse desde el siguiente enlace:

Los archivos directamente relacionados con la Base de Datos, son:

  • src/database/config.mjs
  • src/database/seed.mjs
  • src/lib/postgres.js
  • src/lib/actions.js
// src/database/config.mjs
const config = {
    user: 'postgres',
    password: 'postgres',
    host: 'localhost',
    port: 5432,
    database: 'postgres',
}

export default config
// src/database/seed.mjs
import pg from 'pg'
import config from './config.mjs'

const { Client } = pg
const client = new Client(config)


const load = async () => {
    try {
        await client.connect()

        let result = await client.query(`
        CREATE TABLE IF NOT EXISTS articulos (
            id SERIAL PRIMARY KEY,
            nombre TEXT NOT NULL,
            descripcion TEXT,
            precio DECIMAL(10,2)
            );
        `)
        console.log("Creada tabla artículos");

        result = await client.query(`
        INSERT INTO articulos (nombre, descripcion, precio)
        VALUES 
           ('PC', 'Ordenador de sobremesa', 999.99),
           ('Impresora', 'Impresora Epson', 55.99),
           ('Teclado', 'Teclado USB', 19.91);
        `)
        console.log("Insertados varios artículos");

    } catch (error) {
        console.log(error);
    } finally {
        await client.end()
    }
}

load()
// src/lib/postgres.js
import pg from 'pg'
import config from '../database/config.mjs'

const { Pool } = pg

export const pool = new Pool(config)

/*
// OTRA FORMA DE CREAR EL POOL
import pg from 'pg';

const { Pool } = pg;

export const pool = new Pool({
  connectionString: "postgres://usuario:contraseña@host:5432/basedatos?sslmode=require"
})
'use server'
// src/lib/actions.js
import { pool } from '@/lib/postgres'


export async function getArticulos() {

    // ...
    const results = await pool.query('select * from articulos');
    // ...

}

export async function createArticulo(formData) {

    // ...
    const query = 'insert into articulos(nombre,descripcion,precio) values ($1, $2, $3)';
    const results = await pool.query(query, [nombre, descripcion, precio]);
    // ...

}


export async function updateArticulo(formData) {

    // ...
    const query = 'update articulos set nombre=$1, descripcion=$2, precio=$3 where id=$4';
    const results = await pool.query(query, [nombre, descripcion, precio, id]);
    // ...

}

export async function deleteArticulo(formData) {

    // ...
    const query = 'delete from articulos where id=$1';
    const results = await pool.query(query, [id]);
    // ...

}

5.2. Otros aspectos

El driver pg es uno de los más descargados, con más de 5M de descargas semanales. Este driver permite disponer de un pool (o grupo de conexiones) de conexiones, lo cual es muy útil cuando estamos trabajando en una aplicación web u otro software que realiza consultas frecuentes.

Otro driver similar es postgres, aunque con muchas menos descargas.

6. Postgres (Vercel)

A diferencia del apartado anterior, en el que hemos trabajado con Postgres en un entorno local, en este apartado usaremos Postgres en la nube.

Aunque usaremos la base de datos Postgres proporcionada por Vercel, también disponemos de otras como Neon.tech o Supabase

Para trabajar con él los haremos con el driver @vercel/postgres y usando el DBaaS proporcionado por Vercel.

6.1. Proyecto

La estructura del proyecto es la siguiente:

Archivos de CRUD Postgres

El código fuente completo puede obtenerse desde el siguiente enlace:

Los archivos directamente relacionados con la Base de Datos, son:

  • .env
  • src/database/seed.mjs
  • src/lib/actions.js

El driver @vercel/postgres trabaja con la variable de entorno POSTGRES_URL, por lo tanto es importante que la indiquemos en el archivo .env, en este caso lo haremos en 2 sitios con el mismo formato:

POSTGRES_URL="postgres://usuario:password@host:5432/basedatos"
// src/database/seed.mjs
import { sql } from '@vercel/postgres';


const load = async () => {
    try {
        let result = await sql`
        CREATE TABLE IF NOT EXISTS articulos (
            id SERIAL PRIMARY KEY,
            nombre TEXT NOT NULL,
            descripcion TEXT,
            precio DECIMAL(10,2)
            );
        `;
        console.log("Creada tabla artículos");

        result = await sql`
        INSERT INTO articulos (nombre, descripcion, precio)
        VALUES 
           ('PC', 'Ordenador de sobremesa', 999.99),
           ('Impresora', 'Impresora Epson', 55.99),
           ('Teclado', 'Teclado USB', 19.91);
        `;
        console.log("Insertados varios artículos");

        await sql.end()

    } catch (error) {
        console.log(error);
    }
}

load();
'use server'
// src/lib/actions.js
import { sql } from '@vercel/postgres';


export async function getArticulos() {

    // ...
    const { rows } = await sql`select * from articulos;`
    // ...

}

export async function createArticulo(formData) {

    // ...
    const results = await sql`
    insert into articulos(nombre,descripcion,precio) values (${nombre}, ${descripcion}, ${precio});
    `
    // ...

}


export async function updateArticulo(formData) {

    // ...
    const results = await sql` 
    update articulos set nombre=${nombre}, descripcion=${descripcion}, precio=${precio} where id = ${id};
    `
    // ...

}

export async function deleteArticulo(formData) {

    // ...
    const results = await sql`delete from articulos where id = ${id};`
    // ...

}

6.2. Otros aspectos

Por supuesto, un prerrequisito para que todo ello funcione es tener creada una base de datos en Vercel. Puedes hacerlo desde tu dashboard en Add New..., Storage.

Vercel Create Storage

Luego pulsaremos en el botón Create Database.

Vercel Create Database

A la hora de desplegar en Vercel la aplicación deberemos configurar las variables de entorno, en este caso POSTGRES_URL.

Vercel Create Env var

Note

En realidad vercel subcontrata la BD a NeonDB. Proveedores principales de DBaaS Postgres son:

Tip

Evolución DBaaS -> BaaS

Algunos DBaaS han evolucionado rápidamente para convertirse en BaaS. Es el caso de Supabase, que proporciona una serie de servicios backend listos para usar, de modo que puedas desarrollar aplicaciones sin tener que construir y administrar toda la infraestructura desde cero. Entre sus características principales están:

  • Base de datos basada en PostgreSQL.
  • Autenticación de usuarios (email, OAuth, etc.).
  • API REST y GraphQL generadas automáticamente.
  • Almacenamiento de archivos (Storage).
  • Funciones serverless (Edge Functions).
  • Suscripciones en tiempo real (Realtime).

También suele clasificarse como una plataforma de desarrollo full-stack backend o una alternativa de código abierto a Firebase, aunque a diferencia de Firebase está centrada en PostgreSQL.

Otra ventaja de Supabase es que al ser open source puedes instalarla en tu propio servidor, con lo cual tienes tu propio backend con las funcionalidades indicadas anteriormente. Para automatizar el proceso puedes consultar este proyecto para realizar autohospedaje automatizado.

En este artículo puedes comprobar lo simple y potente que es la implementación de autenticación en una aplicación frontend de React.

Similar a Supabase, NeonDB está siguiendo el mismo camino. Esta evolución permite, en muchos casos, desarrollar una aplicación web implementando únicamente el frontend y usando como backend el BaaS.

7. Primeros pasos con ORM Prisma

Un ORM, o Object Relational Mapper, es una pieza de software diseñada para traducir entre las representaciones de datos utilizadas por las bases de datos y las utilizadas en la programación orientada a objetos.

Desde la perspectiva de un desarrollador, un ORM le permite trabajar con datos respaldados por bases de datos utilizando las mismas estructuras y mecanismos orientados a objetos que usaría para cualquier tipo de datos internos. En general, los ORM sirven como una capa de abstracción entre la aplicación y la base de datos.

Cada lenguaje/framework tiene su propio ORM. A continuación se muestran los más conocidos:

  • Doctrine (PHP/Symfony)
  • Eloquent (PHP/Laravel)
  • JPA (Java)
  • Hibernate (Java/Spring)
  • Sequelize (Node.js)
  • Prisma (Node.js)

En este tema veremos el ORM Prisma, disponible para Javascript/Typescript y que soporta las siguientes bases de datos:

  • PostgreSQL
  • MySQL
  • SQLite
  • SQL Server
  • MongoDB
  • CockroachDB

En general, las tareas básicas a la hora de gestionar la persistencia de datos son tres:

  1. Crear la base de datos: reserva de espacio en un DBaaS o similar.
  2. Migrar (migrate): creación de tablas.
  3. Sembrar (seed): inserción de datos iniciales.

Las dos primeras tareas son obligatorias. La tercera tarea es opcional.

7.1. Instalación del paquete prisma

Caution

Trabajaremos con la versión 7 de Prisma. La versión 8 está a punto de ser publicada pero hay bastantes cambios respecto a la versión anterior y aún es muy reciente para su uso en producción. Trabajaremos con base de datos Postgresql.

npm  install  prisma@7  -D
npm  install  @prisma/client  @prisma/adapter-pg

Note

Al instalar el paquete prisma también se instala el paquete dotenv, necesario para leer archivo .env. Al instalar el paquete adapter-pg también se instala el paquete pg que es el driver para trabajar con Postgresql.

7.2. Comandos disponibles

npx prisma

7.3. Inicialización

npx prisma init

Note

Podemos indicar el proveedor de datos en la inicialización. Por ejemplo:

npx prisma init --datasource-provider postgresql
npx prisma init --datasource-provider mysql
npx prisma init --datasource-provider sqlite

Este comando hace 3 cosas:

  1. crea un nuevo directorio y archivo llamados prima/schema.prisma, que contiene el esquema de Prisma con la variable de conexión de su base de datos.
  2. crea un nuevo archivo llamado prisma.config.ts que contiene una configuración básica de prisma.
  3. añade al archivo .env en el directorio raíz del proyecto la variable de entorno DATABASE_URL, que debeás posteriormente editar manualmente para apuntar a tu base de datos.

prisma/schema.prisma

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

Como provider del datasource tenemos los siguientes:

  • sqlite
  • postgresql
  • mysql
  • sqlserver
  • mongodb
  • cockroachdb

.env

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

Note

El formato de la variable de entorno DATABASE_URL es el siguiente:

DATABASE_URL='<provider>://<user>:<pass>@<host>:<port>/<db>'

7.4. Generación del cliente

El cliente es el código que nos permite conectar y gestionar la conexión a la base de datos.

Cada vez que cambie la variable DATABASE_URL deberemos generar el cliente con el comando:

npx  prisma  generate

De esta manera se nos creará una carpeta src/generated/prisma con varios archivos en su interior, uno de ellos llamado src/generated/prisma/client.ts

8. Definiendo el Esquema de Datos

Hay dos formas alternativas de definir un esquema de datos:

  1. Escribir el esquema de datos manualmente y usar Prisma Migrate: puedes escribir tu esquema de datos manualmente y asignarlo a tu base de datos usando Prisma Migrate. En este caso, el esquema de datos es la única fuente de verdad para los modelos de tu aplicación.

  2. Generar el esquema de datos mediante introspección: cuando tienes una base de datos existente o prefieres migrar el esquema de tu base de datos con SQL, genera el esquema de datos mediante una introspección de tu base de datos. En este caso, el esquema de la base de datos es la única fuente de verdad para los modelos de tu aplicación.

Nosotros usaremos la primera forma, aunque se explica la segunda forma de manera somera más adelante.

8.1. Escribir el esquema de datos manualmente

En este caso, tenemos una base de datos totalmente vacía, sin tablas creadas previamente. Para generar el esquema desde cero, editamos el archivo prisma/schema.prisma para añadir los modelos deseados. Una vez hecho lo anterior ejecutamos:

npx prisma migrate dev   

Si no ha habido cambios se mostrará un mensaje similar al siguiente:

prisma migrate sin cambios

Si ha habido algún cambio al esquema, entonces nos solicitará un nombre para la migración:

prisma migrate con cambios

Tip

Una operación muchos más cómoda y directa, es hacer:

npx prisma db push

8.1.1. Modelos

Los modelos son cada una de las partes de las que se compone un esquema. Los modelos:

  • Representan las entidades del dominio de aplicación.

  • Se asignan a las tablas (bases de datos relacionales como PostgreSQL) o colecciones (MongoDB) de la base de datos.

  • Modelos en Prisma

Reglas de nombrado para Modelos:

  • Los nombres de los modelos deben cumplir con la siguiente expresión regular: [A-Za-z][A-Za-z0-9_]*
  • Los nombres de los modelos deben comenzar con una letra y normalmente se escriben en PascalCase
  • Los nombres de los modelos deben usar la forma singular (por ejemplo, Usuario en lugar de usuario, usuarios o Usuarios)

Note

Puede utilizar el atributo @@map para asignar un modelo (por ejemplo, Usuario) a una tabla con un nombre diferente que no coincide con las convenciones de nomenclatura del modelo (por ejemplo, usuarios).

Reglas de nombrado para Campos

Reglas de nombrado:

  • Debe cumplir con la siguiente expresión regular: [A-Za-z][A-Za-z0-9_]*
  • Debe comenzar con una letra y normalmente se escriben en camelCase

Note

Puede utilizar el atributo @map para asignar un nombre de campo a una columna con un nombre diferente que no coincida con las convenciones de nomenclatura de campos: p. ej. miCampo @map("mi_campo").

Ejemplo:

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Articulo {
  id          Int       @id @default(autoincrement())
  nombre      String
  descripcion String?
  precio      Decimal?  

  @@map("articulos")
}

Note

El signo ? significa que el valor no es requerido, es decir admite NULL.

Tipos de datos

Prisma define los siguientes tipos de datos:

Estos tipos de datos son mapeados a los tipos nativos de cada base de datos según se muestra en la siguiente tabla:

Prisma String Boolean Int BigInt Float Decimal DateTime Json
PostgreSQL text boolean integer bigint double precision decimal(65,30) timestamp(3) jsonb
SQL Server nvarchar(1000) tinyint int int float(53) decimal(32,16) datetime2 Not supported
MySQL varchar(191) TINYINT(1) INT BIGINT DOUBLE DECIMAL(65,30) DATETIME(3) JSON
MongoDB String Bool Int Long Double Not supported Timestamp A valid BSON object (Relaxed mode)
SQLite TEXT INTEGER INTEGER INTEGER REAL DECIMAL NUMERIC Not supported
CockroachDB STRING BOOL INT INTEGER DOUBLE PRECISION DECIMAL TIMESTAMP JSONB

8.1.2. Relaciones

Esquema de ejemplo

Uno a Uno

model User {
  id      Int      @id @default(autoincrement())

  profile Profile?
}


model Profile {
  id     Int  @id @default(autoincrement())

  user   User @relation(fields: [userId], references: [id])
  userId Int  @unique  // campo escalar (usado en atributo `@relation`)
}

Uno a Muchos

model User {
  id    Int    @id @default(autoincrement())

  posts Post[]
}


model Post {
  id       Int  @id @default(autoincrement())

  author   User @relation(fields: [authorId], references: [id])
  authorId Int  // campo escalar (usado en atributo `@relation`)
}

Una relación uno-muchos puede ser opcional.

En el siguiente ejemplo, se permite crear un Post sin asignar un User. Observar el signo ? en los 2 últimos campos.

model User {
  id    Int    @id @default(autoincrement())

  posts Post[]
}


model Post {
  id       Int   @id @default(autoincrement())

  author   User? @relation(fields: [authorId], references: [id])
  authorId Int?
}

Muchos a Muchos

model Post {
  id         Int       @id @default(autoincrement())
  title      String

  categories CategoriesOnPosts[]
}


model Category {
  id    Int            @id @default(autoincrement())
  name  String

  posts CategoriesOnPosts[]
}


model CategoriesOnPosts {
  assignedAt DateTime @default(now())
  assignedBy String

  post       Post     @relation(fields: [postId], references: [id])
  postId     Int     // campo escalar (usado en atributo `@relation`)

  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int     // campo escalar (usado en atributo `@relation`)

  @@id([postId, categoryId])
}

Si en la tabla intermedia no tenemos campos propios, Prisma nos permite simplificar el esquema, que quedaría así:

model Post {
  id         Int         @id @default(autoincrement())
  title      String

  categories Category[]
}


model Category {
  id    Int              @id @default(autoincrement())
  name  String

  posts Post[]
}

Esto se conoce como relación implícita de muchos a muchos. Esta relación todavía se manifiesta en una tabla de relaciones en la base de datos subyacente. Sin embargo, Prisma gestiona esta tabla de relaciones.

8.1.3. Sincronizando el esquema con la base de datos

Siempre que actualices tu esquema Prisma, deberás actualizar el esquema de tu base de datos utilizando npx prisma migrate dev o npx prisma db push. Esto mantendrá el esquema de tu base de datos sincronizado con tu esquema Prisma. Los comandos también regenerarán Prisma Client.

Para ello ejecutaremos:

npx prisma migrate dev --name nombremigracion

o, en su lugar, ejecutaremos:

npx prisma db push

Important

La operación npx prisma db push eliminará todas las tablas previas en la base de datos que no aparezcan registradas en prisma/schema.prisma.

8.2. Generar el esquema de datos mediante introspección

En el caso de que dispongamos de tablas previamente creadas en la base de datos y deseemos mantener la información, podemos generar el esquema a partir de dichas tablas. Para ello ejecutamos:

npx prisma db pull    
npx prisma generate

Important

La operación npx prisma db pull borra el esquema previo de prisma/schema.prisma.

prisma db pull

prisma generate

9. Consultas

9.1. Consultas CRUD

CRUD es el acrónimo para:

  • Create
  • Read
  • Update
  • Delete

Estas son las 4 operaciones básicas necesarias para la gestión de información.

A continuación, usaremos ejemplos con valores explícitos. Usaremos el siguiente esquema:

diagrama ER

Esquema de prisma (Pulsa aquí para ver)

generator client { provider = "prisma-client" output = "../src/generated/prisma" }

datasource db { provider = "postgresql" }

model User { id Int @id name String? email String @unique password String age Int? country String? profileViews Int role Role @default(USER) coinflips Boolean[] posts Post[] profile Profile? }

model Post { id Int @id title String published Boolean @default(true) author User @relation(fields: [authorId], references: [id]) authorId Int }

model Profile { id Int @id biography String user User @relation(fields: [userId], references: [id]) userId Int @unique }

enum Role { USER ADMIN }

Tip

Para generar diagramas ER a partir del esquema de prisma (schema.prisma) puedes usar la herramienta https://prisma-erd.simonknott.de/

9.1.1. Create

const user = await prisma.user.create({
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
  },
})

Insertar user y algunos posts asociados

const user = await prisma.user.create({
  data: {
    email: 'ariadne@prisma.io',
    name: 'Ariadne',
    posts: {
      create: [
        {
          title: 'My first day at Prisma',
        },
        {
          title: 'How to connect to a SQLite database',          
        },
      ],
    },
  },
})

9.1.2. Read

Encontrar un registro por ID

const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
})

Encontrar el primer registro por nombre

const user = await prisma.user.findFirst({
  where: {
    name: 'Lenny',
  },
})

Encontrar todos los registros

const users = await prisma.user.findMany()
const users = await prisma.user.findMany({}) // equivalente a la anterior

Estructura general de consultas find

Note

Cada una de las siguentes propiedades es opcional y puede colocarse en cualquier orden.

Podemos usar esta forma

const users = await prisma.user.findMany({
  select: { /*...*/},
  where: { /*...*/},
  orderBy: {/*...*/},
  skip: /*...*/,         // corresponde al OFFSET de SQL
  take: /*...*/,         // corresponde al LIMIT de SQL
})

o, también podemos usar

const users = await prisma.user.findMany({
  include: { /*...*/},  // corresponde al JOIN de SQL
  where: { /*...*/},
  orderBy: {/*...*/},
  skip: /*...*/,         // corresponde al OFFSET de SQL
  take: /*...*/,         // corresponde al LIMIT de SQL
})

Important

NO SE PERMITE USAR select E include A LA VEZ EN EL MISMO NIVEL.

INCORRECTO

ko - skip & take

CORRECTO

ok - skip & take

Ejemplo

const getUser = await prisma.user.findMany({
  select: {
    email: true,
    name: true,
    age: true,
    posts: true,     // todos los campos de posts
    // posts: {
    //   select: {   // sólo algunos campos de posts
    //     id: true,
    //     title: true,
    //     published: true,
    //   },
    // }
  },
  where: {
    name: {
      not: 'Lenny',
      contains: 'le',  
      // startsWith: 'le',
      // endsWith: 'le',
      mode: 'insensitive',  // no diferencia entre mayúsculas y minúsculas
    },
    age: {
      gt: 30,     // gte, lt, lte, not
    }
  },
  orderBy: {
    age: 'desc',  //  'asc'
  },
  skip: 100,      // ignoramos los primeros 100 registros
  take: 25,       // obtenenos sólo 25 registros 
})

Si deseamos obtener todos los campos de user y todos los campos de posts, podemos simplificar la consulta

const getUser = await prisma.user.findMany({
  include: { 
    posts: true,
  }
  where: {
    name: {
      not: 'Lenny',
      contains: 'le',  
      // startsWith: 'le',
      // endsWith: 'le',
      mode: 'insensitive',  // no diferencia entre mayúsculas y minúsculas
    },
    age: {
      gt: 30,     // gte, lt, lte, not
    }
  },
  orderBy: {
    age: 'desc',  //  'asc'
  },
  skip: 100,      // ignoramos los primeros 100 registros
  take: 25,       // obtenenos sólo 25 registros 
})

skip & take

Note

La documentación referida a operadores y condiciones de filtrado está accesible en

https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators

9.1.3. Update

Actualizar un registro por ID

La forma de esta consulta es muy similar a create. La única diferencia, aparte del uso de update, es que es necesario usar una propiedad where.

const user = await prisma.user.update({
  where: {
    id: 1,
  }
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
  },
})

9.1.4. Delete

Elimnar un registro por ID

Esta es una de las consultas más sencillas de expresar.

const deleteUser = await prisma.user.delete({
  where: {
    id: 1,
  },
})

9.2. Otras consultas

9.2.1. count

Usaremos count() para contar la cantidad de registros o valores de campos no nulos. La siguiente consulta de ejemplo cuenta todos los usuarios:

const userCount = await prisma.user.count()

Para contar cuantos usuarios tienen el campo profileViews igual o superior a 100.

const userCount = await prisma.user.count({
  where: {
    profileViews: {
      gte: 100,
    },
  },
})

9.2.2. groupBy

El uso de groupBy() nos permite agrupar registros por uno o más valores de campo, como país o país y ciudad, y realizar agregaciones en cada grupo, como encontrar la edad promedio de las personas que viven en una ciudad en particular.

Para agregar valores usaremos:

  • _count
  • _sum
  • _avg
  • _min
  • _max
const groupUsers = await prisma.user.groupBy({
  by: ['country'],
  where: {
    country: {
      notIn: ['Sweden', 'Ghana'],
    },
  },
  _sum: {
    profileViews: true,
  },
  having: {
    profileViews: {
      _min: {
        gte: 10,
      },
    },
  },
})

Documentación disponible en:

9.3. Consultas en paralelo

Cuando tenemos que realizar múltiples consultas, y éstas son independientes unas de otras, podemos usar Promise.all o Promise.allSettled para mejorar el rendimiento.

  // Obtener datos en paralelo.
  // Las consultas se envían a la base de datos prácticamente al mismo tiempo.
  const [totalLibros, totalAutores, totalSocios, prestamosActivos, ultimosPrestamos] = await Promise.all([
    prisma.libro.count(),
    prisma.autor.count(),
    prisma.socio.count(),
    prisma.prestamo.count({ where: { devueltoEn: null } }),
    prisma.prestamo.findMany({
      where: { devueltoEn: null },
      include: {
        libro: { select: { titulo: true } },
        socio: { select: { nombre: true } },
      },
      orderBy: { prestadoEn: "desc" },
      take: 5,
    }),
  ]);
// Obtener datos en serie. 
// La segunda consulta no comienza hasta que termina la primera,
// y la tercera no comienza hasta que termina la segunda, y así sucesivamente.
const totalLibros = await prisma.libro.count();
const totalAutores = await prisma.autor.count();
const totalSocios = await prisma.socio.count();
const prestamosActivos =  prisma.prestamo.count({ where: { devueltoEn: null } });
const ultimosPrestamos = prisma.prestamo.findMany({
      where: { devueltoEn: null },
      include: {
        libro: { select: { titulo: true } },
        socio: { select: { nombre: true } },
      },
      orderBy: { prestadoEn: "desc" },
      take: 5,
  });

10. Ver datos de las tablas

Ejecutamos

npx prisma studio

y abrimos en el navegador la URL http://localhost:5555

prisma studio 1

prisma studio 2

11. Cómo organizar el código

En la mayoría de frameworks tradicionales el código se organiza según el patrón MVC.

Sin embargo, Next.js trabaja por componentes y no sigue este patrón. Aunque este framework deja bastante libertad a la hora de organizar nuestro código, una propuesta recomendable es la siguiente.

  1. Como mínimo, trabaja con 3 carpetas: app, components y lib
src
├── app           (Páginas)
├── components    (Componentes de servidor y de cliente)
└── lib           (Conexión a BD, leer BD, modificar BD)
  1. Como mínimo, coloca 2 archivos en la carpeta lib: data.js y action.js
src/lib
├── actions.js    (Mutar Datos)
└── data.js       (Obtener datos) 

A continuación se muestra como organizar los archivos en una aplicación que trabaja con Productos.

Esquema de Prisma
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  // provider = "postgresql"
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Producto {
  id     Int    @id @default(autoincrement())
  nombre String
}
src
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.jsx
│   ├── page.jsx
│   └── productos
│       ├── [id]
│       │   └── page.jsx
│       └── page.jsx
├── components
│   ├── Producto.jsx
│   └── Productos.jsx
└── lib
    ├── actions.js
    └── data.js

11.1. Obtener datos

Todas las operaciones para realizar consultas de lectura en la BD las colocaremos en el archivo lib/data.js

// src/lib/data.js
'use server'
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient()



export async function obtenerProductos() {
    const produtos = await prisma.producto.findMany()
    return produtos
}


export async function obtenerLoteProductos(offset=0, limit=5) {
    const produtos = await prisma.producto.findMany({
        skip: offset,
        take: limit,
        orderBy: { id: "asc" },
    })
    return produtos
}


export async function obtenerProducto(id) {
    const producto = await prisma.producto.findUnique({
        where: {
            id: +id
        }
    })
    return producto
}

A la hora de mostrar la información recuperada, podemos hacerlo de 2 formas diferentes:

  • en un componente cliente (ofrece interactividad)
  • en un componente servidor (NO ofrece interactividad)

Veamos cada caso por separado.

11.1.1. Usando componente cliente

Tip

Streaming de datos desde el servidor al cliente

Next.js 16 y React 19 incorpora la API use() que permite realizar streaming de datos de forma sencilla haciendo uso de Suspense en el servidor y use() en el cliente. Ejemplo de uso:

En página en el servidor

import { getArticulos } from '@/lib/data'

export default function Pagina() {
   const promesa = getArticulos() // no usamos await
   
   return (
       <Suspense fallback="Recuperando lista de artículos...">
           <ComponenteCliente promesa={promesa} />  {/* Pasamos promesa */}
       </Suspense>
   )
}

En componente en el cliente

'use client'

export default function ComponenteCliente( {promesa}) {
   const articulos = use(promesa)   // Resolvemos promesa

   return (
       <div className="flex flex-wrap gap-4">
           {articulos.map(articulo => <Item key={articulo.id} articulo={articulo} />)}
       </div>
   )
}

Si necesitamos proporcionar interactidad, recuperaremos los datos de la BD dentro de una página de servidor y haremos streaming directo a un componente cliente.

Envolvemos dicho componente dentro de Suspense para mostrar un mensaje al usuario mediante la propiedad fallback mientras se cargan los datos.

EJEMPLO 1: Listado de productos

// src/app/productos/page.jsx
import { obtenerProductos } from "@/lib/data";
import { Suspense } from "react";

export default function PaginaProductos() {
    return (
        <div>
            <h1>Listado</h1>

            <Suspense fallback={"..."}>
                <Productos promesa={obtenerProductos()}/> {/* Pasamos promesa */} 
            </Suspense>

        </div>
    );
}
// --------------------- Componente de cliente -------------------
'use client'

export default function Productos({promesa}) {

    const productos = use(promesa)   // Resolvemos promesa

    return (
        <div>
            {productos.map(producto =>
                <p key={producto.id}>
                    {producto.nombre}
                </p>
            )}
        </div>

    );
}

EJEMPLO 2: Producto con id

// src/app/productos/[id]/page.jsx
import { obtenerProducto } from "@/lib/data";
import { Suspense } from "react";

export default async function PaginaProducto({ params }) {

    const { id } = await params

    return (
        <div>
            <h1>Producto #{id}</h1>

            <Suspense fallback={"..."}>
                <Producto promesa={obtenerProducto(id)}/> {/* Pasamos promesa */} 
            </Suspense>

        </div>
    );
}
// --------------------- Componente de cliente -------------------

'use client'

export default function Producto({ promesa }) {

    const producto = use(promesa)   // Resolvemos promesa

    return (
        <div>
            {producto.nombre}
        </div>

    );
}

Note

En Next.js también es posible recuperar datos directamente desde una página del lado cliente. A continuación tienes el código fuente para hacer un listado de Productos:

'use client'

import { useEffect, useState } from "react";
import { obtenerProductos } from "@/lib/data";


function PaginaCliente() {

   const [productos, setProductos] = useState([])

   // esto equivale a hacer fetch pero sin la necesidad de disponer de una API 
   async function cargar() {
       const productos = await obtenerProductos()  
       setProductos(productos)
   }

   useEffect(() => {
       cargar()
   }, [])



   if (productos.length === 0) return <p>Obteniendo datos ...</p>
   return (
       <div>
           <h1>Listado</h1>

           {productos.map(producto =>
               <p key={producto.id}>
                   {producto.nombre}
               </p>
           )}
       </div>
   );
} 

export default PaginaCliente;

Note

Además, si deseamos interactividad, podemos crear un botón que al pulsar cargue un nuevo lote de productos

'use client'

import { useEffect, useState, useTransition } from "react";
import { obtenerLoteProductos } from "@/lib/data";


function PaginaCliente() {

   const [productos, setProductos] = useState([])
   const [offset, setOffset] = useState(0);

   // esto equivale a hacer fetch pero sin la necesidad de disponer de una API 
   async function cargarMas() {
       const nuevos = await obtenerLoteProductos(offset, 5)  

       if (nuevos.length > 0) {
          setProductos(prev => [...prev, ...nuevos]);
          setOffset(prev => prev + 5);
       }
   }

   
   useEffect(() => {
       cargarMas()   // Carga inicial
   }, [])

   // Opcionalmente, podemos utilizar el hook useTransition para mostrar estado pendiente
   const [isPending, startTransition] = useTransition();


   return (
       <div>
           <h1>Listado</h1>
            
           {/* Botón */} 
           <button onClick={cargarMas}>
             Cargar más artículos
           </button> 

           {/* Otro Botón que muestra estado pendiente */} 
           <button onClick={() => startTransition(cargarMas)}>
             {isPending ? "Cargando..." : "Cargar más"}
           </button>

           {productos.map(producto =>
               <p key={producto.id}>
                   {producto.nombre}
               </p>
           )}
       </div>
   );
} 

export default PaginaCliente;

11.1.2. Usando componente servidor

Si NO necesitamos proporcionar interactidad, recuperaremos los datos de la BD dentro de un componente de servidor.

Envolvemos dicho componente dentro de Suspense para mostrar un mensaje al usuario mediante la propiedad fallback mientras se cargan los datos.

// src/app/productos/page.jsx
import { obtenerProductos } from "@/lib/data";
import { Suspense } from "react";

export default function PaginaProductos() {
    return (
        <div>
            <h1>Listado</h1>

            <Suspense fallback={"..."}>
                <Productos />
            </Suspense>

        </div>
    );
}
// --------------------- Componente de servidor -------------------

async function Productos() {
    const productos = await obtenerProductos()

    return (
        <div>
            {productos.map(producto =>
                <p key={producto.id}>
                    {producto.nombre}
                </p>
            )}
        </div>

    );
}
// src/app/productos/[id]/page.jsx
import { obtenerProducto } from "@/lib/data";
import { Suspense } from "react";

export default async function PaginaProducto({ params }) {

    const { id } = await params

    return (
        <div>
            <h1>Producto #{id}</h1>

            <Suspense fallback={"..."}>
                <Producto id={id} />
            </Suspense>

        </div>
    );
}
// --------------------- Componente de servidor -------------------

async function Producto({ id }) {

    const producto = await obtenerProducto(id)

    return (
        <div>
            {producto.nombre}
        </div>

    );
}

11.2. Mutar datos

Entendemos por mutación de datos a las operaciones de:

  • Insertar
  • Modificar
  • Eliminar

Todas las operaciones para realizar consultas de mutación en la BD las colocaremos en el archivo lib/actions.js

// src/lib/actions.js
'use server'
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";  // IMPORTANTE: importar desde next/navigation
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient()



export async function insertarProducto(formData) {

    const nombre = formData.get('nombre')

    const produto = await prisma.producto.create({
        data: { nombre }
    })

    revalidatePath("/productos")
}



export async function modificarProducto(formData) {

    const id = Number(formData.get('id'))
    const nombre = formData.get('nombre')

    const produto = await prisma.producto.update({
        where: {  id  },
        data: {  nombre  }
    })

    revalidatePath("/productos")
    redirect("/productos")
}



export async function eliminarProducto(formData) {

    const id = Number(formData.get('id'))

    const produto = await prisma.producto.delete({
        where: {  id  },
    })

    revalidatePath("/productos")
    redirect("/productos")
}
// src/components/Productos.jsx
import { eliminarProducto, insertarProducto, modificarProducto } from "@/lib/actions";
import { obtenerProductos } from "@/lib/data";
import Link from "next/link";

export default
    async function Productos() {
    const productos = await obtenerProductos()

    return (
        <div>
            {productos.map(producto =>
                <div key={producto.id}>
                    <Links
                        href={`/productos/${producto.id}`}
                        className="block">
                        {producto.nombre}
                    </Link>

                    <form >
                        <input type="hidden" name="id" defaultValue={producto.id}/>
                        <input name="nombre" defaultValue={producto.nombre} />

                        <button formAction={modificarProducto}>Modificar</button>
                        <button formAction={eliminarProducto}>Eliminar</button>
                    </form>
                </div>
            )}

            <form action={insertarProducto}>
                <input name="nombre" />
                <button>Insertar</button>
            </form>
        </div>
    );
}
// src/components/Producto.jsx
import { eliminarProducto, modificarProducto } from "@/lib/actions";
import { obtenerProducto } from "@/lib/data";


export default
    async function Producto({ id }) {
    const producto = await obtenerProducto(id)

    return (
        <>
            <p>{producto.nombre}</p>
            <form>
                <input type="hidden" name="id" defaultValue={producto.id}/>
                <input name="nombre" defaultValue={producto.nombre} />

                <button formAction={modificarProducto}>Modificar</button>
                <button formAction={eliminarProducto}>Eliminar</button>
            </form>
        </>
    );
}
// src/app/productos/page.jsx
import Productos from "@/components/Productos";
import { Suspense } from "react";


export default function PaginaProductos() {
    return (
        <div>
            <h1 className="text-2xl">Listado</h1>

            <Suspense fallback={"..."}>
                <Productos />
            </Suspense>

        </div>
    );
}
// src/app/productos/[id]/page.jsx
import { Suspense } from "react";
import Producto from "@/components/Producto";

export default async function PaginaProducto({ params }) {

    const { id } = await params

    return (
        <div>
            <h1 className="text-2xl">Producto #{id}</h1>

            <Suspense fallback={"..."}>
                <Producto id={id} />
            </Suspense>

        </div>
    );
}

12. Cliente de Prisma

El cliente de prisma es el objeto que permite a nuestra aplicación (cliente) conectar e interactuar con el servidor de la base de datos.

En la fase de desarrollo de nuestra aplicación es habitual que ésta sea reiniciada cada vez que realizamos un cambio en ella. Esto provoca que las conexiones a la base de datos se abran de forma frecuente, lo cual puede acarrear problemas si nuestro plan gratuito tiene límite de conexiones o costos económicos en planes no gratuitos.

Para evitar esto durante la fase de desarrollo reutilizamos la conexión previa de prisma guardándola en el objeto global.

Por este motivo es frecuente implementar un archivo lib/prisma.js con el código mostrado a continuación o similar, donde exportarmos el objeto prisma que luego importaremos en lib/data.js y lib/actions.js.

// lib/prisma.js
import { PrismaClient } from '@prisma/client';

const prisma = global.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") global.prisma = prisma;

export default prisma

13. Despliegue en Vercel

Vercel almacenará en caché automáticamente las dependencias durante el despliegue. Para la mayoría de las aplicaciones, esto no causará ningún problema. Sin embargo, para Prisma, puede resultar en una versión obsoleta de Prisma Client si se cambia su esquema de Prisma.

Para evitar este problema, debemos insertar prisma generate al script build en el archivo package.json:

{
  ...
  "scripts" {
    ...
    "build": "prisma generate && next build",
  }
  ...
}

14. Referencias