ORM: cómo trabajar con bases de datos sin escribir SQL directamente
Cuando desarrollamos una aplicación que utiliza una base de datos relacional, tarde o temprano nos encontramos con SQL:
SELECT * FROM users WHERE id = 10;
SQL es el lenguaje que utilizamos para consultar y modificar bases de datos como MySQL, PostgreSQL, SQL Server o MariaDB.
Sin embargo, en aplicaciones modernas normalmente trabajamos con lenguajes como Java, JavaScript/TypeScript, PHP o C#. En estos lenguajes pensamos en objetos, clases, propiedades y métodos, mientras que las bases de datos relacionales trabajan con tablas, columnas, filas y relaciones.
Aquí aparece el concepto de ORM.
¿Qué es un ORM?
ORM significa Object-Relational Mapping, o Mapeo Objeto-Relacional.
Un ORM es una herramienta que permite representar las tablas de una base de datos mediante estructuras del lenguaje de programación.
Por ejemplo, una tabla:
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(150)
);
podría representarse conceptualmente como una clase:
User
├── id
├── name
└── email
El ORM se encarga de convertir nuestras operaciones sobre esos objetos en consultas que la base de datos pueda ejecutar.
Por ejemplo, nosotros podemos escribir algo como:
buscar usuario con id = 10
y el ORM puede generar una consulta equivalente a:
SELECT * FROM users WHERE id = 10;
Esto no significa que el ORM elimine SQL de la aplicación. El SQL sigue existiendo; simplemente dejamos que la herramienta lo genere en lugar de escribirlo manualmente en muchas partes de nuestro código.
> Importante: el SQL mostrado en los siguientes ejemplos es una equivalencia conceptual. El SQL real generado puede variar dependiendo del ORM, configuración, proveedor de base de datos y operación realizada.
Hibernate — Java
En el ecosistema Java, uno de los ORM más conocidos es Hibernate.
Hibernate permite mapear clases Java con tablas de una base de datos.
Supongamos que tenemos la tabla:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(150)
);
Podemos representar esa tabla mediante una entidad Java:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
private String name;
private String email;
// Getters y setters
}
Ahora podemos buscar un usuario utilizando Java:
User user = entityManager.find(User.class, 10L);
Conceptualmente, Hibernate necesita realizar una operación equivalente a:
SELECT *
FROM users
WHERE id = 10;
Insertar un registro
Con Hibernate podemos crear un objeto:
User user = new User();
user.setId(10L);
user.setName("Arturo");
user.setEmail("arturo@example.com");
entityManager.persist(user);
Conceptualmente, esto corresponde a:
INSERT INTO users (id, name, email)
VALUES (10, 'Arturo', 'arturo@example.com');
Actualizar un registro
Podemos modificar el objeto:
user.setName("Arturo Juárez");
Hibernate puede detectar el cambio y generar una operación equivalente a:
UPDATE users
SET name = 'Arturo Juárez'
WHERE id = 10;
Esta es una de las ideas fundamentales de un ORM: trabajamos con objetos mientras el ORM se encarga de traducir esas operaciones hacia el modelo relacional.
Prisma — Node.js / TypeScript
En el ecosistema de Node.js, especialmente cuando trabajamos con TypeScript, uno de los ORM más conocidos es Prisma.
En Prisma normalmente definimos nuestros modelos mediante el archivo schema.prisma.
Por ejemplo:
model User {
id Int @id
name String
email String
}
A partir de este modelo podemos trabajar con la base de datos desde TypeScript.
Por ejemplo, para buscar un usuario:
const user = await prisma.user.findUnique({
where: {
id: 10
}
});
La operación es conceptualmente equivalente a:
SELECT *
FROM users
WHERE id = 10;
Insertar un usuario
En Prisma podemos utilizar:
const user = await prisma.user.create({
data: {
id: 10,
name: "Arturo",
email: "arturo@example.com"
}
});
Conceptualmente:
INSERT INTO users (id, name, email)
VALUES (10, 'Arturo', 'arturo@example.com');
Actualizar un usuario
const user = await prisma.user.update({
where: {
id: 10
},
data: {
name: "Arturo Juárez"
}
});
Equivalencia conceptual:
UPDATE users
SET name = 'Arturo Juárez'
WHERE id = 10;
Una ventaja importante de Prisma es que podemos trabajar con un API orientada a objetos y aprovechar el tipado de TypeScript.
Por ejemplo:
const users = await prisma.user.findMany({
where: {
name: {
contains: "Arturo"
}
}
});
Conceptualmente:
SELECT *
FROM users
WHERE name LIKE '%Arturo%';
Eloquent — PHP / Laravel
En Laravel, el ORM utilizado es Eloquent.
Eloquent utiliza modelos PHP para representar las tablas de nuestra base de datos.
Por ejemplo:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
}
Una consulta para buscar un usuario puede escribirse así:
$user = User::find(10);
Conceptualmente:
SELECT *
FROM users
WHERE id = 10
LIMIT 1;
Insertar un registro
Podemos crear un usuario:
$user = new User();
$user->name = 'Arturo';
$user->email = 'arturo@example.com';
$user->save();
Conceptualmente:
INSERT INTO users (name, email)
VALUES ('Arturo', 'arturo@example.com');
Consultar varios usuarios
$users = User::where('name', 'like', '%Arturo%')->get();
Conceptualmente:
SELECT *
FROM users
WHERE name LIKE '%Arturo%';
Actualizar
$user = User::find(10);
$user->name = 'Arturo Juárez';
$user->save();
Conceptualmente:
UPDATE users
SET name = 'Arturo Juárez'
WHERE id = 10;
Eloquent también destaca por su integración con Laravel y por permitir representar relaciones entre modelos.
Por ejemplo:
$user->posts;
puede representar una relación entre un usuario y sus publicaciones.
Conceptualmente podríamos terminar realizando una consulta como:
SELECT *
FROM posts
WHERE user_id = 10;
Entity Framework Core — C# / .NET
En el ecosistema de C# y .NET, una de las herramientas principales para ORM es Entity Framework Core, normalmente conocido como EF Core.
Podemos definir una entidad:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Y posteriormente configurar nuestro contexto:
public class AppDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
}
Para buscar un usuario:
var user = await context.Users
.FirstOrDefaultAsync(u => u.Id == 10);
Conceptualmente:
SELECT TOP 1 *
FROM Users
WHERE Id = 10;
El SQL exacto depende del proveedor utilizado. Por ejemplo, SQL Server utiliza construcciones diferentes a PostgreSQL.
Insertar
var user = new User
{
Id = 10,
Name = "Arturo",
Email = "arturo@example.com"
};
context.Users.Add(user);
await context.SaveChangesAsync();
Conceptualmente:
INSERT INTO Users (Id, Name, Email)
VALUES (10, 'Arturo', 'arturo@example.com');
Actualizar
var user = await context.Users
.FirstOrDefaultAsync(u => u.Id == 10);
user.Name = "Arturo Juárez";
await context.SaveChangesAsync();
Conceptualmente:
UPDATE Users
SET Name = 'Arturo Juárez'
WHERE Id = 10;
Una característica importante de Entity Framework Core es LINQ, que permite expresar consultas utilizando C#:
var users = await context.Users
.Where(u => u.Name.Contains("Arturo"))
.ToListAsync();
Conceptualmente:
SELECT *
FROM Users
WHERE Name LIKE '%Arturo%';
El mismo problema utilizando cuatro ORMs
Una forma sencilla de entender los ORM es comparar la misma operación.
Queremos obtener al usuario cuyo id sea 10.
SQL
SELECT *
FROM users
WHERE id = 10;
Java + Hibernate
User user = entityManager.find(User.class, 10L);
Node.js + Prisma
const user = await prisma.user.findUnique({
where: {
id: 10
}
});
PHP + Laravel Eloquent
$user = User::find(10);
C# + Entity Framework Core
var user = await context.Users
.FirstOrDefaultAsync(u => u.Id == 10);
Aunque la sintaxis cambia entre lenguajes, la intención es prácticamente la misma:
Aplicación
↓
ORM
↓
SQL
↓
Base de datos
¿Por qué utilizar un ORM?
Un ORM puede reducir una gran cantidad de código repetitivo.
Sin ORM, una aplicación podría necesitar escribir constantemente:
SELECT ...
INSERT ...
UPDATE ...
DELETE ...
además de encargarse manualmente de convertir los resultados de esas consultas en objetos del lenguaje de programación.
Con un ORM podemos trabajar directamente con modelos:
User
Order
Product
Invoice
Customer
y utilizar operaciones del propio lenguaje:
find()
create()
update()
delete()
o mecanismos equivalentes.
Esto también facilita trabajar con relaciones entre entidades.
Por ejemplo:
User
├── id
├── name
└── email
│
└── Posts
├── id
├── title
└── content
En una base de datos relacional podríamos tener:
users
↓
posts
mediante una clave foránea:
CREATE TABLE posts (
id INT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
El ORM permite representar esa relación utilizando las estructuras del propio lenguaje.
¿Un ORM significa que ya no necesitamos aprender SQL?
No.
De hecho, aprender SQL sigue siendo muy importante.
Un ORM facilita el trabajo cotidiano, pero detrás de una consulta como:
await prisma.user.findMany({
where: {
email: {
contains: "@gmail.com"
}
}
});
sigue existiendo una operación SQL equivalente.
Para entender realmente qué está haciendo nuestra aplicación es conveniente conocer conceptos como:
SELECT
INSERT
UPDATE
DELETE
JOIN
WHERE
GROUP BY
ORDER BY
INDEX
PRIMARY KEY
FOREIGN KEY
Además, cuando una consulta es compleja o presenta problemas de rendimiento, conocer SQL permite inspeccionar qué está ocurriendo realmente en la base de datos.
ORM frente a SQL directo
Podemos verlo de esta manera:
SQL directo
SELECT *
FROM users
WHERE id = 10;
ORM
const user = await prisma.user.findUnique({
where: { id: 10 }
});
El segundo enfoque no reemplaza conceptualmente al primero.
Lo que cambia es quién escribe el SQL.
Con SQL directo:
Desarrollador
↓
SQL
↓
Base de datos
Con ORM:
Desarrollador
↓
ORM
↓
SQL generado
↓
Base de datos
Conclusión
Un ORM es una capa de abstracción que permite trabajar con una base de datos relacional utilizando las estructuras propias de nuestro lenguaje de programación.
Las equivalencias que hemos visto son:
| Ecosistema | ORM |
|---|---|
| Java | Hibernate |
| Node.js / TypeScript | Prisma |
| PHP / Laravel | Eloquent |
| C# / .NET | Entity Framework Core |
Por debajo, todos terminan interactuando con una base de datos relacional mediante operaciones que pueden expresarse en SQL.
Por eso una buena forma de entender los ORM no es pensar:
> "El ORM reemplaza SQL."
Sino:
> "El ORM me permite expresar operaciones de base de datos desde mi lenguaje de programación y se encarga de traducirlas a SQL."
La combinación de ambas habilidades —conocer el ORM y entender SQL— permite trabajar con mayor comodidad sin perder de vista lo que realmente está ocurriendo en la base de datos.