Prisma ORM: el puente tipado entre tu código y la base de datos

¿Qué es Prisma?

Prisma es un ORM (Object-Relational Mapping) moderno para Node.js y TypeScript. Permite trabajar con bases de datos relacionales describiendo tus tablas como modelos y consultando datos con una API tipada, en lugar de escribir SQL a mano en cada lugar donde lo necesitas.

Su flujo general es:

Modelo (schema.prisma)
-> Prisma Client (código tipado)
-> Base de datos

El punto clave es que el esquema es la única fuente de verdad: a partir de él, Prisma genera un cliente con tipos y autocompletado.

Modelado de datos

El modelado de datos consiste en definir las entidades o tablas de tu base de datos, así como sus campos, tipos y restricciones. En Prisma esto se escribe dentro del archivo de esquema.

model Usuario {
  id        Int     @id @default(autoincrement())
  email     String  @unique
  nombre    String
  posts     Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  titulo    String
  publicado Boolean @default(false)
  autorId   Int
  autor     Usuario @relation(fields: [autorId], references: [id])
}

Aquí podemos observar:

  • Campos: id, email, nombre, titulo, etc.
  • Tipos: Int, String, Boolean.
  • Restricciones: @id, @unique, @default y @relation.
  • Relaciones: el campo posts en Usuario y autor en Post.

Mapeo objeto-relacional (ORM)

El término ORM hace referencia a la traducción que ocurre entre los objetos de tu código y los registros de la base de datos.

Objeto del código <-> Prisma <-> Registro en la tabla

Gracias a ese mapeo, en lugar de escribir SQL a mano:

SELECT * FROM usuarios WHERE email = 'ana@ejemplo.com';

Puedes escribir:

const usuario = await prisma.usuario.findUnique({
  where: { email: "ana@ejemplo.com" },
});

Prisma se encarga de convertir el llamado en la consulta SQL correspondiente y de transformar el resultado de vuelta en un objeto JavaScript/TypeScript.

Esquema / Schema

El esquema es la descripción formal de la estructura de la base de datos: tablas, columnas, tipos y relaciones. En Prisma vive en prisma/schema.prisma.

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

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

El esquema no solo documenta la base de datos: es la entrada con la que Prisma genera el cliente y crea las migraciones.

Migraciones

Las migraciones permiten evolucionar la estructura de la base de datos de forma controlada y versionada. Cada cambio al esquema (agregar una tabla, un campo o una relación) produce una migración que puede aplicarse, revisarse y repetirse en todos los entornos.

npx prisma migrate dev --name crear_modelo_usuario

Los pasos típicos son:

  1. Modificar el esquema.
  2. Generar la migración con prisma migrate dev.
  3. Revisar el SQL que Prisma propone.
  4. Aplicarla a la base de datos de desarrollo.
  5. Aplicarla a producción con prisma migrate deploy.

Las migraciones quedan guardadas como archivos dentro del proyecto, por lo que la evolución de la base de datos se versiona junto con el código.

CRUD

CRUD son las cuatro operaciones fundamentales sobre los datos: Create, Read, Update y Delete.

// Create
await prisma.usuario.create({
  data: { email: "ana@ejemplo.com", nombre: "Ana" },
});

// Read
const usuario = await prisma.usuario.findUnique({
  where: { email: "ana@ejemplo.com" },
});

// Update
await prisma.usuario.update({
  where: { email: "ana@ejemplo.com" },
  data: { nombre: "Ana María" },
});

// Delete
await prisma.usuario.delete({
  where: { email: "ana@ejemplo.com" },
});

Consultas tipadas

Una de las ventajas más importantes de usar Prisma desde TypeScript es que las consultas son tipadas: el compilador conoce los campos de cada modelo y puede validar tu código y ofrecer autocompletado.

Esto convierte errores que antes se descubrían en tiempo de ejecución (una columna mal escrita, un campo inexistente) en errores de compilación.

// Si "Usuario" no tuviera el campo "nombre", esto no compilaría:
const usuario = await prisma.usuario.findUnique({
  where: { email: "ana@ejemplo.com" },
});

Si el esquema cambia, los tipos se regeneran y el compilador muestra exactamente qué código necesita actualizarse.

Relaciones

Los modelos pueden relacionarse entre sí de tres formas principales.

1:1 (uno a uno)

model Usuario {
  id     Int      @id @default(autoincrement())
  perfil Perfil?
}

model Perfil {
  id        Int     @id @default(autoincrement())
  bio       String
  usuarioId Int     @unique
  usuario   Usuario @relation(fields: [usuarioId], references: [id])
}

1:N (uno a muchos)

model Usuario {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id      Int     @id @default(autoincrement())
  autorId Int
  autor   Usuario @relation(fields: [autorId], references: [id])
}

N:M (muchos a muchos)

model Usuario {
  id     Int     @id @default(autoincrement())
  cursos Curso[]
}

model Curso {
  id       Int       @id @default(autoincrement())
  usuarios Usuario[]
}

Prisma se encarga de manejar las tablas intermedias y permite traer datos relacionados con include:

const usuariosConPosts = await prisma.usuario.findMany({
  include: { posts: true },
});

Transacciones

Una transacción permite ejecutar varias operaciones como una sola unidad atómica: o se aplican todas, o no se aplica ninguna. Esto evita estados inconsistentes cuando una operación depende de otra.

await prisma.$transaction(async (tx) => {
  const usuario = await tx.usuario.create({
    data: { email: "luis@ejemplo.com", nombre: "Luis" },
  });

  await tx.cuenta.create({
    data: { numero: "001", saldo: 100, usuarioId: usuario.id },
  });
});

Si la segunda operación falla, la primera se deshace y la base de datos queda como estaba antes.

Integridad y seguridad de datos

Prisma no se limita a traducir consultas: ayuda a mantener la integridad de los datos y a evitar consultas mal construidas.

  • Restricciones del modelo: claves primarias y foráneas, valores @unique, valores por defecto.
  • Validación en el cliente: el generador produce un cliente que solo permite usar campos y valores válidos según el esquema.
  • Consultas parametrizadas: Prisma genera SQL parametrizado, lo que reduce el riesgo de inyección SQL comparado con concatenar cadenas a mano.
  • Rechazo de consultas inválidas: el generador impide escribir consultas con campos inexistentes o combinaciones no permitidas por el esquema.

Abstracción del motor de BD

Prisma oculta buena parte de las diferencias entre motores de base de datos. Cambiar de PostgreSQL a MySQL, SQLite o SQL Server implica, en muchos casos, solo ajustar el provider del datasource y regenerar las migraciones.

datasource db {
  provider = "postgresql" // "mysql", "sqlite", "sqlserver", "cockroachdb"
  url      = env("DATABASE_URL")
}

Sin embargo, es importante entender que no elimina la necesidad de conocer SQL ni las características propias de cada motor: índices, funciones específicas, comportamiento de las transacciones o el costo de ciertas consultas siguen dependiendo de la base de datos que elijas.

Consideraciones

Prisma simplifica el trabajo diario con bases de datos relacionales al reducir la escritura manual de SQL, generar código tipado y versionar los cambios de esquema, pero conocer los fundamentos de SQL y de tu motor de base de datos sigue siendo necesario para tomar buenas decisiones.

© Arturo Juárez Monroy