Natürlich behandelt der Artikel technisch das Problem aus der Titelzeile. Aber zunächst wird grundlegend erörtert, was das Problem mit der Relation ist. Ich nenne es das Rechnungsparadoxon.
Vor ganz vielen Jahren hat der Unternehmer für die Rechnung einen Block genommen, mit Durchschlagpapier und fein säuberlich notiert:
- Oben den Empfänger und das Datum
- in der Mitte die zu verrechnenden Artikel bzw Positionen
- Unten die Summe und ggf einen Betrag Danken erhalten Stempel.
Auf dem Block war jedes Blatt durchnummeriert. Hat der Azubi sich verschrieben, musste er den Zettel wegwerfen. Im Nummernkreis fehlt dann eine Rechnungsnummer.
Mit der Erfindung von relationalen Datenbank Modellen wurden dazu zwei Tabellen geschaffen, Rechnung und Positionen.
Nun stellte sich schon anfangs die Frage, wie deklariert man die Relation eindeutig und unumstößlich? Soll ja nicht die Position auf einer anderen Rechnung auftauchen. Eine Datenbank nimmt dazu einen Primärschlüssel und in der zweiten Tabellen einen Fremdschlüssel. Nur vom welchen Typ? Und wer ist der Schlüsselmeister?
Im klassischen Client Server Szenario definiert sich das am Ort des Geschehens. Offen ist, soll der Primary Key auf dem SQL Server oder im Client generiert und verwaltet werden?
In den Anfängen tendierten Softwarearchitekten dazu am Desktop einen ID zu generieren und dann die Relationen in der Positionsdatenbank damit zu erzeugen. Probates Mittel dazu die GUID. So eine 128 Bit ID soll einmalig sein und steht als Feldtyp im SQL Server bereit. a62760b8-d2d2-4946-8ed6-684dc6fd3640 ist aber recht lange und aus Performance Gründen wenig ideal.
Der Vorteil ist, man kann komplett am Client seine Rechnung generieren und wenn man sie nicht braucht verwerfen oder andernfalls zur Datenbank schicken, wo sie persistent abgelegt wird.
Eine Randnotiz sei mir dazu erlaubt. Während man die Rechnung mit seinen Positionen zusammenbaut, hängt man voll und ganz am Status im Arbeitsspeicher. Etwas was z.B. REST Service Entwickler zum Unding erklärt haben. Aber ganz ohne Status geht es auch nicht.
Eine weitere Möglichkeit besteht darin sich die Verantwortung mit dem Server zu teilen. Erst wird in der Tabelle ein Datensatz für Rechnung angelegt. Die ID Spalte ist dann idealerweise vom Typ Integer und inkrementiert automatisch. Die neue Auto ID muss dann natürlich dem Client mitgeteilt werden, damit dieser Positionsdatensätze mit passenden Fremdschlüssel anlegen kann. Detailproblem: was macht man mit der Summenspalte in Rechnung? Diese erhöht sich ja laufen mit jeder neuen Position? Ein Trigger mit Stored Procedure oder am Client berechnen und dann die Rechnungstabelle updaten? Außerdem fehlen ID Werte wenn die Rechnung verworfen werden muss.
Im Web Umfeld erscheint das alles wenig Ideal. Steigende Anzahl von Clients mit recht lockerer HTTP Anbindung erzeugen neue Aufgabenstellungen. Soll der Status des Rechnungsobjektes am Client oder am Server gehalten werden? Die sogenannten SPA Single Page Application Frameworks tendieren zum Client. ASP.NET MVC rendert am Server mit häufigen reloads um jede Änderung am Web Server verarbeiten zu können.
Soweit der Status, eine ideale Lösung die überall passt ist nicht in Sicht.
Microsoft hat das Rechnungsparadoxon in seine Datenbank Framework Entity Framework mit aufgenommen. Man kann die Rechnung als Objekt behandeln und die Relation und seine magische Key Generierung im Code völlig ignorieren. Dazu programmiert man sein Model
1: public class Rechnung
2: {
3: public Rechnung()
4: {
5: Positionen = new List<Positionen>();
6: }
7: public int ID { get; set; }
8: public DateTime Date { get; set; }
9: public string KopfText { get; set; }
10: public int KundeID { get; set; }
11: public float Summe { get; set; }
12: virtual public List<Positionen> Positionen { get; set; }
13: }
14: public class Positionen
15: {
16: public int PositionenID { get; set; }
17: public int RechnungID { get; set; }
18: public string Text { get; set; }
19: public int Anzahl { get; set; }
20: public float Preis { get; set; }
21: virtual public Rechnung Rechnung { get; set; }
22: }
Die Rechnung erhält als virtuelle Eigenschaft eine Liste von Positionen, die im Konstruktor initalisiert werden, um keinen NULL Wert mit zu ziehen. Die Tabelle Positionen definiert über RechnungID den Fremdschlüssel und über das virtuelle Property die Relation zur Rechnung. Die Benennung Tabelle + ID folgt einer EF Konvention.
Hinweis: Meist sieht man als Typ der Liste häufig Hashtable. Im weiteren Vorgang benötigt die Lösung zum Rechnungsparadoxon Zugriff per Indexer, den der Typ List zur Verfügung stellt.
Als .NET Entwickler kann man nun das Papierformular als Objekt in einem Rutsch erstellen. Man beachte, das die ID Felder nicht gefüllt werden und trotzdem die Relation gepflegt ist.
1: var ctx = new ModelRechnung();
2: var r = new Rechnung()
3: {
4: Date = DateTime.Now,
5: KopfText = "sometext",
6: Positionen = new List<Positionen>()
7: { new Positionen(){Anzahl=2,Text="postext"},
8: new Positionen(){Anzahl=3,Text="postextweiters"},
9: }
10: };
Wenn Entity Framework beauftragt wird, die Daten endgültig zu sichern, werden die SQL Statements so erzeugt, das die Relationen in der Datenbank passen. Also die Primary Key ID’s werden passend von der Datgenbank erzeugt und zugewiesen.
1: ctx.Rechnung.Add(r);
2: ctx.SaveChanges();
Das wäre mit blankem ADO.NET ein Riesenaufwand.
Das funktioniert auch ganz gut anders rum. Also eine Rechnung samt Positionen per Entity Framework laden, einzelne Felder ändern oder Positionen ergänzen und speichern. Entity Framework hat ein automatisches Change Tracking um die geänderten Zeilen verwalten zu können.
Bei ASP.NET core Razor Web Anwendungen bin ich bisher mit automatischen Change Tracking nicht zufrieden. Websites haben ein Problem mit Status. Das HTML UI und der Server befinden sich eben in getrennten Räumen und ein Objekt kann nicht geshared werden. Folgender C# Code ist ein einfacher Auszug einer möglichen Lösung. Alle neuen Positionen haben ohnehin den Status Added und werden per SQL Insert bei SaveChanges eingefügt. Den anderen Positionen wird sozusagen vorsichtshalber unterstellt verändert (Modified) worden zu sein.
1: _context.Attach(Rechnung).State = EntityState.Modified;
2: foreach (var item in Rechnung.Positionen)
3: {
4: if (item.PositionenID>0)
5: {
6: _context.Attach(item).State = EntityState.Modified;
7: }
8: }
Diese Lösung ist für ganz wenige Positionen wie sie in einer Rechnung auftreten akzeptabel. Man kann sich natürlich auch damit beschäftigen, ob ein Eintrag wirklich verändert worden ist. Das ist je nach UI Technologie recht aufwändig. Frameworks wie Angular übernehmen diese Aufgabe am HTML Client, bringen aber an anderer Stelle Komplexität.
Rechnungen werden im Entity Framework als komplettes Objekt gehalten. Update, Insert, Select und Delete wird vom EF verwaltet und die passenden SQL Statements automatisch generiert.