Arbeiten mit einer Datenbank aus einer Anwendung

Am Anfang werde ich einige Probleme und Funktionen bei der Arbeit mit der Datenbank skizzieren und Löcher in Abstraktionen zeigen. Als nächstes werden wir eine einfachere Abstraktion analysieren, die auf Immunität basiert.


Der Leser sollte ein wenig mit den Mustern Active Record , Data Maper , Identity Map und Unit of Work vertraut sein.


Probleme und Lösungen werden im Zusammenhang mit ausreichend großen Projekten betrachtet, die nicht weggeworfen und schnell neu geschrieben werden können.


Identitätskarte


Das erste Problem ist das Problem der Aufrechterhaltung der Identität. Identität ist etwas, das eine Entität eindeutig identifiziert. In der Datenbank ist dies der Primärschlüssel und im Speicher der Link (Zeiger). Es ist gut, wenn Links nur auf ein Objekt verweisen.


Für Ruby ActiveRecord- Bibliotheken ist dies nicht der Fall:


post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id # true post_a.title = "foo" post_b.title != "foo" # true 

Das heißt, Wir erhalten 2 Verweise auf 2 verschiedene Objekte im Speicher.


Daher können wir Änderungen verlieren, wenn wir versehentlich mit derselben Entität arbeiten, die jedoch durch unterschiedliche Objekte dargestellt wird.


Der Ruhezustand hat eine Sitzung, in der Tat einen Cache der ersten Ebene, in dem die Zuordnung einer Entitätskennung zu einem Objekt im Speicher gespeichert wird. Wenn wir dieselbe Entität erneut anfordern, erhalten wir einen Link zu einem vorhandenen Objekt. Das heißt, Der Ruhezustand implementiert das Identity Map- Muster.


Lange Transaktionen


Aber was ist, wenn wir nicht nach Kennung auswählen? Um zu verhindern, dass der Status von Objekten und der Status der Datenbank nicht synchron sind, leeren Sie den Ruhezustand, bevor Sie eine Auswahl anfordern.
d.h. speichert schmutzige Objekte in der Datenbank, sodass die Anforderung die vereinbarten Daten liest.


Dieser Ansatz zwingt Sie dazu, die Datenbanktransaktion offen zu halten, während die Geschäftstransaktion ausgeführt wird.
Wenn der Geschäftsvorgang lang ist, ist auch der Prozess, der für die Verbindung in der Datenbank selbst verantwortlich ist, inaktiv. Dies kann beispielsweise passieren, wenn ein Geschäftsvorgang Daten über das Netzwerk anfordert oder komplexe Berechnungen durchführt.


N + 1


Das vielleicht größte „Loch“ in der ORM-Abstraktion ist das N + 1-Abfrageproblem.


Beispiel für Ruby für die ActiveRecord-Bibliothek:


 posts = Post.all # select * from posts posts.each do |post| like = post.likes.order(id: :desc).first # SELECT * FROM likes WHERE post_id = ? ORDER BY id DESC LIMIT 1 # ... end 

ORM führt den Programmierer auf die Idee, dass er einfach mit Objekten im Speicher arbeitet. Es funktioniert jedoch mit einem Dienst, der über das Netzwerk verfügbar ist, sowie beim Aufbau von Verbindungen und bei der Datenübertragung
es braucht Zeit. Selbst wenn die Anforderung 50 ms ausgeführt wird, werden 20 Anforderungen für eine Sekunde ausgeführt.


Zusätzliche Daten


Um das oben beschriebene N + 1-Problem zu vermeiden, schreiben Sie z
Anfrage :


 SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true; 

Das heißt, Zusätzlich zu den Attributen des Beitrags werden auch alle Attribute des letzten Like ausgewählt. Welcher Entität werden diese Daten zugeordnet? In diesem Fall können Sie ein Paar von der Post zurückgeben und mögen, weil Das Ergebnis enthält alle notwendigen Attribute.


Was aber, wenn wir nur einen Teil der Felder oder ausgewählte Felder ausgewählt haben, die nicht im Modell enthalten sind, z. B. die Anzahl der Veröffentlichungen? Müssen sie Entitäten zugeordnet werden? Vielleicht lassen sie nur Daten?


Staat & Identität


Betrachten Sie den js-Code:


 const alice = { id: 0, name: 'Alice' }; 

Hier erhielt die Objektreferenz den Namen alice . Weil Es ist eine Konstante, dann gibt es keine Möglichkeit, Alice ein anderes Objekt zu nennen. Gleichzeitig blieb das Objekt selbst veränderlich.


Zum Beispiel können wir einen vorhandenen Bezeichner zuweisen:


 const bob = { id: 1, name: 'Bob' }; alice.id = bob.id; 

Ich möchte Sie daran erinnern, dass eine Entität zwei Identitäten hat: einen Link und einen Primärschlüssel in der Datenbank. Und die Konstanten können auch nach dem Speichern nicht aufhören, Alice Bob zu machen.


Das Objekt, die Verbindung, zu der wir alice , erfüllt zwei Aufgaben: Es modelliert gleichzeitig Identität und Zustand. Ein Status ist ein Wert, der eine Entität zu einem bestimmten Zeitpunkt beschreibt.


Aber was ist, wenn wir diese beiden Verantwortlichkeiten trennen und unveränderliche Strukturen für den Staat verwenden?


 function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM! 

Ref - ein Behälter für einen unveränderlichen Zustand, der seinen kontrollierten Austausch ermöglicht. Ref modelliert Identität genauso wie wir Objekte benennen. Wir nennen die Wolga, aber zu jedem Zeitpunkt hat sie einen anderen unveränderlichen Zustand.


Lagerung


Betrachten Sie die folgende API:


 storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); }); 

t.get und t.create geben eine Instanz von Ref .


Wir öffnen den Geschäftsvorgang t , finden Alice anhand ihrer Kennung, erstellen Bob und geben an, dass Alice Bob als ihre Freundin betrachtet.


Objekt t steuert die Erstellung von ref .


t kann die Zuordnung von Entitätskennungen zu dem sie enthaltenden ref in sich speichern. Das heißt, kann Identity Map implementieren. In diesem Fall fungiert t als Cache. Auf Alices wiederholte Anforderung wird keine Anforderung an die Datenbank gesendet.


t kann sich den Anfangszustand von Entitäten merken, um am Ende der Transaktion zu verfolgen, welche Änderungen in die Datenbank geschrieben werden müssen. Das heißt, kann Unit of Work implementieren. Wenn die Beobachterunterstützung zu Ref hinzugefügt wird, können die Änderungen an der Datenbank bei jeder Änderung von ref . Dies sind optimistische und pessimistische Ansätze zur Behebung von Änderungen.


Mit einem optimistischen Ansatz müssen Sie die Statusversionen von Entitäten verfolgen.
Beim Ändern von der Datenbank müssen wir uns die Version merken und beim Festschreiben von Änderungen überprüfen, ob sich die Version der Entität in der Datenbank nicht von der ursprünglichen unterscheidet. Andernfalls müssen Sie den Geschäftsvorgang wiederholen. Dieser Ansatz ermöglicht die Verwendung von Gruppeneinfüge- und -löschvorgängen und sehr kurzen Datenbanktransaktionen, wodurch Ressourcen gespart werden.


Mit einem pessimistischen Ansatz stimmt eine Datenbanktransaktion vollständig mit einer Geschäftstransaktion überein. Das heißt, Wir sind gezwungen, die Verbindung zum Zeitpunkt des Abschlusses des Geschäftsvorfalls aus dem Pool zu entfernen.


Mit der API können Sie Entitäten einzeln extrahieren, was nicht sehr optimal ist. Weil Nachdem wir das Identity Map- Muster implementiert haben, können wir die preload Methode in die API eingeben:


 storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache }); 

Abfragen


Wenn wir keine langen Transaktionen wollen, können wir keine Auswahl mit einem beliebigen Schlüssel treffen, weil Der Speicher kann schmutzige Objekte enthalten und die Auswahl gibt ein unerwartetes Ergebnis zurück.


Wir können Query verwenden und alle Daten (Status) außerhalb der Transaktion abrufen und die Daten während der Transaktion erneut lesen.


 const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); }); 

Somit gibt es eine Aufteilung der Verantwortung. Bei Abfragen können wir Suchmaschinen verwenden, um das Lesen mithilfe von Replikaten zu skalieren. Und die Speicher-API funktioniert immer mit dem Hauptspeicher (Master). Natürlich enthalten die Replikate veraltete Daten. Ein erneutes Lesen der Daten in der Transaktion löst dieses Problem.


Befehle


Es gibt Situationen, in denen eine Operation ausgeführt werden kann, ohne Daten zu lesen. Ziehen Sie beispielsweise eine monatliche Gebühr von den Konten aller Kunden ab. Oder fügen Sie im Konfliktfall Daten ein und aktualisieren Sie sie (Upsert).


Bei Leistungsproblemen kann das Bundle aus Storage and Query durch einen solchen Befehl ersetzt werden.


Kommunikation


Wenn Entitäten zufällig aufeinander verweisen, ist es schwierig, beim Ändern Konsistenz zu gewährleisten. Beziehungen versuchen zu vereinfachen, zu rationalisieren, unnötig aufzugeben.


Aggregate sind eine Möglichkeit, Beziehungen zu organisieren. Jedes Aggregat verfügt über eine Stammentität und verschachtelte Entitäten. Jede externe Entität kann nur auf die Wurzel des Aggregats verweisen. Die Wurzel stellt die Integrität der gesamten Einheit sicher. Eine Transaktion kann keine Aggregatgrenze überschreiten, dh das gesamte Aggregat ist an der Transaktion beteiligt.


Ein Aggregat kann beispielsweise aus der Fastenzeit (Wurzel) und ihren Übersetzungen bestehen. Oder der Orden und seine Positionen.


Unsere API arbeitet mit ganzen Aggregaten. Gleichzeitig liegt die referenzielle Integrität zwischen den Aggregaten bei der Anwendung. Die API unterstützt das verzögerte Laden von Links nicht.
Aber wir können die Richtung der Beziehungen wählen. Betrachten Sie die Eins-zu-Viele-Beziehung Benutzer - Beitrag. Wir können die Benutzer-ID in der Post speichern, aber wird es bequem sein? Wir erhalten viel mehr Informationen, wenn wir eine Reihe von Post-IDs im Benutzer speichern.


Fazit


Ich habe die Probleme bei der Arbeit mit der Datenbank hervorgehoben und die Möglichkeit der Verwendung der Immunität aufgezeigt.
Das Format des Artikels erlaubt es nicht, das Thema im Detail zu enthüllen.


Wenn Sie an diesem Ansatz interessiert sind, achten Sie auf meine Buch- App von Grund auf , in der die Erstellung einer Webanwendung von Grund auf mit Schwerpunkt auf Architektur beschrieben wird. Es versteht SOLID, Clean Architecture und Arbeitsmuster mit der Datenbank. Die Codebeispiele im Buch und in der Anwendung selbst sind in der Clojure-Sprache verfasst, die von den Ideen der Immunität und der Bequemlichkeit der Datenverarbeitung durchdrungen ist.

Source: https://habr.com/ru/post/de433100/


All Articles