ORM: cómo trabajar con bases de datos sin escribir SQL directamente

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:

EcosistemaORM
JavaHibernate
Node.js / TypeScriptPrisma
PHP / LaravelEloquent
C# / .NETEntity 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.