DESARROLLO WEB EN ENTORNO SERVIDOR
Utilización de técnicas de acceso a datos
BASES DE DATOS con ORM, PRISMA
- 1. Introducción
- 2. SQLite
- 3. MySQL
- 4. MySQL (Serverless)
- 5. Postgres
- 6. Postgres (Vercel)
- 7. Primeros pasos con ORM Prisma
- 8. Definiendo el Esquema de Datos
- 9. Consultas
- 10. Ver datos de las tablas
- 11. Cómo organizar el código
- 12. Cliente de Prisma
- 13. Despliegue en Vercel
- 14. Referencias
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:
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.
La estructura del proyecto es la siguiente:
El código fuente completo puede obtenerse desde el siguiente enlace:
Los archivos directamente relacionados con la Base de Datos, son:
src/database/db.jssrc/lib/sqlite.jssrc/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]);
// ...
}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.
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.
La estructura del proyecto es la siguiente:
El código fuente completo puede obtenerse desde el siguiente enlace:
Los archivos directamente relacionados con la Base de Datos, son:
src/database/db.sqlsrc/lib/mysql.jssrc/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:
- Consultas simples
- 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);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.jsysrc/app/articulos/delete/page.js - El componente
src/components/Articulo.jsacepta propiedades, entre ellas el famosochildren, que nos permite incrustar contenido JSX. - El componente
src/components/Form.jstambié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 Articulosrc/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 FormMySQL/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.
La estructura del proyecto es la siguiente:
El código fuente completo puede obtenerse desde el siguiente enlace:
Los archivos directamente relacionados con la Base de Datos, son:
src/database/db.sqlsrc/lib/mysql.jssrc/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]);
// ...
}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.
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.
La estructura del proyecto es la siguiente:
El código fuente completo puede obtenerse desde el siguiente enlace:
Los archivos directamente relacionados con la Base de Datos, son:
src/database/config.mjssrc/database/seed.mjssrc/lib/postgres.jssrc/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]);
// ...
}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.
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.
La estructura del proyecto es la siguiente:
El código fuente completo puede obtenerse desde el siguiente enlace:
Los archivos directamente relacionados con la Base de Datos, son:
.envsrc/database/seed.mjssrc/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};`
// ...
}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.
Luego pulsaremos en el botón Create Database.
A la hora de desplegar en Vercel la aplicación deberemos configurar las variables de entorno, en este caso POSTGRES_URL.
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.
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:
- Crear la base de datos: reserva de espacio en un DBaaS o similar.
- Migrar (
migrate): creación de tablas. - Sembrar (
seed): inserción de datos iniciales.
Las dos primeras tareas son obligatorias. La tercera tarea es opcional.
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-pgNote
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.
npx prisma
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 sqliteEste comando hace 3 cosas:
- 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. - crea un nuevo archivo llamado
prisma.config.tsque contiene una configuración básica de prisma. - añade al archivo
.enven el directorio raíz del proyecto la variable de entornoDATABASE_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>'
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 generateDe 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
Hay dos formas alternativas de definir un esquema de datos:
-
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.
-
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.
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:
Si ha habido algún cambio al esquema, entonces nos solicitará un nombre para la migración:
Tip
Una operación muchos más cómoda y directa, es hacer:
npx prisma db push
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.
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,
Usuarioen 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 |
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`)
}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?
}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.
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 nombremigraciono, en su lugar, ejecutaremos:
npx prisma db pushImportant
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.
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 generateImportant
La operación npx prisma db pull borra el esquema previo de prisma/schema.prisma.
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:
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/
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',
},
],
},
},
})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 anteriorEstructura 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
})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
})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
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',
},
})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,
},
})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,
},
},
})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:
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,
});Ejecutamos
npx prisma studioy abrimos en el navegador la URL http://localhost:5555
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.
- 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)
- 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
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.
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;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>
);
}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>
);
}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 prismaVercel 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",
}
...
}- CRUD usando ORM Prisma con 3 tablas en relación 1:N y N:M
- Video: Nextjs y Prisma ORM desde Cero usando Typescript
- Video: Prisma in Next.js
- MySQL API con NextJS
- MySQL CRUD con NextJS
- BD Serverless en PlanetScale
- Usando SQLite con NextJS 13
- Getting Started with Vercel Postgres
- Get started with Prisma
- Prisma schema



















