← Volver a los artículos
Rendimiento y bases de datos

Congelar el tiempo: qué son los snapshots de base de datos y cómo simplifican la vida del desarrollador

Una guía práctica del patrón Snapshot: copias inmutables del estado de un agregado para auditoría, rendimiento en Event Sourcing e informes puntuales, con un ejemplo práctico en Laravel.

Congelar el tiempo: qué son los snapshots de base de datos y cómo simplifican la vida del desarrollador
Artículo destacado ↗

Congelar el tiempo: qué son los snapshots de base de datos y cómo simplifican la vida del desarrollador

Si trabajas con bases de datos, especialmente en el contexto de un Domain-Driven Design (DDD) complejo, programación orientada a eventos o analítica, probablemente te hayas enfrentado a este problema: ¿cómo trabajar de forma eficiente con un objeto cuyo estado cambia decenas de veces?

Imagina una entidad Order. Atraviesa un ciclo de vida: creada → pagada → enviada → entregada → devuelta. Cada estado es un cambio en la base de datos. El historial de cambios puede ser enorme. Pero ¿qué pasa si necesitas:

  • Restaurar el estado del pedido en una fecha específica?
  • Analizar cómo cambió su precio total con el tiempo?
  • Optimizar las consultas para un agregado con una estructura compleja y profunda?

Aquí es donde entra en juego el patrón Snapshot.

¿Qué es un Snapshot?

Un Snapshot es una copia inmutable del estado de un objeto (un agregado en términos de DDD) en un momento específico en el tiempo. No es un registro de cambios (delta), sino una «fotografía» completa de todos los datos importantes del objeto en ese momento.

Analogía de la vida real: Es como guardar una copia completa de un documento Contract_v1.2_final_updated_2024-12-01.docx antes de hacer ediciones. Siempre puedes volver a esa versión exacta.

¿Por qué son necesarios? 3 razones clave

  1. Registro histórico y auditoría. El caso de uso más obvio. Puedes saber con precisión cuál era el saldo de un usuario, la composición de un pedido o el texto de un artículo hace exactamente un mes. Esto es crítico para sistemas financieros y sistemas con requisitos de auditoría legal (por ejemplo, GDPR, SOX).
  2. Mejora del rendimiento (Event Sourcing). En la arquitectura Event Sourcing, el estado de un objeto se recrea aplicando secuencialmente todos los eventos de su historial (OrderCreated, ItemAdded, OrderPaid). Si hay miles de eventos, reproducirlos todos en cada consulta es costoso. La solución: guardar periódicamente un snapshot del agregado (versión #100, #200, etc.). Para restaurar el estado más reciente, cargas el último snapshot (#300) y aplicas solo los eventos posteriores (#301 a #350). Esto reduce drásticamente la carga.
  3. Captura puntual para informes. Para construir un informe analítico (por ejemplo, «Ingresos del último trimestre»), necesitas datos fijados al final del período. El uso de snapshots garantiza que los cambios posteriores no distorsionen la imagen histórica.

¿Cómo trabajar con ellos? Práctica con Laravel (con Eloquent)

Implementemos un sistema de snapshots sencillo para una entidad Invoice.

1. Estructura de la base de datos

// Tabla principal invoices Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->string('number'); $table->decimal('amount', 10, 2); $table->string('status'); // draft, paid, cancelled $table->json('items'); // Array de elementos como JSON $table->timestamps(); }); // Tabla para snapshots Schema::create('invoice_snapshots', function (Blueprint $table) { $table->id(); $table->foreignId('invoice_id')->constrained()->cascadeOnDelete(); $table->json('state'); // TODO el estado de la factura en el momento del snapshot $table->string('reason')->nullable(); // Motivo de creación (por ejemplo, "before_payment", "monthly_archive") $table->timestamp('snapshot_at'); // El momento en que se capturó el estado $table->timestamps(); });

2. Modelo y creación del snapshot

// app/Models/Invoice.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class Invoice extends Model { protected $casts = [ 'items' => 'array', ]; // Relación con los snapshots public function snapshots(): HasMany { return $this->hasMany(InvoiceSnapshot::class); } // Método para crear un snapshot public function createSnapshot(string $reason = null): InvoiceSnapshot { // Recopilar TODOS los datos cruciales $state = [ 'number' => $this->number, 'amount' => $this->amount, 'status' => $this->status, 'items' => $this->items, // Puedes incluir datos relacionados si son críticos para el contexto // 'client_name' => $this->client->name, 'created_at' => $this->created_at->toIso8601String(), 'updated_at' => $this->updated_at->toIso8601String(), ]; return $this->snapshots()->create([ 'state' => $state, 'reason' => $reason, 'snapshot_at' => now(), // o carbon->clone() ]); } } // app/Models/InvoiceSnapshot.php namespace App\Models; use Illuminate\Database\Eloquent\Model; class InvoiceSnapshot extends Model { protected $casts = [ 'state' => 'array', 'snapshot_at' => 'datetime', ]; public function invoice() { return $this->belongsTo(Invoice::class); } // Accessors auxiliares para obtener fácilmente datos desde el estado del snapshot public function getInvoiceNumberAttribute() { return $this->state['number']; } public function getInvoiceAmountAttribute() { return $this->state['amount']; } }

3. Uso en el código

// Crear una factura $invoice = Invoice::create([ 'number' => 'INV-2024-001', 'amount' => 1500.00, 'status' => 'draft', 'items' => [['name' => 'SEO Service', 'price' => 1500]], ]); // Tomar un snapshot antes de un cambio crítico (por ejemplo, el pago) $invoice->createSnapshot('before_payment'); // Aplicar el cambio $invoice->update(['status' => 'paid', 'amount' => 1470.00]); // Después de aplicar un descuento // Ahora tenemos historial: $lastSnapshot = $invoice->snapshots()->latest->first(); echo $lastSnapshot->invoice_number; // 'INV-2024-001' echo $lastSnapshot->invoice_amount; // 1500.00 (importe anterior) echo $invoice->amount; // 1470.00 (importe actual) // Encontrar snapshots en una fecha específica $snapshotsOnDate = InvoiceSnapshot::where('snapshot_at', '<=', '2024-12-01 23:59:59')->get();

Consideraciones importantes y conclusiones

  • Profundidad de datos: Decide qué entra en el state. Solo los campos del agregado o también datos relacionados (por ejemplo, el nombre del cliente). Lo último simplifica la lectura pero duplica datos.
  • Frecuencia: Los snapshots pueden crearse por eventos (antes del pago), por programación (diario) o por versión (cada N cambios).
  • Almacenamiento: JSON en la base de datos es conveniente para objetos complejos. Para analítica, los snapshots pueden enviarse a un almacenamiento separado (por ejemplo, un Data Warehouse).
  • Herramientas: Para Laravel, paquetes como spatie/laravel-activitylog facilitan el logging, y orangehill/iseed ayuda con el seeding, pero para los snapshots una implementación personalizada suele adaptarse mejor a necesidades específicas.

En resumen: Los snapshots son un patrón poderoso para trabajar con el tiempo en tu aplicación. Transforman tu base de datos de un almacén estático del «estado actual» en una máquina del tiempo para tus datos, proporcionando auditabilidad, rendimiento y fiabilidad.

Tecnologías y temas

Etiquetas del artículo

No hay artículos que coincidan con estos filtros.

¿Tienes un proyecto o una idea para discutir?

Hablemos ↗