DDD und CQRS sind die goldene Kombination

Sind Sie bei Ihrer täglichen Arbeit auf folgende Situationen gestoßen:

  1. Bei der Verwendung einer vorhandenen Schnittstelle für die Geschäftsentwicklung kam es nach der Online-Schaltung zu gravierenden Leistungsproblemen. Der Chef fragte ihn öffentlich: „Warum verwenden Sie nicht die Cache-Schnittstelle? Diese Schnittstelle nutzt alle die Datenbank. Wie können Sie dem widerstehen!“

  2. Bei der Entwicklung einer Backend-Verwaltungsfunktion heißt es im Geschäftsfeedback, dass die Daten immer falsch sind. Nach dem Vergleich wurde festgestellt, dass der Cache nicht mit der Datenbank übereinstimmt. Warum sollte die Cache-Schnittstelle verwendet werden? Sind Sie in Gedanken versunken?

  3. Das Produkt erforderte das Hinzufügen neuer Funktionen zu xxx, und das Codieren, Testen und Online-Schalten erfolgte in einem Rutsch. Schließlich wurde festgestellt, dass ein anderer Prozess blockiert war und eine Ausnahme auftrat, die zurückgesetzt werden musste!

  4. In einem Szenario mit hoher Parallelität ist die Datenbank zum Flaschenhals des Systems geworden. Abfragen ohne Indizierung können damit nicht umgehen, und Aktualisierungen mit Indizierung können damit nicht umgehen. Wie sollen wir damit umgehen?

  5. Mit zunehmender Datenmenge wird das System insbesondere bei komplexen Abfrageszenarien mit Hintergrundverwaltung immer langsamer. Komplexe Joins überfordern die Datenbank.

  6. ……

Warum passiert das? Das Wesentliche ist immer noch, dass die Organisationsstruktur des Codes unvernünftig ist. Wir vermischen verschiedene Komplexitäten, wodurch eine größere Komplexität entsteht, und wiederholen diesen Vorgang dann, ohne es zu wissen, in einen riesigen Strudel der Komplexität zu geraten und nicht in der Lage zu sein, uns selbst daraus zu befreien.

1. Was ist CQRS?

CQRS ist die Abkürzung für Command Query Responsibility Segregation. Ein einfaches Verständnis besteht darin, die Vorgänge „Schreiben“ (Befehl) und „Lesen“ (Abfrage) zu trennen. Schüler, die schnell reagieren, werden sagen: „Ist das nicht eine fortschrittliche Technologie? Ist es nicht nur die Trennung von Lesen und Schreiben in der Datenbank?“

Ja, die Trennung von Lesen und Schreiben in der Datenbank kann als eine Art CQRS betrachtet werden, aber die Bedeutung von CQRS ist viel komplizierter.

CRQS ist sowohl eine beliebte Geschäftsarchitektur als auch eine Form des Design Thinking.

Der Kern von CQRS ist die „Aufteilung“, die das komplexe System in zwei Teile aufteilt: Befehl und Abfrage, unterschiedliche Modi für unterschiedliche Szenarien verwendet, die am besten geeignete Technologie zur Implementierung der besten Lösung auswählt und verhindert, dass sich die beiden Teile gegenseitig stören .

Der Zweck von CQRS besteht darin, die Komplexität des gesamten Systems zu reduzieren. Welche Logik steckt also dahinter?

Angenommen, in einem System:

  1. Befehl hat Komplexität M

  2. Die Abfrage hat die Komplexität N

Wenn derselbe Satz von Modellen zur Verarbeitung von Befehlen und Abfragen verwendet wird, beträgt die Systemkomplexität im Extremfall M * N, da sich die beiden gegenseitig beeinflussen und Sie bei der Anpassung eines Modells immer auf die Auswirkungen auf das andere achten müssen .

Diese Art von Entwurfsmethode „Du hast mich und ich habe dich“, die „Interaktion zwischen den beiden“ ist zum kompliziertesten Teil des Systems geworden, und es wird viel Energie darauf verwendet, „die Auswirkungen zu überprüfen“, anstatt die wertvollstes Design und Codierung.

Wenn Befehl und Abfrage vollständig getrennt sind, beträgt die Komplexität des Systems M + N. Änderungen an Command wirken sich nicht auf Query aus, und Änderungen an Query wirken sich nicht auf Command aus.

Natürlich sind die beiden oben genannten Extreme in der Praxis selten und die Komplexität des Systems liegt normalerweise zwischen beiden.

Dabei handelt es sich lediglich um eine theoretische Ableitung. Auch die „Konflikte“, die überall in der tatsächlichen Arbeit zu beobachten sind, sind ein Hinweis auf „Spaltung“.

2. Konflikte in Schichtarchitekturen 

Die gebräuchlichste Schichtarchitektur wird wie folgt vorgestellt:

Wie in der Abbildung gezeigt, ist das System in 5 Schichten unterteilt, und die Bedeutung jeder Schicht ist wie folgt:

  1. Webzugriffsebene. Es wird hauptsächlich verwendet, um Systemeingaben zu verarbeiten, die Eingabeinformationen zu überprüfen, Anwendungsdienste aufzurufen, um Geschäftsvorgänge abzuschließen, die Ergebnisse zu konvertieren und sie schließlich an den Anrufer zurückzugeben.

  2. Anwendungsdienstschicht. Er kümmert sich hauptsächlich um die Orchestrierung von Geschäftsprozessen, ruft Domänenobjekte aus dem Lager ab, führt Geschäftsvorgänge des Domänenmodells aus, synchronisiert den neuesten Objektstatus über das Lager mit der Datenspeicher-Engine und veröffentlicht Domänenereignisse extern.

  3. Domänenschicht. Der Tragepunkt der Geschäftslogik ist ein konzentrierter Ausdruck des Geschäftswerts. Er basiert normalerweise auf objektorientiertem Design und gewährleistet die Wiederverwendbarkeit und Skalierbarkeit der Geschäftslogik basierend auf Funktionen wie Kapselung, Vererbung und Polymorphismus.

  4. Lagerebene. Wird hauptsächlich für den Datenzugriff, die Bereitstellung von Datenbetriebsdiensten für Anwendungsdienste nach oben und die Abschirmung der Unterschiede verschiedener Speicher-Engines nach unten verwendet.

  5. Datenschicht. Wird hauptsächlich zum Speichern und Abrufen von Daten verwendet. Alle gängigen Datenspeicher-Engines gehören zu dieser Schicht, z. B. MySQL, Redis, ES usw.;

Tatsächlich ist die Schichtarchitektur selbst auch eine Art „Spaltung“, die unterschiedliche Anliegen auf verschiedenen Ebenen zusammenfasst. Aber zusätzlich zur horizontalen Schichtung kann es basierend auf CQRS auch vertikal aufgeteilt werden, das heißt, die Komponenten jeder Schicht werden in zwei Teile aufgeteilt: Befehl und Abfrage.

Da der Zugriffsschichtkonflikt gering ist, ist die Aufteilung selbst von geringer Bedeutung und hier nicht erforderlich. Im strengen Sinne wird die Aufteilung jedoch dennoch empfohlen.

 

3. Konflikt und Aufteilung der Anwendungsdienstschicht

Bei der Aufteilung der Anwendungsdienstschicht wird ein Anwendungsdienst in zwei Gruppen aufgeteilt: CommandService und QueryService.

Dadurch können viele unnötige Probleme vermieden werden. Es gibt folgende große Unterschiede zwischen Befehl und Abfrage:

CommandService Abfrageservice
Abhängige Komponenten sind unterschiedlich ValidateService-Verifizierungsdienst; LazyLoaderFactory-Lazy-Loading-Dienst; CommandRepository-Warehouse ohne Caching; EventPublisher-Ereignisherausgeber QueryRepository-Warehouse mit Caching-Funktion; JoinService-Datenaggregationsdienst; Converter-Datenkonvertierungsdienst
Die Kernprozesse sind unterschiedlich Validierung, Laden, Geschäftsbetrieb, Synchronisierung, Veröffentlichung von Ereignissen Validierung, Laden, Datenassemblierung, Konvertierung
Verschiedene Funktionen werden erweitert Hauptsächlich Transaktionsmanager Hauptsächlich Caching-Komponenten

Wenn Sie an das eingangs erwähnte Szenario zurückdenken, müssen Sie sich nach Abschluss der Aufteilung der Anwendungsebene keine Sorgen mehr über die Verwendung falscher Komponenten machen:

  1. Das Repository von CommandService verwendet keinen Cache und betreibt nur die Datenbank

  2. Das Repository von QueryService kann Caching verwenden, um die Zugriffsleistung zu verbessern

Darüber hinaus kann für einen einheitlichen Betriebsprozess eine weitere Abstraktion verwendet werden, um wiederholte „Vorlagencodes“ zu eliminieren, wie zum Beispiel:

  1. Einführung des „Vorlagenmethoden-Entwurfsmusters“, um die Wiederverwendung der Kernlogik zu erreichen

    1. Abstrahieren Sie die beiden übergeordneten Klassen BaseCommandService und BaseQueryService, um den Kernprozess zu vereinheitlichen

    2. Unterklassen implementieren abstrakte Methoden von BaseCommandService und BaseQueryService, um die Funktionserweiterung abzuschließen

  2. Verwendung des Proxy-Modells basierend auf „Konvention vor Konfiguration“, wobei nur Schnittstellen definiert werden, ohne Implementierungscode zu schreiben

    1. Definieren Sie die CommandService- und QueryService-Schnittstellen gemäß den Spezifikationen und vervollständigen Sie relevante Konfigurationen durch Anmerkungen

    2. Generieren Sie automatisch Proxy-Implementierungsklassen, um die Prozessorchestrierung abzuschließen

 

4. Konflikt und Aufteilung der Modellebene

Die Modellschicht ist der Kern des Systems und ihr Design wirkt sich direkt auf die Qualität des gesamten Systems aus. Als Kern der Geschäftslogik umfasst die Umsetzungsstrategie des Vergleichsprozesses:

  1. Der Kern des domänengesteuerten DDD-Designs besteht darin, erweiterte objektorientierte Funktionen (Kapselung, Vererbung, Polymorphismus, Zusammensetzung usw.) für das Design zu verwenden, was sich sehr gut für komplexe Geschäftsszenarien eignet. Dies spiegelt sich in der Existenz vieler Objektgruppen mit hoher Kohäsion und geringer Kopplung (Aggregatwurzeln) wider, und die Geschäftslogik wird durch die Zusammenarbeit dieser kleinen Objekte vervollständigt.

  2. Transaktionsskripte nutzen prozedurales Denken, um Datenoperationen in Prozesse einzubinden, und eignen sich besser für unkomplizierte Geschäftsszenarien. Seine Manifestation besteht darin, dass es viele „Gottesdienste“ gibt und dass der Dienst viele sehr lange Methoden enthält und die Geschäftslogik durch diese Methoden vervollständigt wird.

Im Internet wird seit vielen Jahren darüber diskutiert, welche Lösung die beste ist, und am Ende gibt es kein Fazit. Aber ich glaube immer, dass „das Diskutieren von Plänen ohne Geschäftsszenarien nur ein Schurke ist.“

Ausgehend von verschiedenen Anwendungsszenarien lassen sich folgende Schlussfolgerungen ziehen:

  1. Befehlsszenarien müssen eine strenge Geschäftslogik gewährleisten und sind in der Regel komplex, daher ist DDD die optimale Lösung.

  2. Abfrageszenarien erfordern als Unterstützung flexiblere Datenassemblierungsfunktionen und sind in der Regel relativ einfach, sodass Transaktionsskripte die optimale Lösung sind.

Ich sage oft: „Das einfachste „Schreiben“ ist auch komplex, und das komplexeste „Lesen“ ist auch einfach.“ Die Logik dahinter basiert auf der Szenenbeurteilung von Command and Query.

Teilen Sie das Modell wie folgt in Befehl und Abfrage auf:

 

Nach Abschluss des Modellsplits weist das neue Modell folgende Merkmale auf:

  1. Agg ist die Aggregatwurzel in DDD. Sie wird hauptsächlich zur Verarbeitung komplexer Befehlslogik verwendet und besteht aus „Rich Objects“ mit einer großen Anzahl von Geschäftsvorgängen.

  2. View ist ein Standard-POJO, das hauptsächlich als Abfrageergebnisobjekt fungiert. Es ist ein typisches „anämisches Objekt“ und dient nur als Datenträger und stellt Daten entsprechend den Anzeigeanforderungen zusammen.

  3. View verfügt nicht über ein eigenes Repository und kann sich zum Abrufen von Daten nur auf CommandRepository verlassen. Die Converter-Komponente ist für die Konvertierung des Agg-Modells in das View-Modell verantwortlich.

Dies ist der entscheidende Punkt der Aufteilung. Um das Verständnis zu erleichtern, geben wir ein einfaches Beispiel:

Zum Beispiel im Bestellmodul des E-Commerce:

  • Im Bestellprozess dient Order als Aggregatstamm zur Koordinierung des internen OrderItem und PayInfo.

  • Auf der Bestelllistenseite müssen nur Bestell- und Benutzerinformationen angezeigt werden.

  • Bestelldetails: Bestellung, Benutzer, Adresse, OrderItem, PayInfo, Produkt und andere Informationen müssen angezeigt werden

Wenn ein Modell drei Szenarien gleichzeitig unterstützt, wird das Modell selbst sehr komplex und es ist schwierig zu bestimmen, zu welchem ​​Szenario eine bestimmte Methode oder ein bestimmtes Feld gehört.

An dieser Stelle sollte das Modell je nach Szene aufgeteilt werden:

  1. OrderBO wird im DDD-Modus modelliert, bietet extern einheitliche Geschäftsabläufe und koordiniert intern mehrere Entitätsobjekte wie OrderItem und PayInfo.

  2. OrderListVO ist als POJO modelliert und seine Attribute enthalten Bestell- und Benutzerinformationen;

  3. OrderDetailVO wird im POJO-Modus modelliert und seine Attribute umfassen Bestellung, Benutzer, Adresse, OrderItem, PayInfo, Produkt und andere Informationen;

Die drei Modelle sind unabhängig voneinander und beeinflussen sich nicht gegenseitig.

Aufgrund der Verwendung eines einheitlichen Repositorys ist es natürlich auch erforderlich, einen Konverter bereitzustellen, der VO entspricht:

  1. OrderListVOConverter konvertiert OrderBO in ein OrderListVO-Objekt

  2. OrderDetailVOConverter konvertiert OrderBO in ein OrderDetailVO-Objekt

 

5. Konflikt und Aufteilung der Lagerschicht 

Es ist auch sehr wichtig, die Lagerebene aufzuteilen. Auf dieser Ebene gibt es mehrere Hauptkonflikte:

CommandRepository QueryRepository
Die zugrunde liegende Implementierung ist unterschiedlich Basiert hauptsächlich auf der DB-Implementierung Basierend auf mehreren Speicher-Engines wie DB, Redis, ES usw.
Die Komplexität der Methode variiert Bietet nur wenige Methoden und reicht aus, um die meisten Szenarien zu unterstützen, z. B. Speichern, Aktualisieren, getById usw. Angepasst an Geschäftsszenarien, mit verschiedenen Methoden (Einzel-, Stapel-, Paging-, Sortier-, Statistik- usw.) und verschiedenen Dimensionen (ID, Benutzer, Status)
Der Rückgabewert ist unterschiedlich Gibt das vollständig zusammengesetzte Rich-Objekt direkt zurück Passen Sie die Rückgabewerte entsprechend den Geschäftsszenarien an

Die Gesamtstruktur nach der Aufteilung des Lagers ist wie folgt:

Die Lageraufteilung weist folgende Merkmale auf:

  1. View erfordert nicht mehr die Converter-Komponente, um die Datenkonvertierung abzuschließen

  2. Die Ansichtsdaten stammen aus einem eigenen Repository, das flexibel an die Anzeigeanforderungen angepasst werden kann

  3. Command und Query verwenden immer noch denselben Satz Datenbanken und denselben Satz Datentabellen

 

6. Konflikt und Aufteilung der Datenschicht 

Die Aufteilung der Datenschicht stellt die wichtigste Aufteilung dar. Wenn es um die Trennung geht, ist die erste Reaktion die „Datenbank-Master-Slave-Trennung“.

Der Kern der Aufteilung der Datenschicht besteht darin, dass die besten Anwendungsszenarien verschiedener Datenspeicher-Engines sehr unterschiedlich sind und es häufig Widersprüche bei der Lese- und Schreiboptimierung gibt.

Nehmen Sie immer noch die am häufigsten verwendete Datenbank als Beispiel:

  1. Um die Abfrageleistung zu verbessern, wird empfohlen, Indizes für verschiedene Abfragedimensionen zu erstellen

  2. Um die Schreibleistung zu verbessern, müssen Sie immer weniger Indizes in der Tabelle haben

  3. Um die Aktualisierungsleistung zu beschleunigen, wird empfohlen, drei Paradigmen zum Entwerfen der Tabellenstruktur und zum Reduzieren redundanter Informationen zu verwenden.

  4. Um die Abfrageleistung zu beschleunigen, wird empfohlen, ein Anti-Paradigmen-Design zu verwenden, redundante Daten zu verwenden und Verknüpfungsvorgänge zwischen Datentabellen zu vermeiden.

Sie können nicht gleichzeitig Fisch und Bärentatze haben, und das wird auf der Datenbankebene anschaulich angezeigt!

Nach der Aufteilung der Datenschicht sieht die Struktur wie folgt aus:

Das Modell weist folgende Eigenschaften auf:

  1. Der Datenspeicher ist vollständig aufgeteilt; sowohl Command als auch Query können flexibel die am besten geeignete Speicher-Engine auswählen;

  2. Befehl und Abfrage müssen einen Synchronisierungsmechanismus einführen, um ihre Datensynchronisierung abzuschließen. Gängige Synchronisierungsmechanismen sind:

    1. Datensynchronisierung basierend auf Domänenereignissen auf der Anwendungsebene, wie in der Abbildung dargestellt

    2. Arbeiten Sie in der Datenschicht basierend auf der Synchronisierung von Protokolldaten, z. B. der Master-Slave-Synchronisierung von MySQL, Canal2XX usw.

Die Aufteilung der Datenschicht ist das endgültige Ziel großer Systeme. Nehmen wir dennoch das Bestellsystem als Beispiel:

  1. Da es sich um ein System mit extrem hohen Konsistenzanforderungen handelt, ist die erste Wahl für Bestellungen immer noch eine relationale Datenbank mit ACID auf der Befehlsseite. Auch wenn es sich um eine Unterdatenbank oder Untertabelle handelt, bleibt der zugrunde liegende Speicher unverändert;

  2. Um Hochleistungsabfrageanforderungen zu erfüllen, muss Redis auf der Abfrageseite als verteilter Cache eingeführt werden, um den Zugriff zu beschleunigen.

  3. Um den komplexen und mehrdimensionalen Geschäftsabfragen im Hintergrund gerecht zu werden, muss ES auf der Abfrageseite eingeführt werden, um den Volltextabruf zu beschleunigen.

  4. Um verschiedene Echtzeit-Berichtsanforderungen zu erfüllen, muss TiDB auf der Abfrageseite eingeführt werden, um den Echtzeitabruf großer Datenmengen zu ermöglichen.

Hier landen wir: „Datenintensive Systeme“ Eine wachsende Zahl von Anwendungen stellt strenge und umfassende Anforderungen, und ein einziges Tool reicht nicht aus, um alle Datenverarbeitungs- und Speicheranforderungen zu erfüllen. Stattdessen wird die Gesamtarbeit in eine Reihe von Aufgaben aufgeteilt, die von einem einzigen Tool effizient erledigt werden können, und diese werden durch Anwendungscode zusammengefügt, und Dienste werden extern über APIs bereitgestellt, wodurch interne Komplexität abgeschirmt wird.

 

7. Methoden zur Verbesserung der Leistung relationaler Datenbanken

Die Verbesserung der Leistung relationaler Datenbanken ist eine der großen Herausforderungen, denen sich viele große Anwendungen und Websites gegenübersehen. Hier sind zwei gängige Möglichkeiten, die Leistung relationaler Datenbanken zu verbessern:

Lese-Schreib-Trennung

  • Konzept : Die Lese- und Schreibtrennung ist eine Strategie, die Lesevorgänge und Schreibvorgänge in der Datenbank trennt. Typischerweise führen Anwendungen weitaus mehr Lesevorgänge als Schreibvorgänge aus, sodass Lesevorgänge einem oder mehreren Datenbankservern zugewiesen werden können, die nur für das Lesen von Daten zuständig sind, während Schreibvorgänge an den Hauptdatenbankserver gesendet werden.

  • Arbeitsablauf :

    • Hauptdatenbank (Schreibdatenbank): Verantwortlich für die Verarbeitung von Schreibvorgängen, einschließlich Einfügen, Aktualisieren und Löschen von Daten.
    • Slave-Datenbank (Lesedatenbank): Verantwortlich für die Verarbeitung von Lesevorgängen, einschließlich der Abfrage von Daten. Die Slave-Datenbank ist normalerweise eine Kopie der Master-Datenbank, um die Datenkonsistenz sicherzustellen.
  • Vorteile :

    • Verbessert die Leistung von Lesevorgängen, da diese parallel verarbeitet werden können.
    • Reduzieren Sie die Belastung der Primärdatenbank, damit diese sich mehr auf Schreibvorgänge konzentrieren kann.
    • Verbessern Sie die Skalierbarkeit der Datenbank, indem Sie weitere Slave-Datenbanken hinzufügen, um mehr Lesevorgänge zu unterstützen.
  • Dinge zu beachten :

    • Das Kopieren von Daten kann zu einer gewissen Latenz führen, sodass Lesevorgänge möglicherweise nicht sofort die neuesten Schreibvorgänge widerspiegeln.
    • Datenkonsistenz- und Synchronisierungsstrategien müssen berücksichtigt werden.

Sharding

  • Konzept : Datenbank-Sharding und Tabellen-Sharding ist eine Strategie zur Aufteilung einer Datenbank in mehrere unabhängige Unterdatenbanken oder Datentabellen. Jede Unterdatenbank oder Tabelle ist für die Speicherung von Daten in einem bestimmten Bereich oder einer bestimmten Bedingung verantwortlich. Auf diese Weise können Abfragen und Schreibvorgänge auf verschiedene Datenbankknoten verteilt werden, wodurch die Belastung einer einzelnen Datenbank verringert wird.

  • Arbeitsablauf :

    • Horizontales Sharding von Datenbanktabellen: Verteilen Sie Daten basierend auf einem bestimmten Datenfeld (z. B. Benutzer-ID oder Zeitstempel) auf verschiedene Tabellen.
    • Datenbank-Datenbank-Horizontales Sharding: Verteilen Sie Daten basierend auf einem bestimmten Attribut der Daten (z. B. geografischer Standort oder Geschäftstyp) in verschiedene Datenbanken.
  • Vorteile :

    • Verbessert die Skalierbarkeit der Datenbank, da Daten auf mehrere Knoten verteilt werden können.
    • Reduzieren Sie die Belastung einer einzelnen Datenbank und verbessern Sie die Leistung.
    • Erleichtert die Wartung und Sicherung der Datenbank, da die Daten dezentral gespeichert werden.
  • Dinge zu beachten :

    • Um eine gleichmäßige Verteilung der Daten sicherzustellen, ist eine gut durchdachte Sharding-Strategie erforderlich.
    • Wenn Abfragen über Shards hinweg ausgeführt werden müssen, sind möglicherweise komplexe Abfragepläne erforderlich.

Diese Methoden können einzeln oder in Kombination verwendet werden und werden je nach Bedarf und Leistungsanforderungen der Anwendung ausgewählt. Beispielsweise kann eine große E-Commerce-Website die Lese-/Schreibtrennung verwenden, um die Leistung der Produktsuche zu verbessern, und Sharding und Sharding verwenden, um die Speicherung und den Abruf von Bestelldaten zu verwalten. Zusammen betrachtet können diese Strategien dazu beitragen, unterschiedliche Ebenen von Datenbankleistungsherausforderungen zu bewältigen.

 

8. Zusammenfassung

„Split“ ist eines der wichtigen Mittel zur „Trennung von Anliegen“. Der Zweck der Aufteilung besteht darin, Probleme zu klassifizieren und dann gezielte Maßnahmen zu ergreifen, um sie besser zu lösen.

Als Architektur klassifiziert CQRS verschiedene Teile des Geschäftssystems. Als nächstes müssen wir die optimale Lösung für Command and Query finden:

  1. Command nutzt DDD als theoretische Grundlage, um den besten praktischen Kampf im taktischen Modell umzusetzen, einschließlich

    1. Aggregatdesign

    2. Lagerdesign

    3. LazyLoad + Kontextmodus

    4. Unternehmensüberprüfung

    5. Domänenereignisse

  2. Query stellt den Datenabruf und die Assemblierung als seine Kernfunktionen dar. Das Design bleibt den Entwicklern überlassen und die Implementierung wird dem Framework überlassen, einschließlich

    1. QueryObject-Abfrageobjektmodus

    2. Memory Join-Modus

    3. Breiter Tabellen- und redundanter Tabellenmodus

 

 

Supongo que te gusta

Origin blog.csdn.net/summer_fish/article/details/132777065
Recomendado
Clasificación