Gut gestaltete Abfragebefehle und / oder Spezifikationen

90

Ich habe schon seit einiger Zeit nach einer guten Lösung für die Probleme gesucht, die durch das typische Repository-Muster entstehen (wachsende Liste von Methoden für spezielle Abfragen usw. Siehe: http://ayende.com/blog/3955/repository-) is-the-new-singleton ).

Ich mag die Idee, Befehlsabfragen zu verwenden, besonders durch die Verwendung des Spezifikationsmusters. Mein Problem mit der Spezifikation besteht jedoch darin, dass sie sich nur auf die Kriterien einfacher Auswahl bezieht (im Grunde genommen auf die where-Klausel) und sich nicht mit anderen Fragen von Abfragen befasst, wie z. B. Zusammenfügen, Gruppieren, Auswahl von Teilmengen oder Projektion usw. Grundsätzlich müssen alle zusätzlichen Rahmen, die viele Abfragen durchlaufen müssen, um den richtigen Datensatz zu erhalten.

(Hinweis: Ich verwende den Begriff "Befehl" wie im Befehlsmuster, auch als Abfrageobjekte bezeichnet. Ich spreche nicht von Befehl wie bei der Befehls- / Abfragetrennung, bei der zwischen Abfragen und Befehlen unterschieden wird (Aktualisieren, Löschen, einfügen))

Daher suche ich nach Alternativen, die die gesamte Abfrage kapseln, aber dennoch flexibel genug sind, um nicht nur Spaghetti-Repositorys gegen eine Explosion von Befehlsklassen auszutauschen.

Ich habe zum Beispiel Linqspecs verwendet, und obwohl ich einen gewissen Wert darin finde, Auswahlkriterien aussagekräftige Namen zuzuweisen, reicht dies einfach nicht aus. Vielleicht suche ich eine gemischte Lösung, die mehrere Ansätze kombiniert.

Ich suche nach Lösungen, die andere möglicherweise entwickelt haben, um entweder dieses Problem oder ein anderes Problem anzugehen, aber diese Anforderungen dennoch erfüllen. In dem verlinkten Artikel schlägt Ayende vor, den nHibernate-Kontext direkt zu verwenden, aber ich bin der Meinung, dass dies Ihre Geschäftsschicht erheblich kompliziert, da sie jetzt auch Abfrageinformationen enthalten muss.

Ich werde ein Kopfgeld dafür anbieten, sobald die Wartezeit abgelaufen ist. Bitte machen Sie Ihre Lösungen mit guten Erklärungen bounty-würdig, und ich werde die beste Lösung auswählen und die Zweitplatzierten bewerten.

HINWEIS: Ich suche etwas, das ORM-basiert ist. Muss nicht explizit EF oder nHibernate sein, aber diese sind am häufigsten und passen am besten. Wenn es leicht an andere ORMs angepasst werden kann, wäre das ein Bonus. Linq kompatibel wäre auch schön.

UPDATE: Ich bin wirklich überrascht, dass es hier nicht viele gute Vorschläge gibt. Es scheint, als wären die Leute entweder komplett CQRS oder sie befinden sich komplett im Repository-Lager. Die meisten meiner Apps sind nicht komplex genug, um CQRS zu rechtfertigen (etwas, für das die meisten CQRS-Befürworter bereitwillig sagen, dass Sie es nicht verwenden sollten).

UPDATE: Hier scheint es ein wenig Verwirrung zu geben. Ich suche keine neue Datenzugriffstechnologie, sondern eine einigermaßen gut gestaltete Schnittstelle zwischen Unternehmen und Daten.

Im Idealfall suche ich eine Art Kreuzung zwischen Abfrageobjekten, Spezifikationsmuster und Repository. Wie oben erwähnt, behandelt das Spezifikationsmuster nur den Aspekt der where-Klausel und nicht die anderen Aspekte der Abfrage, wie z. B. Verknüpfungen, Unterauswahlen usw. Repositorys behandeln die gesamte Abfrage, geraten jedoch nach einer Weile außer Kontrolle . Abfrageobjekte behandeln auch die gesamte Abfrage, aber ich möchte Repositorys nicht einfach durch Explosionen von Abfrageobjekten ersetzen.

Erik Funkenbusch
quelle
5
Fantastische Frage. Ich würde auch gerne sehen, welche Leute mehr Erfahrung haben als ich vorschlage. Ich arbeite gerade an einer Codebasis, in der das generische Repository auch Überladungen für Befehlsobjekte oder Abfrageobjekte enthält, deren Struktur der von Ayende in seinem Blog beschriebenen ähnelt. PS: Dies könnte auch bei Programmierern Aufmerksamkeit erregen.
Simon Whitehead
Warum nicht einfach ein Repository verwenden, das IQueryable verfügbar macht, wenn Ihnen die Abhängigkeit von LINQ nichts ausmacht? Ein gängiger Ansatz ist ein generisches Repository. Wenn Sie darüber hinaus wiederverwendbare Logik benötigen, erstellen Sie mit Ihren zusätzlichen Methoden einen abgeleiteten Repository-Typ.
Devdigital
@devdigital - Die Abhängigkeit von Linq ist nicht gleichbedeutend mit der Abhängigkeit von der Datenimplementierung. Ich möchte Linq für Objekte verwenden, damit ich andere Business-Layer-Funktionen sortieren oder ausführen kann. Das heißt aber nicht, dass ich Abhängigkeiten von der Implementierung des Datenmodells haben möchte. Worüber ich hier wirklich spreche, ist die Layer / Tier-Schnittstelle. Als Beispiel möchte ich in der Lage sein, eine Abfrage zu ändern und nicht an 200 Stellen ändern zu müssen. Dies passiert, wenn Sie IQueryable direkt in das Geschäftsmodell einbinden.
Erik Funkenbusch
1
@devdigital - das verschiebt im Grunde nur die Probleme mit einem Repository in Ihre Geschäftsschicht. Sie mischen nur das Problem herum.
Erik Funkenbusch
1
@MystereMan Schauen Sie sich diese 2 Artikel an: blog.gauffin.org/2012/10/griffin-decoupled-the-queries and cutedge.it/blogs/steven/pivot/entry.php?id=92
david.s

Antworten:

94

Haftungsausschluss: Da es noch keine großartigen Antworten gibt, habe ich beschlossen, einen Teil eines großartigen Blogposts zu veröffentlichen, den ich vor einiger Zeit gelesen und fast wörtlich kopiert habe. Den vollständigen Blogbeitrag finden Sie hier . Hier ist es also:


Wir können die folgenden zwei Schnittstellen definieren:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

Das IQuery<TResult>gibt eine Nachricht an, die eine bestimmte Abfrage mit den zurückgegebenen Daten unter Verwendung des TResultgenerischen Typs definiert. Mit der zuvor definierten Schnittstelle können wir eine Abfragenachricht wie folgt definieren:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

Diese Klasse definiert eine Abfrageoperation mit zwei Parametern, die zu einem Array von UserObjekten führt. Die Klasse, die diese Nachricht verarbeitet, kann wie folgt definiert werden:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

Wir können jetzt die Verbraucher von der generischen IQueryHandlerSchnittstelle abhängig machen :

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

Dieses Modell bietet uns sofort viel Flexibilität, da wir jetzt entscheiden können, was in das Modell injiziert werden soll UserController. Wir können eine völlig andere Implementierung einfügen oder eine, die die eigentliche Implementierung umschließt, ohne Änderungen an der UserController(und allen anderen Verbrauchern dieser Schnittstelle) vornehmen zu müssen .

Die IQuery<TResult>Schnittstelle bietet uns Unterstützung bei der Kompilierung beim Angeben oder Einfügen IQueryHandlersunseres Codes. Wenn wir das ändern FindUsersBySearchTextQueryzurückzukehren UserInfo[]statt (durch die Implementierung IQuery<UserInfo[]>), das UserControllerwird nicht kompilieren, da die generische Typ Einschränkung für IQueryHandler<TQuery, TResult>nicht in der Lage sein , kartieren FindUsersBySearchTextQueryzu User[].

Das Injizieren der IQueryHandlerSchnittstelle in einen Verbraucher weist jedoch einige weniger offensichtliche Probleme auf, die noch angegangen werden müssen. Die Anzahl der Abhängigkeiten unserer Verbraucher kann zu groß werden und zu einer Überinjektion des Konstruktors führen - wenn ein Konstruktor zu viele Argumente verwendet. Die Anzahl der Abfragen, die eine Klasse ausführt, kann sich häufig ändern, was ständige Änderungen der Anzahl der Konstruktorargumente erforderlich machen würde.

Wir können das Problem beheben, dass zu viele IQueryHandlersmit einer zusätzlichen Abstraktionsebene injiziert werden müssen . Wir erstellen einen Mediator, der zwischen den Verbrauchern und den Abfragehandlern sitzt:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

Dies IQueryProcessorist eine nicht generische Schnittstelle mit einer generischen Methode. Wie Sie in der Schnittstellendefinition sehen können, IQueryProcessorhängt dies von der IQuery<TResult>Schnittstelle ab. Dies ermöglicht uns eine Unterstützung bei der Kompilierungszeit bei unseren Verbrauchern, die von der abhängig ist IQueryProcessor. Lassen Sie uns das umschreiben UserController, um das Neue zu verwenden IQueryProcessor:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

Das UserControllerhängt jetzt von einem ab IQueryProcessor, der alle unsere Anfragen bearbeiten kann. Die Methode UserController's SearchUsersruft die IQueryProcessor.ProcessMethode auf, die ein initialisiertes Abfrageobjekt übergibt. Da das FindUsersBySearchTextQuerydie IQuery<User[]>Schnittstelle implementiert , können wir es an die generische Execute<TResult>(IQuery<TResult> query)Methode übergeben. Dank der C # -Typinferenz kann der Compiler den generischen Typ bestimmen, sodass wir den Typ nicht explizit angeben müssen. Der Rückgabetyp der ProcessMethode ist ebenfalls bekannt.

Es liegt nun in der Verantwortung der Umsetzung des IQueryProcessor, das Richtige zu finden IQueryHandler. Dies erfordert eine dynamische Typisierung und optional die Verwendung eines Dependency Injection-Frameworks und kann mit nur wenigen Codezeilen durchgeführt werden:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

Die QueryProcessorKlasse erstellt einen bestimmten IQueryHandler<TQuery, TResult>Typ basierend auf dem Typ der angegebenen Abfrageinstanz. Dieser Typ wird verwendet, um die angegebene Containerklasse aufzufordern, eine Instanz dieses Typs abzurufen. Leider müssen wir die HandleMethode mit Reflection aufrufen (in diesem Fall mit dem Schlüsselwort C # 4.0 dymamic), da es zu diesem Zeitpunkt unmöglich ist, die Handlerinstanz zu konvertieren, da das generische TQueryArgument zur Kompilierungszeit nicht verfügbar ist. Sofern die HandleMethode nicht umbenannt wird oder andere Argumente erhält, schlägt dieser Aufruf niemals fehl. Wenn Sie möchten, ist es sehr einfach, einen Komponententest für diese Klasse zu schreiben. Die Verwendung von Reflexion führt zu einem leichten Abfall, ist jedoch kein Grund zur Sorge.


Um eines Ihrer Anliegen zu beantworten:

Daher suche ich nach Alternativen, die die gesamte Abfrage kapseln, aber dennoch flexibel genug sind, um nicht nur Spaghetti-Repositorys gegen eine Explosion von Befehlsklassen auszutauschen.

Eine Konsequenz der Verwendung dieses Entwurfs ist, dass es viele kleine Klassen im System gibt, aber es ist eine gute Sache, viele kleine / fokussierte Klassen (mit klaren Namen) zu haben. Dieser Ansatz ist eindeutig viel besser als viele Überladungen mit unterschiedlichen Parametern für dieselbe Methode in einem Repository, da Sie diese in einer Abfrageklasse gruppieren können. Sie erhalten also immer noch viel weniger Abfrageklassen als Methoden in einem Repository.

david.s
quelle
2
Sieht aus wie Sie die Auszeichnung bekommen. Ich mag die Konzepte, ich hatte nur gehofft, dass jemand etwas wirklich anderes präsentiert. Glückwunsch.
Erik Funkenbusch
1
@FuriCuri, braucht eine einzelne Klasse wirklich 5 Abfragen? Vielleicht könnten Sie das als eine Klasse mit zu vielen Verantwortlichkeiten ansehen. Wenn die Abfragen aggregiert werden, sollten sie alternativ möglicherweise tatsächlich eine einzelne Abfrage sein. Dies sind natürlich nur Vorschläge.
Sam
1
@stakx Sie haben absolut Recht, dass in meinem ersten Beispiel der generische TResultParameter der IQuerySchnittstelle nicht nützlich ist. In meiner aktualisierten Antwort wird der TResultParameter jedoch von der ProcessMethode von verwendet IQueryProcessor, um die IQueryHandlerzur Laufzeit aufzulösen .
David.s
1
Ich habe auch ein Blog mit einer sehr ähnlichen Implementierung, wodurch ich auf dem richtigen Weg bin. Dies ist der Link jupaol.blogspot.mx/2012/11/… und ich verwende ihn seit einiger Zeit in PROD-Anwendungen. aber ich hatte ein Problem mit diesem Ansatz. Verketten und Wiederverwenden von Abfragen Nehmen wir an, ich habe mehrere kleine Abfragen, die kombiniert werden müssen , um komplexere Abfragen zu erstellen. Am Ende habe ich nur den Code dupliziert, aber ich suche nach einem besseren und saubereren Ansatz. Irgendwelche Ideen?
Jupaol
4
@Cemre Am Ende habe ich meine Abfragen in Erweiterungsmethoden gekapselt, die zurückgegeben wurden, IQueryableund sichergestellt, dass die Auflistung nicht aufgelistet wurde. Anschließend habe QueryHandlerich die Abfragen aufgerufen / verkettet. Dies gab mir die Flexibilität, meine Abfragen zu testen und zu verketten. Ich habe einen Anwendungsservice zusätzlich zu meinem QueryHandler, und mein Controller ist dafür verantwortlich, direkt mit dem Service anstelle des
Handlers
4

Mein Umgang damit ist eigentlich simpel und ORM-agnostisch. Meine Ansicht für ein Endlager ist dies: Das Repository Aufgabe ist die App mit dem Modell für den Kontext erforderlich sind, so dass die App fragt nur den Repo für was es will , es aber nicht sagen , wie es zu bekommen.

Ich versorge die Repository-Methode mit einem Kriterium (Ja, DDD-Stil), das vom Repo zum Erstellen der Abfrage verwendet wird (oder was auch immer erforderlich ist - es kann sich um eine Webservice-Anforderung handeln). Joins und Gruppen imho sind Details zum Wie, nicht das Was und ein Kriterium sollten nur die Basis sein, um eine where-Klausel zu erstellen.

Modell = das endgültige Objekt oder die Datenstruktur, die von der App benötigt werden.

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

Wahrscheinlich können Sie die ORM-Kriterien (Nhibernate) direkt verwenden, wenn Sie dies möchten. Die Repository-Implementierung sollte wissen, wie die Kriterien mit dem zugrunde liegenden Speicher oder DAO verwendet werden.

Ich kenne Ihre Domain und die Modellanforderungen nicht, aber es wäre seltsam, wenn der beste Weg darin besteht, dass die App die Abfrage selbst erstellt. Das Modell ändert sich so sehr, dass Sie nichts Stabiles definieren können?

Diese Lösung erfordert eindeutig zusätzlichen Code, koppelt jedoch nicht den Rest an einen ORM oder was auch immer Sie für den Zugriff auf den Speicher verwenden. Das Repository fungiert als Fassade und IMO ist es sauber und der Code für die Kriterienübersetzung ist wiederverwendbar

MikeSW
quelle
Damit werden die Probleme des Repository-Wachstums und der ständig wachsenden Liste von Methoden zur Rückgabe verschiedener Arten von Daten nicht behoben. Ich verstehe, dass Sie vielleicht kein Problem damit sehen (viele Leute sehen es nicht), aber andere sehen es anders (ich schlage vor, den Artikel zu lesen, auf den ich verlinkt habe, es gibt viele andere Leute mit ähnlichen Meinungen).
Erik Funkenbusch
1
Ich spreche es an, weil die Kriterien viele Methoden unnötig machen. Natürlich kann ich nicht von allen viel sagen, ohne etwas über das zu wissen, was Sie brauchen. Ich bin allerdings unter dem Eindruck, dass Sie die Datenbank direkt abfragen möchten, wahrscheinlich ist ein Repository nur im Weg. Wenn Sie direkt mit der relationalen Sotrage arbeiten müssen, gehen Sie direkt darauf ein, ohne dass ein Repository erforderlich ist. Und als Hinweis ist es ärgerlich, wie viele Leute Ayende mit diesem Beitrag zitieren. Ich bin damit nicht einverstanden und ich denke, dass viele Entwickler das Muster nur falsch verwenden.
MikeSW
1
Es kann das Problem etwas reduzieren, aber bei einer ausreichend großen Anwendung werden immer noch Monster-Repositories erstellt. Ich bin nicht mit Ayendes Lösung einverstanden, nHibernate direkt in der Hauptlogik zu verwenden, aber ich stimme ihm in Bezug auf die Absurdität des außer Kontrolle geratenen Repository-Wachstums zu. Ich möchte die Datenbank nicht direkt abfragen, sondern das Problem auch nicht nur aus einem Repository in eine Explosion von Abfrageobjekten verschieben.
Erik Funkenbusch
2

Ich habe dies getan, dies unterstützt und dies rückgängig gemacht.

Das Hauptproblem ist folgendes: Egal wie Sie es tun, die hinzugefügte Abstraktion verschafft Ihnen keine Unabhängigkeit. Es wird per Definition lecken. Im Wesentlichen erfinden Sie eine ganze Ebene, nur um Ihren Code niedlich aussehen zu lassen ... aber es reduziert nicht die Wartung, verbessert die Lesbarkeit oder bringt Ihnen irgendeine Art von Modell-Agnostizismus.

Der lustige Teil ist, dass Sie Ihre eigene Frage als Antwort auf Oliviers Antwort beantwortet haben: "Dies dupliziert im Wesentlichen die Funktionalität von Linq ohne alle Vorteile, die Sie von Linq erhalten."

Fragen Sie sich: Wie könnte es nicht sein?

Stu
quelle
Nun, ich habe definitiv die Probleme bei der Integration von Linq in Ihre Geschäftsschicht erlebt. Es ist sehr mächtig, aber wenn wir Datenmodelländerungen vornehmen, ist es ein Albtraum. Mit Repositorys werden die Dinge verbessert, da ich die Änderungen an einem lokalisierten Ort vornehmen kann, ohne die Geschäftsschicht stark zu beeinflussen (außer wenn Sie auch die Geschäftsschicht ändern müssen, um die Änderungen zu unterstützen). Repositorys werden jedoch zu aufgeblähten Schichten, die die SRP massiv verletzen. Ich verstehe Ihren Standpunkt, aber er löst auch keine Probleme.
Erik Funkenbusch
Wenn Ihre Datenschicht LINQ verwendet und Änderungen am Datenmodell Änderungen an Ihrer Geschäftsschicht erfordern, sind Sie nicht richtig geschichtet.
Stu
Ich dachte, Sie sagten, Sie hätten diese Ebene nicht mehr hinzugefügt. Wenn Sie sagen, dass die hinzugefügte Abstraktion Ihnen nichts bringt, bedeutet dies, dass Sie mit Ayende einverstanden sind, die nHibernate-Sitzung (oder den EF-Kontext) direkt an Ihre Geschäftsschicht zu übergeben.
Erik Funkenbusch
1

Sie können eine fließende Schnittstelle verwenden. Die Grundidee ist, dass Methoden einer Klasse die aktuelle Instanz genau dieser Klasse zurückgeben, nachdem sie eine Aktion ausgeführt haben. Auf diese Weise können Sie Methodenaufrufe verketten.

Durch Erstellen einer geeigneten Klassenhierarchie können Sie einen logischen Fluss zugänglicher Methoden erstellen.

public class FinalQuery
{
    protected string _table;
    protected string[] _selectFields;
    protected string _where;
    protected string[] _groupBy;
    protected string _having;
    protected string[] _orderByDescending;
    protected string[] _orderBy;

    protected FinalQuery()
    {
    }

    public override string ToString()
    {
        var sb = new StringBuilder("SELECT ");
        AppendFields(sb, _selectFields);
        sb.AppendLine();

        sb.Append("FROM ");
        sb.Append("[").Append(_table).AppendLine("]");

        if (_where != null) {
            sb.Append("WHERE").AppendLine(_where);
        }

        if (_groupBy != null) {
            sb.Append("GROUP BY ");
            AppendFields(sb, _groupBy);
            sb.AppendLine();
        }

        if (_having != null) {
            sb.Append("HAVING").AppendLine(_having);
        }

        if (_orderBy != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderBy);
            sb.AppendLine();
        } else if (_orderByDescending != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderByDescending);
            sb.Append(" DESC").AppendLine();
        }

        return sb.ToString();
    }

    private static void AppendFields(StringBuilder sb, string[] fields)
    {
        foreach (string field in fields) {
            sb.Append(field).Append(", ");
        }
        sb.Length -= 2;
    }
}

public class GroupedQuery : FinalQuery
{
    protected GroupedQuery()
    {
    }

    public GroupedQuery Having(string condition)
    {
        if (_groupBy == null) {
            throw new InvalidOperationException("HAVING clause without GROUP BY clause");
        }
        if (_having == null) {
            _having = " (" + condition + ")";
        } else {
            _having += " AND (" + condition + ")";
        }
        return this;
    }

    public FinalQuery OrderBy(params string[] fields)
    {
        _orderBy = fields;
        return this;
    }

    public FinalQuery OrderByDescending(params string[] fields)
    {
        _orderByDescending = fields;
        return this;
    }
}

public class Query : GroupedQuery
{
    public Query(string table, params string[] selectFields)
    {
        _table = table;
        _selectFields = selectFields;
    }

    public Query Where(string condition)
    {
        if (_where == null) {
            _where = " (" + condition + ")";
        } else {
            _where += " AND (" + condition + ")";
        }
        return this;
    }

    public GroupedQuery GroupBy(params string[] fields)
    {
        _groupBy = fields;
        return this;
    }
}

Sie würden es so nennen

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

Sie können nur eine neue Instanz von erstellen Query. Die anderen Klassen haben einen geschützten Konstruktor. Der Punkt der Hierarchie besteht darin, Methoden zu "deaktivieren". Beispielsweise gibt die GroupByMethode a zurück, GroupedQuerydie die Basisklasse von ist Queryund keine WhereMethode hat (die where-Methode ist in deklariert Query). Daher ist es nicht möglich, rufen Wherenach GroupBy.

Es ist jedoch nicht perfekt. Mit dieser Klassenhierarchie können Sie Mitglieder nacheinander ausblenden, aber keine neuen anzeigen. HavingLöst daher eine Ausnahme aus, wenn sie zuvor aufgerufen wird GroupBy.

Beachten Sie, dass Sie Wheremehrmals anrufen können . Dies fügt ANDden vorhandenen Bedingungen neue Bedingungen hinzu . Dies erleichtert das programmgesteuerte Erstellen von Filtern aus einzelnen Bedingungen. Das gleiche ist möglich mit Having.

Die Methoden, die Feldlisten akzeptieren, haben einen Parameter params string[] fields. Sie können entweder einzelne Feldnamen oder ein String-Array übergeben.


Fließende Schnittstellen sind sehr flexibel und erfordern keine großen Überladungen von Methoden mit unterschiedlichen Parameterkombinationen. Mein Beispiel funktioniert mit Zeichenfolgen, der Ansatz kann jedoch auf andere Typen erweitert werden. Sie können auch vordefinierte Methoden für Sonderfälle oder Methoden deklarieren, die benutzerdefinierte Typen akzeptieren. Sie können auch Methoden wie ExecuteReaderoder hinzufügen ExceuteScalar<T>. Auf diese Weise können Sie solche Abfragen definieren

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

Selbst auf diese Weise erstellte SQL-Befehle können Befehlsparameter haben und so SQL-Injection-Probleme vermeiden und gleichzeitig das Zwischenspeichern von Befehlen durch den Datenbankserver ermöglichen. Dies ist kein Ersatz für einen O / R-Mapper, kann jedoch in Situationen hilfreich sein, in denen Sie die Befehle ansonsten mithilfe einer einfachen Zeichenfolgenverkettung erstellen würden.

Olivier Jacot-Descombes
quelle
3
Hmm .. Interessant, aber Ihre Lösung scheint Probleme mit den SQL Injection-Möglichkeiten zu haben und erstellt nicht wirklich vorbereitete Anweisungen für die vorkompilierte Ausführung (wodurch die Leistung langsamer wird). Es könnte wahrscheinlich angepasst werden, um diese Probleme zu beheben, aber dann bleiben wir bei den nicht typsicheren Datensatzergebnissen und was nicht. Ich würde eine ORM-basierte Lösung bevorzugen, und vielleicht sollte ich das explizit spezifizieren. Dies dupliziert im Wesentlichen die Funktionalität von Linq ohne alle Vorteile, die Sie von Linq erhalten.
Erik Funkenbusch
Ich bin mir dieser Probleme bewusst. Dies ist nur eine schnelle und schmutzige Lösung, die zeigt, wie eine fließende Schnittstelle aufgebaut werden kann. In einer realen Lösung würden Sie Ihren bestehenden Ansatz wahrscheinlich in eine flüssige Oberfläche „backen“, die an Ihre Bedürfnisse angepasst ist.
Olivier Jacot-Descombes