Patrón Active Record
Envuelve una tabla o vista de base de datos en una clase donde una instancia está vinculada a una sola fila, y la clase provee métodos para operaciones CRUD directamente en el objeto.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripción General
El Patrón Active Record envuelve una tabla de base de datos en una clase donde cada instancia representa una sola fila. El objeto lleva tanto datos (atributos) como comportamiento (métodos CRUD). Llamar user.save() persiste el objeto directamente a la base de datos sin una capa de acceso a datos separada.
Este patrón es la forma más simple de conectar objetos y bases de datos relacionales. Ruby on Rails, Django ORM y Laravel Eloquent están todos construidos sobre Active Record. Reduce boilerplate pero acopla lógica de dominio a persistencia.
Cuándo Usar
Usa el Patrón Active Record cuando:
- El modelo de dominio mapea estrechamente a tablas de base de datos
- Quieres mínimo boilerplate para operaciones CRUD
- El prototipado rápido es más importante que la pureza arquitectónica
- La aplicación es pequeña a mediana y no necesita lógica de dominio compleja
Cuándo Evitar
- La lógica de dominio compleja debería aislarse de la persistencia (usa Data Mapper o Repository)
- La misma entidad necesita persistirse a múltiples fuentes de datos
- El testing unitario sin base de datos es difícil porque el objeto depende de ella
- La aplicación crece y los objetos Active Record se vuelven bloated
Solución
Python
import sqlite3
from typing import Optional, List
class User:
_db_path = "app.db"
def __init__(self, id: int = None, name: str = "", email: str = ""):
self.id = id
self.name = name
self.email = email
@classmethod
def _connect(cls):
return sqlite3.connect(cls._db_path)
def save(self):
with self._connect() as conn:
if self.id is None:
cursor = conn.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(self.name, self.email)
)
self.id = cursor.lastrowid
else:
conn.execute(
"UPDATE users SET name = ?, email = ? WHERE id = ?",
(self.name, self.email, self.id)
)
conn.commit()
return self
def delete(self):
with self._connect() as conn:
conn.execute("DELETE FROM users WHERE id = ?", (self.id,))
conn.commit()
@classmethod
def find_by_id(cls, user_id: int) -> Optional["User"]:
with cls._connect() as conn:
row = conn.execute(
"SELECT id, name, email FROM users WHERE id = ?", (user_id,)
).fetchone()
return cls(*row) if row else None
@classmethod
def find_all(cls) -> List["User"]:
with cls._connect() as conn:
rows = conn.execute("SELECT id, name, email FROM users").fetchall()
return [cls(*row) for row in rows]
# Uso
user = User(name="Alice", email="alice@example.com")
user.save()
print(User.find_by_id(user.id))
Java
import java.sql.*;
import java.util.*;
public class User {
private static String dbUrl = "jdbc:sqlite:app.db";
private int id;
private String name;
private String email;
public User() {}
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public void save() {
try (Connection conn = DriverManager.getConnection(dbUrl)) {
if (id == 0) {
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO users (name, email) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS);
ps.setString(1, name);
ps.setString(2, email);
ps.executeUpdate();
ResultSet keys = ps.getGeneratedKeys();
keys.next();
id = keys.getInt(1);
} else {
PreparedStatement ps = conn.prepareStatement(
"UPDATE users SET name = ?, email = ? WHERE id = ?");
ps.setString(1, name);
ps.setString(2, email);
ps.setInt(3, id);
ps.executeUpdate();
}
} catch (SQLException e) { throw new RuntimeException(e); }
}
public void delete() {
try (Connection conn = DriverManager.getConnection(dbUrl)) {
PreparedStatement ps = conn.prepareStatement("DELETE FROM users WHERE id = ?");
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) { throw new RuntimeException(e); }
}
public static User findById(int id) {
try (Connection conn = DriverManager.getConnection(dbUrl)) {
PreparedStatement ps = conn.prepareStatement("SELECT id, name, email FROM users WHERE id = ?");
ps.setInt(1, id);
ResultSet rs = ps.executeQuery();
if (rs.next()) return new User(rs.getInt(1), rs.getString(2), rs.getString(3));
} catch (SQLException e) { throw new RuntimeException(e); }
return null;
}
public static List<User> findAll() {
List<User> users = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(dbUrl)) {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, name, email FROM users");
while (rs.next()) {
users.add(new User(rs.getInt(1), rs.getString(2), rs.getString(3)));
}
} catch (SQLException e) { throw new RuntimeException(e); }
return users;
}
public int getId() { return id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
// Uso
User user = new User();
user.setName("Alice");
user.save();
System.out.println(User.findById(user.getId()));
JavaScript
class User {
static db = null; // Conexión de base de datos inyectada
constructor({ id = null, name = '', email = '' } = {}) {
this.id = id;
this.name = name;
this.email = email;
}
async save() {
if (this.id === null) {
const result = await User.db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
this.name, this.email
);
this.id = result.lastID;
} else {
await User.db.run(
'UPDATE users SET name = ?, email = ? WHERE id = ?',
this.name, this.email, this.id
);
}
return this;
}
async delete() {
await User.db.run('DELETE FROM users WHERE id = ?', this.id);
}
static async findById(userId) {
const row = await User.db.get('SELECT id, name, email FROM users WHERE id = ?', userId);
return row ? new User(row) : null;
}
static async findAll() {
const rows = await User.db.all('SELECT id, name, email FROM users');
return rows.map(row => new User(row));
}
}
// Uso
const user = new User({ name: 'Alice', email: 'alice@example.com' });
await user.save();
const found = await User.findById(user.id);
console.log(found);
Explicación
Un objeto Active Record combina:
- Datos de dominio: Campos que mapean a columnas de base de datos
- Lógica de persistencia: Métodos como
save(),delete(), yfind()que ejecutan SQL - Validación: Reglas de negocio chequeadas antes de persistir
La clase es tanto un modelo de dominio como un gateway a la base de datos. Esta simplicidad es su fortaleza y debilidad.
Variantes
| Variante | Descripción | Caso de Uso |
|---|---|---|
| Classic Active Record | Objeto posee su persistencia | Rails, Django ORM |
| Data Mapper | Clase mapper separada maneja persistencia | Hibernate, SQLAlchemy |
| Repository | Abstracción tipo colección sobre persistencia | Aggregates DDD |
| Table Data Gateway | Métodos estáticos en clase, no instancias | Utilidades CRUD simples |
Lo que funciona
- Mantén validaciones en el modelo. Chequea constraints antes de guardar y lanza errores significativos.
- Usa callbacks con moderación. Hooks
before_saveyafter_createcrean flujo de control invisible. - Scopea queries.
User.where(active=True)es más seguro que SQL raw en lógica de negocio. - Lazy load asociaciones. Carga registros relacionados solo cuando se acceden, no en cada fetch.
- Evita lógica de negocio en la base de datos. Stored procedures acoplan tu código al vendor de BD.
Errores Comunes
- Fat models con 500 líneas de código. Separa lógica de negocio en service objects.
- N+1 queries al iterar sobre asociaciones. Usa eager loading (
select_related,includes). - Acceso a base de datos en tests unitarios. Active Record hace esto difícil. Usa SQLite in-memory o mocks.
- Validación en controllers en lugar del modelo. El modelo es el lugar autoritativo para reglas.
- Mutar durante iteración. Modificar una colección mientras iteras causa comportamiento indefinido.
Ejemplos del Mundo Real
Ruby on Rails
User.create(name: "Alice") crea un registro, lo valida y lo persiste en una llamada. Asociaciones como user.posts se cargan lazy.
Django ORM
user.save() y User.objects.get(id=1) son operaciones Active Record. Django agrega managers (objects) para queries de colección.
Laravel Eloquent
User::find(1) y $user->save() siguen Active Record. Eloquent también soporta relaciones, scopes y query builders.
Puntos Clave
- Aplica patrón active record cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.
Related Resources
Patrón Data Access Object (DAO)
Abstrae y encapsula todo acceso a una fuente de datos exponiendo una interfaz limpia mientras oculta los detalles de persistencia de la lógica de negocio.
PatternPatrón Data Mapper
Separa objetos de dominio en memoria de la base de datos delegando la persistencia a una capa de mappers dedicada, manteniendo los modelos framework-agnostic.
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
Preguntas frecuentes
- Cuál es la diferencia entre Active Record y Data Mapper?
- Active Record pone métodos de persistencia en el objeto de dominio. [Data Mapper](/patterns/data-mapper-pattern/) usa una clase separada para mapear objetos a la base de datos, manteniendo el modelo...
- Active Record es un anti-pattern?
- No, pero es una mala elección para dominios complejos. Brilla en aplicaciones CRUD-heavy y prototipado rápido.
- Cómo testeo objetos Active Record sin base de datos?
- Usa una base de datos SQLite in-memory para tests, o refactoriza persistencia en una capa separada que puede ser mockeada.