Versionierung in MongoDB
Die meisten Datenbanken bilden einen Ist-Zustand ab. Ein Kunde hat eine Adresse, einen Namen, ein Geburtsdatum – fertig. Doch was passiert, wenn sich die Adresse ändert? In klassischen Systemen wird der alte Wert überschrieben, und die Geschichte geht verloren. Aber manchmal brauchen wir genau diese Geschichte: Wann ist der Kunde umgezogen? Wie war die alte Adresse für Rechnungen? Welcher Preis gilt zu einem bestimmten Zeitpunkt?
Die flexible Dokumentstruktur von MongoDB ermöglicht elegante Versionierungsansätze, die ohne zusätzliche Tabellen oder komplexe Joins auskommen. Statt Daten zu überschreiben, können wir sie erweitern – und dabei die gewohnte Entwicklererfahrung beibehalten.
Wo Versionierung wirklich Sinn ergibt
Nützlich ist der Versionierungsansatz von MongoDB für folgende Anwendungsfelder:
- Kundenstammdaten: Ein klassisches Beispiel: Ein Kunde zieht um, aber für steuerliche Zwecke müssen Sie wissen, wo er zum Zeitpunkt X gewohnt hat. Versicherungsunternehmen brauchen die Adressdaten zum Zeitpunkt des Vertragsabschlusses, nicht die aktuelle Adresse.
- Preisdaten: In E-Commerce-Systemen: Der Kunde bestellt heute, aber die Lieferung erfolgt nächste Woche. Zwischenzeitlich ändert sich der Preis. Welcher Preis gilt für die Bestellung? Ohne Versionierung ein Problem, mit Versionierung eine einfache Abfrage.
- Compliance und Audit: Gesetze oder branchenspezifische Regelungen verlangen oft eine lückenlose Dokumentation von Datenänderungen. Wer hat wann was geändert? Das ist mit Versionierung automatisch dokumentiert.
- Konfigurationsdaten: In DevOps-Umgebungen ändern sich Konfigurationen ständig. Die Möglichkeit, auf eine vorherige Version zurückzurollen, kann produktionskritisch sein.
Bis dato haben Entwickler oft mit „Zeitscheiben“ gearbeitet – separate Tabellen mit Gültigkeitszeiträumen, komplexe Views und aufwendige Queries. MongoDB macht das überflüssig.
Embedded Versioning: Wenn das Array die Geschichte erzählt
Der einfachste Ansatz ist, Versionen direkt im Dokument zu speichern. Betrachten wir Adressdaten als Array innerhalb eines Kunden-Dokuments:
{
"_id": ObjectId("..."),
"customerId": "CUST001",
"name": "Max Mustermann",
"email": "max@mustermann.de",
"addresses": [
{
"street": "Alte Straße 1",
"city": "München",
"zipCode": "80331",
"validFrom": ISODate("2020-01-01"),
"validTo": ISODate("2023-05-15"),
"isCurrent": false
},
{
"street": "Neue Allee 42",
"city": "München",
"zipCode": "80333",
"validFrom": ISODate("2023-05-16"),
"validTo": null,
"isCurrent": true
}
]
}
Das Schöne an diesem Ansatz: Die komplette Adresshistorie lebt im Kundendokument. Keine Joins, keine zusätzlichen Collections, keine komplizierten Queries. Aber wie bekommen wir die aktuelle Adresse elegant in unsere .NET-Anwendung?
Aggregation: Flattening für die Anwendung
Mit der Aggregation Pipeline von MongoDB können wir die aktuelle Adresse direkt „flatten“ und der Anwendung ein sauberes Objekt liefern:
public class CustomerWithCurrentAddress
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("customerId")]
public string CustomerId { get; set; }
[BsonElement("name")]
public string Name { get; set; }
[BsonElement("email")]
public string Email { get; set; }
// Geflatten durch Aggregation
[BsonElement("currentStreet")]
public string CurrentStreet { get; set; }
[BsonElement("currentCity")]
public string CurrentCity { get; set; }
[BsonElement("currentZipCode")]
public string CurrentZipCode { get; set; }
}
public async Task<CustomerWithCurrentAddress> GetCustomerWithCurrentAddressAsync(string customerId)
{
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument("customerId", customerId)),
new BsonDocument("$unwind", "$addresses"),
new BsonDocument("$match", new BsonDocument("addresses.isCurrent", true)),
new BsonDocument("$project", new BsonDocument
{
{ "customerId", 1 },
{ "name", 1 },
{ "email", 1 },
{ "currentStreet", "$addresses.street" },
{ "currentCity", "$addresses.city" },
{ "currentZipCode", "$addresses.zipCode" }
})
};
var result = await _collection
.Aggregate<CustomerWithCurrentAddress>(pipeline)
.FirstOrDefaultAsync();
return result;
}
Die Aggregation macht Folgendes: Sie nimmt das Kunden-Dokument, „unwinded“ das Adress-Array (macht aus jedem Array-Element ein separates Dokument), filtert nach der aktuellen Adresse und projiziert die Felder so, dass sie flach in der .NET-Klasse landen.
Für die Anwendung sieht es so aus, als gäbe es nur eine aktuelle Adresse. Die Versionierung ist komplett transparent. Trotzdem ist die komplette Historie verfügbar, wenn sie gebraucht wird.
Zeitreisen leicht gemacht
Der große Vorteil wird deutlich, wenn Sie Daten zu einem bestimmten Zeitpunkt brauchen. Mit dem Adress-Array-Ansatz können Sie elegante Zeitreise-Queries schreiben:
public async Task<Address> GetCustomerAddressAtDateAsync(string customerId, DateTime targetDate)
{
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument("customerId", customerId)),
new BsonDocument("$unwind", "$addresses"),
new BsonDocument("$match", new BsonDocument
{
{ "addresses.validFrom", new BsonDocument("$lte", targetDate) },
{ "$or", new BsonArray
{
new BsonDocument("addresses.validTo", new BsonDocument("$gte", targetDate)),
new BsonDocument("addresses.validTo", BsonNull.Value)
}
}
}),
new BsonDocument("$replaceRoot", new BsonDocument("newRoot", "$addresses"))
};
var result = await _collection
.Aggregate<Address>(pipeline)
.FirstOrDefaultAsync();
return result;
}
Diese Aggregation findet die Adresse, die zum angegebenen Zeitpunkt gültig war. Perfekt für Compliance-Berichte oder forensische Analysen – Sie können jeden beliebigen Zeitpunkt in der Vergangenheit abfragen und bekommen die damals gültige Adresse.
Der MongoDB-Vorteil: Ein Objekt, eine Geschichte
Hier zeigt sich der fundamentale Unterschied zu relationalen Datenbanken. In SQL hätten Sie vermutlich so etwas:
-- Drei Tabellen für einen versionierten Kunden
CREATE TABLE Customers (
customer_id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);
CREATE TABLE Addresses (
id INT PRIMARY KEY,
customer_id VARCHAR(50),
street VARCHAR(255),
city VARCHAR(255),
zip_code VARCHAR(10),
valid_from DATE,
valid_to DATE,
is_current BOOLEAN,
FOREIGN KEY (customer_id) REFERENCES Customers(customer_id)
);
CREATE TABLE CustomerVersions (
id INT PRIMARY KEY,
customer_id VARCHAR(50),
version_number INT,
name VARCHAR(255),
email VARCHAR(255),
created_at TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES Customers(customer_id)
);
Allein für einen Kunden mit versionierter Adresse brauchen Sie drei Tabellen, Foreign Keys, komplexe Joins. Und das wird mit jedem weiteren versionierten Feld exponentiell komplexer.
In MongoDB ist es ein Dokument. Ein Objekt. Eine Collection. Die Versionierung ist natürlich in der Dokumentstruktur abgebildet, nicht über komplizierte Tabellenbeziehungen erzwungen.
Der Kompromiss zwischen Tradition und Event-Streaming
Diese Art der Versionierung in MongoDB ist interessant, weil sie eine Brücke zwischen klassischer Datenhaltung und modernem Event-Streaming schlägt. In traditionellen Systemen überschreiben Sie Daten und verlieren die Historie. In Event-Streaming-Systemen wie Kafka behandeln Sie jede Änderung als Event und rekonstruieren den aktuellen Zustand aus der Event-Historie.
Der Versionierungsansatz von MongoDB nimmt das Beste aus beiden Welten: Sie behalten die Einfachheit der traditionellen Objektmodellierung bei – ein Kunde ist ein Dokument, nicht eine Sammlung von Events. Gleichzeitig bewahren Sie die Historie wie in Event-Sourcing-Systemen. Der Unterschied: Sie müssen keine Events zu einem aktuellen Zustand aggregieren, denn der aktuelle Zustand ist explizit verfügbar.
Das macht diese Lösung pragmatisch für Teams, die nicht gleich zu Event-Sourcing wechseln wollen, aber trotzdem von auditierbarer Datenhistorie profitieren möchten (oder müssen). Sie bekommen Event-Streaming-ähnliche Vorteile ohne die Komplexität von Command Query Responsibility Segregation (CQRS) oder Event-Store-Management.
Meine Erfahrung: MongoDB macht Versionierung natürlich
Nach Jahren der Arbeit mit relationalen Datenbanken und ihren Versionierungsherausforderungen ist MongoDB eine Befreiung. Keine künstlichen Constraints und Normalisierungen, die relationale Datenbanken uns bescheren.
Die Dokumentstruktur von MongoDB macht Versionierung zu dem, was sie sein sollte: ein natürlicher Teil der Datenmodellierung. Sie denken in Objekten, nicht in Tabellen. Sie versionieren Entitäten, nicht Relationen.
Das bedeutet nicht, dass MongoDB für jeden Versionierungsfall die beste Lösung ist. Wenn Sie komplexe relationale Analysen über versionierte Daten machen müssen, können SQL-Systeme mit Temporal Tables überlegen sein. Aber für die allermeisten Anwendungsfälle – Audit-Trails, Compliance, historische Berichte – ist der Ansatz von MongoDB eleganter und wartbarer.
Die Zukunft der Datenmodellierung liegt in der Flexibilität. MongoDB ermöglicht Ihnen diese Flexibilität, ohne dabei auf Performance oder Skalierbarkeit zu verzichten. Versionierung wird vom notwendigen Übel zur natürlichen Erweiterung Ihrer Anwendungslogik.