← Retour aux articles
Performance et bases de données

Geler le temps : que sont les snapshots de base de données et comment simplifient-ils la vie d'un développeur

Un guide pratique du pattern Snapshot : copies immuables de l'état d'un agrégat pour l'audit, la performance d'Event Sourcing et le reporting ponctuel, avec un exemple Laravel.

✦
Article en vedette ↗

Geler le temps : que sont les snapshots de base de données et comment simplifient-ils la vie d'un développeur

Si vous travaillez avec des bases de données, en particulier dans le contexte d'un Domain-Driven Design (DDD) complexe, de la programmation événementielle ou de l'analytique, vous avez probablement été confronté à ce problème : comment travailler efficacement avec un objet dont l'état change des dizaines de fois ?

Imaginez une entité Order. Elle traverse un cycle de vie : créée → payée → expédiée → livrée → retournée. Chaque statut est un changement dans la base de données. L'historique des modifications peut être énorme. Mais que faire si vous devez :

  • Restaurer l'état de la commande à une date spécifique ?
  • Analyser comment son prix total a évolué dans le temps ?
  • Optimiser les requêtes pour un agrégat à la structure complexe et profonde ?

C'est là que le pattern Snapshot (instantané) vient à la rescousse.

Qu'est-ce qu'un Snapshot ?

Un Snapshot est une copie immuable de l'état d'un objet (un agrégat en termes de DDD) à un moment précis dans le temps. Ce n'est pas un journal de modifications (delta), mais une « photographie » complète de toutes les données importantes de l'objet à ce moment-là.

Analogie de la vie réelle : C'est comme sauvegarder une copie complète d'un document Contract_v1.2_final_updated_2024-12-01.docx avant d'y apporter des modifications. Vous pouvez toujours revenir à cette version exacte.

Pourquoi sont-ils nécessaires ? 3 raisons clés

  1. Enregistrement historique et audit. Le cas d'usage le plus évident. Vous pouvez savoir précisément quel était le solde d'un utilisateur, la composition d'une commande ou le texte d'un article il y a exactement un mois. C'est critique pour les systèmes financiers et les systèmes soumis à des exigences d'audit légal (par exemple, RGPD, SOX).
  2. Gain de performance (Event Sourcing). Dans l'architecture Event Sourcing, l'état d'un objet est recréé en appliquant séquentiellement tous les événements de son historique (OrderCreated, ItemAdded, OrderPaid). S'il y a des milliers d'événements, les rejouer tous à chaque requête est coûteux. La solution : sauvegarder périodiquement un snapshot de l'agrégat (version #100, #200, etc.). Pour restaurer l'état le plus récent, vous chargez le dernier snapshot (#300) et appliquez uniquement les événements qui le suivent (#301 à #350). Cela réduit considérablement la charge.
  3. Capture ponctuelle pour le reporting. Pour construire un rapport analytique (par exemple, « Revenus du dernier trimestre »), vous avez besoin de données figées à la fin de la période. L'utilisation de snapshots garantit que les modifications ultérieures ne déformeront pas l'image historique.

Comment travailler avec eux ? Mise en pratique avec Laravel (et Eloquent)

Implémentons un système de snapshots simple pour une entité Invoice.

1. Structure de la base de données

// Table principale 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'); // Tableau d'éléments au format JSON $table->timestamps(); }); // Table pour les snapshots Schema::create('invoice_snapshots', function (Blueprint $table) { $table->id(); $table->foreignId('invoice_id')->constrained()->cascadeOnDelete(); $table->json('state'); // TOUT l'état de la facture au moment du snapshot $table->string('reason')->nullable(); // Raison de la création (par exemple, "before_payment", "monthly_archive") $table->timestamp('snapshot_at'); // Le moment où l'état a été capturé $table->timestamps(); });

2. Modèle et création de 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', ]; // Relation avec les snapshots public function snapshots(): HasMany { return $this->hasMany(InvoiceSnapshot::class); } // Méthode pour créer un snapshot public function createSnapshot(string $reason = null): InvoiceSnapshot { // Collecter TOUTES les données cruciales $state = [ 'number' => $this->number, 'amount' => $this->amount, 'status' => $this->status, 'items' => $this->items, // Vous pouvez inclure des données liées si elles sont critiques pour le contexte // '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(), // ou 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); } // Accesseurs helpers pour obtenir facilement des données depuis l'état du snapshot public function getInvoiceNumberAttribute() { return $this->state['number']; } public function getInvoiceAmountAttribute() { return $this->state['amount']; } }

3. Utilisation dans le code

// Créer une facture $invoice = Invoice::create([ 'number' => 'INV-2024-001', 'amount' => 1500.00, 'status' => 'draft', 'items' => [['name' => 'SEO Service', 'price' => 1500]], ]); // Prendre un snapshot avant un changement critique (par exemple, le paiement) $invoice->createSnapshot('before_payment'); // Appliquer le changement $invoice->update(['status' => 'paid', 'amount' => 1470.00]); // Après application d'une remise // Maintenant nous avons un historique : $lastSnapshot = $invoice->snapshots()->latest->first(); echo $lastSnapshot->invoice_number; // 'INV-2024-001' echo $lastSnapshot->invoice_amount; // 1500.00 (ancien montant) echo $invoice->amount; // 1470.00 (montant actuel) // Trouver les snapshots à une date spécifique $snapshotsOnDate = InvoiceSnapshot::where('snapshot_at', '<=', '2024-12-01 23:59:59')->get();

Considérations importantes et points clés

  • Profondeur des données : Décidez ce qui entre dans l'état. Seulement les champs de l'agrégat ou aussi les données liées (par exemple, le nom du client). Ce dernier simplifie la lecture mais duplique les données.
  • Fréquence : Les snapshots peuvent être créés sur événements (avant paiement), selon un planning (quotidien), ou par version (tous les N changements).
  • Stockage : Le JSON dans la base de données est pratique pour les objets complexes. Pour l'analytique, les snapshots peuvent être envoyés vers un stockage séparé (par exemple, un Data Warehouse).
  • Outillage : Pour Laravel, des packages comme spatie/laravel-activitylog facilitent le logging, et orangehill/iseed aide au seeding, mais pour les snapshots, une implémentation personnalisée répond souvent mieux aux besoins spécifiques.

En résumé : Les snapshots sont un pattern puissant pour travailler avec le temps dans votre application. Ils transforment votre base de données d'un stockage statique de « l'état actuel » en une machine à remonter le temps pour vos données, offrant auditabilité, performance et fiabilité.

Technologies et sujets

Tags de l'article

Aucun article ne correspond à ces filtres.

Vous avez un projet ou une idée à discuter ?

Parlons-en ↗