[ES] Use ORM for CRUD
Guía práctica de operaciones CRUD con ORMs: crea, lee, actualiza y elimina datos usando SQLAlchemy, Prisma y Hibernate en Python, JavaScript y Java.
Visión General
Los ORMs (Object-Relational Mappers) abstraen las interacciones con la base de datos en objetos de código nativo, reduciendo código repetitivo SQL y mejorando la mantenibilidad. Operaciones CRUD usando SQLAlchemy (Python), Prisma (JavaScript) y Hibernate (Java).
Cuándo Usar
Usa este recurso cuando:
- Construyes aplicaciones con muchos tipos de entidades y relaciones
- Reduces código repetitivo SQL y overhead de migraciones
- Garantizas type safety y autocomplete para operaciones de base de datos
Solución
Python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String, nullable=False, unique=True)
role = Column(String, default='user')
engine = create_engine('postgresql://user:pass@localhost/mydb')
Session = sessionmaker(bind=engine)
# Create
session = Session()
user = User(email='alice@example.com', role='admin')
session.add(user)
session.commit()
# Read
user = session.query(User).filter_by(email='alice@example.com').first()
# Update
user.role = 'superadmin'
session.commit()
# Delete
session.delete(user)
session.commit()
session.close()
JavaScript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function crud() {
// Create
const user = await prisma.user.create({
data: { email: 'alice@example.com', role: 'admin' }
});
// Read
const found = await prisma.user.findUnique({
where: { email: 'alice@example.com' }
});
// Update
const updated = await prisma.user.update({
where: { id: user.id },
data: { role: 'superadmin' }
});
// Delete
await prisma.user.delete({ where: { id: user.id } });
}
Java
import jakarta.persistence.*;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, unique = true)
private String email;
private String role = "user";
// getters y setters omitidos
}
public class UserRepository {
private final EntityManager em;
public UserRepository(EntityManager em) { this.em = em; }
public void create(User user) {
em.getTransaction().begin();
em.persist(user);
em.getTransaction().commit();
}
public User findByEmail(String email) {
return em.createQuery("SELECT u FROM User u WHERE u.email = :email", User.class)
.setParameter("email", email)
.getSingleResult();
}
public void updateRole(Integer id, String role) {
em.getTransaction().begin();
User user = em.find(User.class, id);
user.setRole(role);
em.getTransaction().commit();
}
public void delete(Integer id) {
em.getTransaction().begin();
em.remove(em.find(User.class, id));
em.getTransaction().commit();
}
}
Explicación
SQLAlchemy usa una clase base declarativa donde las clases Python se mapean a tablas. Las sesiones gestionan transacciones y ciclos de vida de objetos. Prisma genera un cliente type-safe desde un archivo de schema, ofreciendo validación en tiempo de compilación y excelente soporte de IDE. Hibernate usa anotaciones JPA (@Entity, @Id, @Column) para mapear objetos Java a tablas, con EntityManager gestionando contextos de persistencia y transacciones.
Variantes
| Tecnología | Enfoque | Notas |
|---|---|---|
| Python | Django ORM | Todo incluido, fuertemente acoplado a Django |
| JavaScript | TypeORM | ORM basado en decoradores con fuerte soporte de TypeScript |
| Java | Spring Data JPA | Abstracción de repositorio sobre Hibernate |
Lo que funciona
- Define schemas y constraints explícitos en el ORM, no solo en la base de datos
- Usa transacciones para operaciones multi-paso para garantizar atomicidad
- Añade índices a nivel de base de datos en columnas frecuentemente consultadas
- Usa eager loading (
joinedload,include,fetch) cuidadosamente para evitar consultas N+1 - Mantén las entidades ORM livianas; mueve la lógica de negocio a capas de servicio
Errores Comunes
- Usar ORMs para consultas analíticas complejas, causando mal rendimiento
- Ignorar el problema de consultas N+1 cargando datos relacionados en bucles
- Almacenar lógica de negocio dentro de clases de entidad ORM
- No manejar
LazyInitializationExceptionen Hibernate fuera de sesiones - Olvidar cerrar sesiones o clientes Prisma, causando fugas de conexiones
Tips de Rendimiento
- Usa
lean()en Mongoose o proyecciones DTO en Hibernate. Omite la hidratación del ORM para consultas de solo lectura:
const users = await User.find().lean().exec();
@Query("SELECT new com.app.dto.UserDTO(u.id, u.email) FROM User u WHERE u.role = :role")
List<UserDTO> findDTOsByRole(@Param("role") String role);
- Usa paginación basada en cursor en lugar de offset. La paginación con offset se vuelve más lenta a medida que avanzas:
# Mal: paginación con offset (lenta para offsets grandes)
session.query(User).offset(100000).limit(20).all()
# Bien: paginación con cursor (tiempo constante)
last_id = 100000
session.query(User).filter(User.id > last_id).order_by(User.id).limit(20).all()
- Habilita el caché de segundo nivel en Hibernate. Cachea entidades leídas frecuentemente:
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
// ...
}
- Usa
EXPLAIN ANALYZEen las consultas generadas por el ORM. El SQL que tu ORM genera puede no usar índices eficientemente. Siempre verifica conEXPLAIN:
EXPLAIN ANALYZE SELECT users.id, users.email, users.role
FROM users
WHERE users.role = 'admin'
ORDER BY users.email;
- Desactiva
auto_flushdurante operaciones bulk. SQLAlchemy hace flush de la sesión antes de cada consulta por defecto. Desactívalo durante batch inserts:
session.autoflush = False
# Operaciones bulk
session.autoflush = True Preguntas frecuentes
¿Debería usar un ORM o SQL crudo?
Usa un ORM para CRUD, relaciones y migraciones. Usa SQL crudo para agregaciones complejas, reportes y rutas críticas de rendimiento. Muchos proyectos usan ambos.
¿Cómo evito consultas N+1 con un ORM?
Usa eager loading: selectinload en SQLAlchemy, include en Prisma, FetchType.EAGER o JOIN FETCH en Hibernate. Monitorea el conteo de consultas en desarrollo.
¿Los ORMs pueden manejar migraciones de base de datos?
Sí. SQLAlchemy usa Alembic, Prisma tiene migraciones integradas, y Hibernate puede autogenerar schemas con hbm2ddl. Sin embargo, las migraciones de producción deben revisarse y probarse.
SQLAlchemy con Relaciones y Eager Loading
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, selectinload
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String, nullable=False, unique=True)
role = Column(String, default='user')
posts = relationship("Post", back_populates="author", cascade="all, delete-orphan")
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
author = relationship("User", back_populates="posts")
engine = create_engine('postgresql://user:pass@localhost/mydb')
Session = sessionmaker(bind=engine)
session = Session()
# Eager loading con selectinload (evita N+1)
from sqlalchemy import select
stmt = (
select(User)
.options(selectinload(User.posts))
.where(User.role == 'admin')
)
admins_with_posts = session.execute(stmt).scalars().all()
# Bulk insert
session.add_all([
User(email=f'user{i}@example.com', role='user')
for i in range(100)
])
session.commit()
# Bulk update con Core
from sqlalchemy import update
session.execute(
update(User)
.where(User.role == 'user')
.values(role='member')
)
session.commit()
Schema de Prisma con Relaciones
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
role String @default("user")
posts Post[]
@@index([role])
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
@@index([authorId])
}
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Crear con registros anidados
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
role: 'admin',
posts: {
create: [
{ title: 'First Post' },
{ title: 'Second Post' },
],
},
},
include: { posts: true },
});
// Transacción con múltiples operaciones
const [newUser, updatedPost] = await prisma.$transaction([
prisma.user.create({ data: { email: 'bob@example.com' } }),
prisma.post.update({ where: { id: 1 }, data: { title: 'Updated' } }),
]);
// Upsert (crear o actualizar)
const result = await prisma.user.upsert({
where: { email: 'carol@example.com' },
update: { role: 'admin' },
create: { email: 'carol@example.com', role: 'admin' },
});
// SQL crudo para consultas complejas
const topUsers = await prisma.$queryRaw`
SELECT u.email, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
GROUP BY u.email
ORDER BY post_count DESC
LIMIT 10
`;
Django ORM
# models.py
from django.db import models
class User(models.Model):
email = models.EmailField(unique=True)
role = models.CharField(max_length=20, default='user')
class Meta:
indexes = [models.Index(fields=['role'])]
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
# Operaciones CRUD
user = User.objects.create(email='alice@example.com', role='admin')
users = User.objects.filter(role='admin').select_related('posts') # Eager load
user.role = 'superadmin'
user.save()
user.delete()
# Bulk create
User.objects.bulk_create([
User(email=f'user{i}@example.com') for i in range(100)
])
# Bulk update
User.objects.filter(role='user').update(role='member')
# Agregación
from django.db.models import Count, Avg
User.objects.annotate(post_count=Count('posts')).filter(post_count__gt=5)
Spring Data JPA Repository
public interface UserRepository extends JpaRepository<User, Integer> {
// Métodos de consulta derivados
Optional<User> findByEmail(String email);
List<User> findByRole(String role);
// Consultas personalizadas con @Query
@Query("SELECT u FROM User u WHERE u.role = :role ORDER BY u.email")
List<User> findByRoleOrdered(@Param("role") String role);
// Consulta nativa
@Query(value = "SELECT * FROM users WHERE email LIKE :pattern",
nativeQuery = true)
List<User> findByEmailPattern(@Param("pattern") String pattern);
// Consultas de modificación
@Modifying
@Query("UPDATE User u SET u.role = :role WHERE u.id = :id")
int updateRole(@Param("id") Integer id, @Param("role") String role);
// Paginación y ordenamiento
Page<User> findByRole(String role, Pageable pageable);
}
// Uso con paginación
Pageable pageable = PageRequest.of(0, 20, Sort.by("email").ascending());
Page<User> adminPage = userRepository.findByRole("admin", pageable);
TypeORM (TypeScript)
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from 'typeorm';
@Entity()
class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column({ default: 'user' })
role: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
@Entity()
class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, user => user.posts)
author: User;
}
// Patrón repository
const userRepo = dataSource.getRepository(User);
// Crear
const user = userRepo.create({ email: 'alice@example.com', role: 'admin' });
await userRepo.save(user);
// Leer con relaciones
const users = await userRepo.find({
where: { role: 'admin' },
relations: ['posts'],
order: { email: 'ASC' },
take: 20,
skip: 0,
});
// Operaciones bulk
await userRepo
.createQueryBuilder()
.update()
.set({ role: 'member' })
.where('role = :role', { role: 'user' })
.execute();
Recursos Relacionados
Conectar a MySQL
Cómo conectar a bases de datos MySQL en Python, JavaScript y Java.
RecipeConectar a PostgreSQL
Cómo conectar a bases de datos PostgreSQL en Python, JavaScript y Java.
RecipeConectar a Redis
Cómo conectarse a Redis y realizar operaciones básicas en Python, JavaScript y Java.
RecipeEjecutar SQL Crudo
Cómo ejecutar consultas SQL crudas de forma segura con sentencias parametrizadas.
PatternPatrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
GuideNormalización de Bases de Datos — 1NF a 5NF Explicado
Guía visual de normalización de bases de datos: aprende 1NF a 5NF con ejemplos prácticos, cuándo aplicar cada forma y cómo balancear normalización con rendimiento.