Strategien zur Vermeidung von SQL in Ihren Controllern… oder wie viele Methoden sollte ich in meinen Modellen haben?

17

Eine Situation, in die ich einigermaßen oft gerate, ist eine, in der meine Modelle entweder beginnen:

  • Wachsen Sie mit unzähligen Methoden zu Monstern heran

ODER

  • Ermöglichen es Ihnen, ihnen SQL-Teile zu übergeben, sodass sie flexibel genug sind, um nicht eine Million verschiedener Methoden zu erfordern

Angenommen, wir haben ein "Widget" -Modell. Wir beginnen mit einigen grundlegenden Methoden:

  • get ($ id)
  • Einfügen ($ record)
  • update ($ id, $ record)
  • löschen ($ id)
  • getList () // Liste der Widgets abrufen

Das ist alles in Ordnung und gut, aber dann brauchen wir eine Berichterstattung:

  • listCreatedBetween ($ start_date, $ end_date)
  • listPurchasedBetween ($ start_date, $ end_date)
  • listOfPending ()

Und dann wird die Berichterstattung immer komplexer:

  • listPendingCreatedBetween ($ start_date, $ end_date)
  • listForCustomer ($ customer_id)
  • listPendingCreatedBetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Sie können sehen, wo dies zunimmt ... irgendwann haben wir so viele spezifische Abfrageanforderungen, dass ich entweder Tonnen und Tonnen von Methoden implementieren muss, oder eine Art "Abfrage" -Objekt, das ich an eine einzelne -> Abfrage (Abfrage) übergeben kann $ query) Methode ...

... oder beißen Sie einfach in die Kugel und machen Sie so etwas:

  • list = MyModel-> query ("start_date> X AND end_date <Y AND pending = 1 AND customer_id = Z")

Es hat einen gewissen Reiz, nur eine Methode wie diese zu haben, anstatt 50 Millionen andere spezifischere Methoden ... aber es fühlt sich manchmal "falsch" an, einen Stapel dessen, was im Grunde SQL ist, in den Controller zu stopfen.

Gibt es einen "richtigen" Weg, mit solchen Situationen umzugehen? Scheint es akzeptabel, solche Abfragen in eine generische -> query () -Methode zu packen?

Gibt es bessere Strategien?

Keith Palmer Jr.
quelle
Ich bin gerade in einem Nicht-MVC-Projekt mit demselben Problem konfrontiert. Es stellt sich immer wieder die Frage, ob die Datenzugriffsschicht jede gespeicherte Prozedur abstrahieren und die Datenbank der Geschäftslogikschicht unabhängig lassen soll oder ob die Datenzugriffsschicht generisch sein soll, wenn die Geschäftsschicht etwas über die zugrunde liegende Datenbank weiß. Vielleicht besteht eine Zwischenlösung darin, etwas wie ExecuteSP (Zeichenfolge spName, Parameter object []) zu haben und dann alle SP-Namen in eine Konfigurationsdatei aufzunehmen, damit die Business-Schicht sie lesen kann. Ich habe jedoch keine wirklich gute Antwort darauf.
Greg Jackson

Antworten:

10

Martin Fowlers Patterns of Enterprise Application Architecture beschreibt eine Reihe von ORM-bezogenen Mustern, einschließlich der Verwendung des Query Object, was ich vorschlagen würde.

Mit Abfrageobjekten können Sie dem Prinzip der Einzelverantwortung folgen, indem Sie die Logik für jede Abfrage in individuell verwaltete und verwaltete Strategieobjekte aufteilen. Entweder kann Ihr Controller die Verwendung direkt verwalten oder an einen sekundären Controller oder ein Hilfsobjekt delegieren.

Wirst du viele davon haben? Bestimmt. Können einige zu generischen Abfragen zusammengefasst werden? Wieder ja.

Können Sie die Abhängigkeitsinjektion verwenden, um die Objekte aus Metadaten zu erstellen? Das ist, was die meisten ORM-Tools tun.

Matthew Flynn
quelle
4

Es gibt keinen richtigen Weg, dies zu tun. Viele Menschen verwenden ORMs, um die Komplexität zu abstrahieren. Einige der fortgeschritteneren ORMs übersetzen Code-Ausdrücke in komplizierte SQL-Anweisungen. ORMs haben auch ihre Nachteile, jedoch überwiegen bei vielen Anwendungen die Vorteile die Kosten.

Wenn Sie nicht mit einem umfangreichen Dataset arbeiten, ist es am einfachsten, die gesamte Tabelle in den Arbeitsspeicher auszuwählen und im Code zu filtern.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

Für interne Berichtsanwendungen ist dieser Ansatz wahrscheinlich in Ordnung. Wenn das Dataset wirklich groß ist, benötigen Sie viele benutzerdefinierte Methoden sowie entsprechende Indizes für Ihre Tabelle.

Dan
quelle
1
+ 1 für "Es gibt keine richtige Möglichkeit, dies zu tun"
ozz
1
Leider ist das Filtern außerhalb des Datensatzes auch bei den kleinsten Datensätzen, mit denen wir arbeiten, nicht wirklich eine Option - es ist einfach zu langsam. :-( Gut zu hören, dass andere auf dasselbe Problem stoßen. :-)
Keith Palmer Jr.
@KeithPalmer aus Neugier, wie groß sind deine Tische?
Dan
Hunderttausende Zeilen, wenn nicht mehr. Zu viele, um mit akzeptabler Leistung außerhalb der Datenbank zu filtern, INSBESONDERE mit einer verteilten Architektur, bei der sich die Datenbanken nicht auf demselben Computer wie die Anwendung befinden.
Keith Palmer Jr.
-1 für "Es gibt keine richtige Möglichkeit, dies zu tun". Es gibt mehrere richtige Wege. Die Anzahl der Methoden zu verdoppeln, wenn Sie ein Feature hinzufügen, während das OP ausgeführt wurde, ist ein nicht skalierbarer Ansatz, und die hier vorgeschlagene Alternative ist ebenfalls nicht skalierbar, nur im Hinblick auf die Datenbankgröße und nicht auf die Anzahl der Abfrage-Features. Es gibt skalierbare Ansätze, siehe die anderen Antworten.
Theodore Murdock
4

In einigen ORMs können Sie komplexe Abfragen ausgehend von grundlegenden Methoden erstellen. Zum Beispiel

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

ist eine vollkommen gültige Abfrage im Django ORM .

Die Idee ist, dass Sie einen Abfrage-Generator haben (in diesem Fall Purchase.objects), dessen interner Status Informationen zu einer Abfrage darstellt. Methoden wie get, filter, exclude, order_bygültig sind und geben eine neue Query Builder mit einem aktualisierten Status. Diese Objekte implementieren eine iterierbare Schnittstelle, sodass beim Durchlaufen der Objekte die Abfrage ausgeführt wird und Sie die Ergebnisse der bisher erstellten Abfrage erhalten. Obwohl dieses Beispiel aus Django stammt, sehen Sie dieselbe Struktur in vielen anderen ORMs.

Andrea
quelle
Ich sehe keinen Vorteil gegenüber so etwas wie old_purchases = Purchases.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Sie reduzieren nicht die Komplexität oder abstrahieren etwas, indem Sie SQL-ANDs und -ORs zu Methoden-ANDs und -ORs machen. Sie ändern nur die Darstellung der ANDs und ORs, oder?
Keith Palmer Jr.
4
Eigentlich nicht. Sie abstrahieren die SQL, was Ihnen viele Vorteile bringt. Erstens vermeiden Sie die Injektion. Anschließend können Sie die zugrunde liegende Datenbank ändern, ohne sich Gedanken über leicht unterschiedliche Versionen des SQL-Dialekts machen zu müssen, da der ORM dies für Sie erledigt. In vielen Fällen können Sie auch ein NoSQL-Backend erstellen, ohne es zu bemerken. Drittens sind diese Abfrage-Builder Objekte, die Sie wie alles andere weitergeben können. Dies bedeutet, dass Ihr Modell die Hälfte der Abfrage erstellen kann (zum Beispiel könnten Sie einige Methoden für die häufigsten Fälle haben) und diese dann im Controller verfeinert werden kann, um das
Andrea
2
... die spezifischsten Fälle. Ein typisches Beispiel ist die Definition einer Standardreihenfolge für Modelle in Django. Alle Abfrageergebnisse folgen dieser Reihenfolge, sofern Sie nichts anderes angeben. Viertens: Wenn Sie Ihre Daten aus Gründen der Leistung jemals denormalisieren müssen, müssen Sie nur den ORM optimieren, anstatt alle Ihre Abfragen neu zu schreiben.
Andrea
+1 Für dynamische Abfragesprachen wie die erwähnte und LINQ.
Evan Plaice
2

Es gibt einen dritten Ansatz.

Ihr spezielles Beispiel weist ein exponentielles Wachstum der Anzahl der erforderlichen Methoden auf, wenn die Anzahl der erforderlichen Features zunimmt: Wir möchten, dass erweiterte Abfragen angeboten und alle Abfragefunktionen kombiniert werden können. Wenn wir dazu Methoden hinzufügen, haben wir eine Methode für a Basisabfrage, zwei, wenn wir ein optionales Feature hinzufügen, vier, wenn wir zwei hinzufügen, acht, wenn wir drei hinzufügen, 2 ^ n, wenn wir n Features hinzufügen.

Das ist offensichtlich über drei oder vier Features hinaus nicht zu erreichen, und es riecht nach einer Menge eng verwandter Codes, die zwischen den Methoden fast kopiert werden.

Sie können dies vermeiden, indem Sie ein Datenobjekt hinzufügen, das die Parameter enthält, und eine einzige Methode verwenden, die die Abfrage auf der Grundlage der angegebenen (oder nicht angegebenen) Parameter erstellt. In diesem Fall ist das Hinzufügen einer neuen Funktion, z. B. eines Datumsbereichs, so einfach wie das Hinzufügen von Setters und Getters für den Datumsbereich zu Ihrem Datenobjekt und anschließendes Hinzufügen eines Codebits, in dem die parametrisierte Abfrage erstellt wird:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... und wo die Parameter zur Abfrage hinzugefügt werden:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Dieser Ansatz ermöglicht ein lineares Codewachstum, wenn Features hinzugefügt werden, ohne dass beliebige, nicht parametrisierte Abfragen zulässig sind.

Theodore Murdock
quelle
0

Meiner Meinung nach besteht der allgemeine Konsens darin, in Ihren Modellen in MVC so weit wie möglich auf Daten zuzugreifen. Ein weiteres Konstruktionsprinzip besteht darin, einige Ihrer allgemeineren Abfragen (die nicht direkt mit Ihrem Modell zusammenhängen) auf eine höhere, abstraktere Ebene zu verschieben, auf der Sie zulassen können, dass sie auch von anderen Modellen verwendet werden. (In RoR haben wir so etwas wie Framework) Es gibt noch eine andere Sache, die Sie berücksichtigen müssen und die die Wartbarkeit Ihres Codes ist. Wenn Ihr Projekt wächst und Sie über Datenzugriff in Controllern verfügen, wird es immer schwieriger, diese aufzuspüren. (Dieses Problem tritt derzeit in einem großen Projekt auf.) Modelle bieten, obwohl sie mit Methoden überladen sind, einen zentralen Ansprechpartner für alle Controller könnte am Ende von den Tabellen quering. (Dies kann auch zu einer Wiederverwendung von Code führen, was wiederum von Vorteil ist.)

Ricketyship
quelle
1
Beispiel für das, wovon du sprichst ...?
Keith Palmer Jr.
0

Ihre Service-Layer-Schnittstelle verfügt möglicherweise über viele Methoden, der Datenbankaufruf kann jedoch nur über eine Methode erfolgen.

Eine Datenbank hat 4 Hauptoperationen

  • Einfügen
  • Aktualisieren
  • Löschen
  • Abfrage

Eine andere optionale Methode kann darin bestehen, eine Datenbankoperation auszuführen, die nicht unter die grundlegenden DB-Operationen fällt. Nennen wir das Ausführen.

Einfügen und Aktualisieren können in einem Vorgang zusammengefasst werden, der als Speichern bezeichnet wird.

Viele Ihrer Methoden sind Abfragen. So können Sie eine generische Schnittstelle erstellen, die die meisten unmittelbaren Anforderungen erfüllt. Hier ist ein Beispiel für eine generische Schnittstelle:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

Das Datenübertragungsobjekt ist generisch und enthält alle Ihre Filter, Parameter, Sortierungen usw. Die Datenschicht ist für das Parsen und Extrahieren dieser Daten und das Einrichten der Operation für die Datenbank über gespeicherte Prozeduren, parametrisierte SQL, Linq usw. verantwortlich. Daher wird SQL nicht zwischen Schichten übergeben. Dies ist in der Regel das, was ein ORM tut. Sie können jedoch Ihre eigene Zeichnung erstellen und eine eigene Abbildung erstellen.

In Ihrem Fall haben Sie also Widgets. Widgets würden die IPOCO-Schnittstelle implementieren.

Also, in Ihrem Service-Layer-Modell hätte getList().

Benötigt eine Mapping-Ebene, in die transformiert getListwerden kann

Search<Widget>(DataTransferObject<Widget> Dto)

und umgekehrt. Wie andere bereits erwähnt haben, geschieht dies manchmal über ein ORM, aber letztendlich erhalten Sie eine Menge Code vom Typ Boilerplate, insbesondere wenn Sie Hunderte von Tabellen haben. Der ORM erstellt auf magische Weise parametrisiertes SQL und führt dieses für die Datenbank aus. Wenn Sie Ihre eigenen Dateien rollen, werden zusätzlich in der Datenebene selbst Mapper benötigt, um SP, Linq usw. einzurichten (im Grunde genommen der SQL-Code, der zur Datenbank geht).

Wie bereits erwähnt, ist das DTO ein aus Kompositionen bestehendes Objekt. Vielleicht ist eines der darin enthaltenen Objekte ein Objekt namens QueryParameters. Dies wären alle Parameter für die Abfrage, die von der Abfrage eingerichtet und verwendet würden. Ein weiteres Objekt wäre eine Liste der zurückgegebenen Objekte aus Abfragen, Aktualisierungen, ext. Das ist die Nutzlast. In diesem Fall wäre die Nutzlast eine Liste von Widgets.

Die grundlegende Strategie lautet also:

  • Service-Layer-Aufrufe
  • Transformieren Sie den Service-Layer-Aufruf in die Datenbank mit einer Art Repository / Mapping
  • Datenbankaufruf

In Ihrem Fall könnte das Modell viele Methoden haben, aber Sie möchten, dass der Datenbankaufruf generisch ist. Sie haben immer noch viel Boilerplate-Mapping-Code (insbesondere mit SPs) oder magischen ORM-Code, der das parametrisierte SQL dynamisch für Sie erstellt.

Jon Raynor
quelle