it-swarm.com.de

Wie werden generische Filteroperatoren in der Abfragezeichenfolge einer API entworfen?

Ich erstelle eine generische API mit Inhalten und einem Schema, das benutzerdefiniert werden kann. Ich möchte API-Antworten Filterlogik hinzufügen, damit Benutzer nach bestimmten Objekten suchen können, die sie in der API gespeichert haben. Wenn ein Benutzer beispielsweise Ereignisobjekte speichert, kann er beispielsweise Folgendes filtern:

  • Array enthält : Gibt an, ob properties.categoriesEngineering enthält.
  • Reifer als : Ist properties.created_at älter als 2016-10-02?
  • Ungleich : Ob properties.address.city nicht Washington ist
  • Equal : Gibt an, ob properties.nameMeetup ist.
  • usw.

Ich versuche, das Filtern in die Abfragezeichenfolge von API-Antworten zu entwerfen und habe ein paar Optionen, bin mir aber nicht sicher, welche Syntax dafür am besten ist ...


1. Operator als verschachtelter Schlüssel

/events?properties.name=Harry&properties.address.city.neq=Washington

In diesem Beispiel wird nur ein verschachteltes Objekt verwendet, um die Operatoren zu spezifizieren (wie in der Abbildung gezeigt, neq). Das ist insofern schön, als es sehr einfach und leicht zu lesen ist.

In Fällen, in denen die Eigenschaften eines Ereignisses vom Benutzer definiert werden können, tritt ein Problem auf, bei dem möglicherweise ein Konflikt zwischen einer Eigenschaft mit dem Namen address.city.neq unter Verwendung eines normalen Gleichheitsoperators und einer Eigenschaft mit dem Namen address.city unter Verwendung eines Nichtgleichheitsoperators auftritt.

Beispiel: Stripe's API


2. Operator als Schlüsselsuffix

/events?properties.name=Harry&properties.address.city+neq=Washington

Dieses Beispiel ähnelt dem ersten, mit der Ausnahme, dass für Vorgänge anstelle von + ein .-Trennzeichen (das einem Leerzeichen entspricht) verwendet wird, damit es nicht zu Verwechslungen kommt, da Schlüssel in meiner Domain keine Leerzeichen enthalten können.

Ein Nachteil ist, dass es etwas schwieriger zu lesen ist, obwohl dies fraglich ist, da es möglicherweise klarer ausgelegt wird. Ein weiterer Grund könnte sein, dass das Parsen etwas schwieriger ist, aber nicht so viel.


3. Operator als Wertpräfix

/events?properties.name=Harry&properties.address.city=neq:Washington

Dieses Beispiel ist dem vorherigen sehr ähnlich, außer dass die Operatorsyntax in den Wert des Parameters anstelle des Schlüssels verschoben wird. Dies hat den Vorteil, dass ein Teil der Komplexität beim Parsen der Abfragezeichenfolge entfällt.

Dies hat jedoch den Nachteil, dass nicht mehr zwischen einer Überprüfung des gleichen Operators für den Literal-String neq:Washington und einer Überprüfung des ungleichen Operators für den String Washington unterschieden werden kann.

Beispiel: Sparkpays API


4. Benutzerdefinierte Filterparameter

/events?filter=properties.name==Harry;properties.address.city!=Washington

In diesem Beispiel wird ein einzelner Abfrageparameter der obersten Ebene, filter, verwendet, um den Namespace für die gesamte Filterlogik zu erstellen. Dies ist insofern von Vorteil, als Sie sich nie Sorgen machen müssen, dass der Namespace der obersten Ebene kollidiert. (Obwohl in meinem Fall alle benutzerdefinierten Elemente unter properties. verschachtelt sind, ist dies zunächst kein Problem.)

Dies ist jedoch mit einem höheren Aufwand an Abfragezeichenfolgen verbunden, wenn Sie eine grundlegende Gleichheitsfilterung durchführen möchten, die wahrscheinlich dazu führt, dass die Dokumentation die meiste Zeit überprüft werden muss. Das Verlassen auf Symbole für die Bediener kann zu Verwirrung bei nicht offensichtlichen Vorgängen wie "nahe" oder "innerhalb" oder "enthält" führen.

Beispiel: Google Analytics API


5. Benutzerdefinierte ausführliche Filterparameter

/events?filter=properties.name eq Harry; properties.address.city neq Washington

In diesem Beispiel wird ein ähnlicher filter -Parameter der obersten Ebene verwendet wie im vorherigen Beispiel, jedoch werden die Operatoren in Word geschrieben, anstatt mit Symbolen definiert zu werden, und zwischen ihnen befinden sich Leerzeichen. Dies ist möglicherweise etwas besser lesbar.

Dies ist jedoch mit einer längeren URL und vielen zu verschlüsselnden Leerzeichen verbunden.

Beispiel: ODatas API


6. Objektfilterparameter

/events?filter[1][key]=properties.name&filter[1][eq]=Harry&filter[2][key]=properties.address.city&filter[2][neq]=Washington

In diesem Beispiel wird auch ein filter -Parameter der obersten Ebene verwendet. Statt jedoch eine vollständig benutzerdefinierte Syntax zu erstellen, die die Programmierung nachahmt, wird stattdessen eine Objektdefinition von Filtern unter Verwendung einer standardmäßigen Abfragezeichenfolgensyntax erstellt. Dies hat den Vorteil, etwas mehr "Standard" zu bringen.

Aber es geht um den Preis, sehr ausführlich zu tippen und schwer zu analysieren zu sein.

Beispiel Magentos API


Welche Syntax ist angesichts all dieser Beispiele oder eines anderen Ansatzes am besten? Im Idealfall wäre es einfach, den Abfrageparameter so zu konstruieren, dass das Herumspielen in der URL-Leiste möglich ist, aber auch keine Probleme für die zukünftige Interoperabilität mit sich bringt.

Ich neige zu # 2 , da es so aussieht, als wäre es lesbar, aber ich habe auch nicht die Nachteile anderer Schemata.

13

Ich beantworte möglicherweise nicht die Frage, welche die beste ist, aber ich kann Ihnen zumindest einige Einsichten und andere Beispiele geben, die Sie berücksichtigen sollten.

Zunächst sprechen Sie von einer "generischen API mit Inhalt und einem benutzerdefinierbaren Schema".

Das klingt sehr nach solr / elasticsearch , die beide High-Level-Wrapper für Apache Lucene sind, die im Grunde genommen Dokumente indizieren und aggregieren.

Die beiden verfolgten völlig unterschiedliche Ansätze für ihre Rest-API. Ich habe zufällig mit beiden gearbeitet.

Elasticsearch:

Sie haben die gesamte JSON-basierte DSL-Abfrage erstellt, die derzeit so aussieht:

GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }}, 
        { "match": { "content": "Elasticsearch" }}  
      ],
      "filter": [ 
        { "term":  { "status": "published" }}, 
        { "range": { "publish_date": { "gte": "2015-01-01" }}} 
      ]
    }
  }
}

Entnommen aus ihrem aktuellen doc . Ich war überrascht, dass Sie tatsächlich Daten inGETeinfügen können ... Es sieht jetzt tatsächlich besser aus, in früheren Versionen war es viel mehr hierarchisch .

Meiner persönlichen Erfahrung nach war dieses DSL leistungsstark, jedoch schwer zu erlernen und flüssig zu verwenden (insbesondere ältere Versionen). Und um tatsächlich ein Ergebnis zu erzielen, müssen Sie mehr als nur mit der URL spielen. Ausgehend von der Tatsache, dass viele Clients nicht einmal Daten inGETrequest unterstützen.

SOLR:

Sie setzen alles in Abfrageparameter, was im Grunde so aussieht (entnommen aus dem doc ):

q=*:*&fq={!cache=false cost=5}inStock:true&fq={!frange l=1 u=4 cache=false cost=50}sqrt(popularity)

Damit zu arbeiten war unkomplizierter. Aber das ist nur mein persönlicher Geschmack.


Nun zu meinen Erfahrungen. Wir implementierten eine weitere Schicht über diesen beiden und nahmen die Annäherungsnummer # 4. Eigentlich denke ich, dass # 4 und # 5 gleichzeitig unterstützt werden sollten. Warum? Denn was auch immer Sie auswählen, die Leute werden sich beschweren und da Sie sowieso Ihr eigenes "Micro-DSL" haben, könnten Sie genauso gut ein paar weitere Aliase für Ihre Keywords unterstützen.

Warum nicht # 2 ? Mit nur einem Filterparameter und einer Abfrage haben Sie die vollständige Kontrolle über DSL. Ein halbes Jahr, nachdem wir unsere Ressource erstellt hatten, erhielten wir eine "einfache" Featureanforderung - logisch ORund Klammer (). Abfrageparameter sind im Grunde genommen eine Liste von AND-Operationen, und logische OR-Operationen wie city=London OR age>25 passen dort nicht wirklich hinein. Andererseits wurde durch Klammern die Verschachtelung in eine DSL-Struktur eingeführt, was auch bei einer flachen Abfragezeichenfolgenstruktur ein Problem darstellen würde.

Nun, das waren die Probleme, auf die wir gestoßen sind. Ihr Fall könnte anders sein. Es lohnt sich jedoch noch zu überlegen, welche zukünftigen Erwartungen an diese API gestellt werden.

5
James Cube

# 4

Mir gefällt, wie dieGoogle AnalyticsFilter-API aus Kundensicht aussieht, einfach zu verwenden und einfach zu verstehen ist.

Sie verwenden eine URL-codierte Form, zum Beispiel:

  • Gleich :% 3D% 3D filters=ga:timeOnPage%3D%3D10
  • Nicht gleich :!% 3D filters=ga:timeOnPage!%3D10

Sie müssen zwar die Dokumentation überprüfen, aber es hat immer noch seine eigenen Vorteile. Wenn Sie denken, dass die Benutzer sich daran gewöhnen können, dann machen Sie es.


# 2

Die Verwendung von Operatoren als Schlüsselsuffixe scheint ebenfalls eine gute Idee zu sein (entsprechend Ihren Anforderungen).

Ich würde jedoch empfehlen, das Zeichen + zu codieren, damit es nicht als space analysiert wird. Es könnte auch etwas schwieriger sein, wie erwähnt zu analysieren, aber ich denke, Sie können einen benutzerdefinierten Parser für diesen Parser schreiben. Ich stolperte vor einiger Zeit über diese Gist von jlong ​​ . Vielleicht finden Sie es nützlich, Ihren Parser zu schreiben.

1

Sie könnten auch versuchen Spring Expression Language (SpEL)

Alles, was Sie tun müssen, ist, sich an das genannte Format im Dokument zu halten. Die SpEL-Engine würde sich darum kümmern, die Abfrage zu analysieren und sie für ein bestimmtes Objekt auszuführen. Ähnlich wie beim Filtern einer Liste von Objekten können Sie die Abfrage wie folgt schreiben:

properties.address.city == 'Washington' and properties.name == 'Harry'

Es unterstützt alle Arten von relationalen und logischen Operatoren, die Sie benötigen würden. Die restliche API könnte diese Abfrage einfach als Filterzeichenfolge verwenden und an die SpEL-Engine übergeben, um sie für ein Objekt auszuführen.

Vorteile: Es ist lesbar, einfach zu schreiben und die Ausführung ist gut gepflegt.

Die URL würde also so aussehen:

/events?filter="properties.address.city == 'Washington' and properties.name == 'Harry'"

Beispielcode mit org.springframework: spring-core: 4.3.4.RELEASE:

Die Hauptfunktion von Interesse:

    /**
     * Filter the list of objects based on the given query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

Komplettes Beispiel mit Hilfsklassen und anderem nicht interessanten Code:

import Java.util.Arrays;
import Java.util.List;
import Java.util.stream.Collectors;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class SpELTest {

    public static void main(String[] args) {
        String query = "address.city == 'Washington' and name == 'Harry'";

        Event event1 = new Event(new Address("Washington"), "Harry");
        Event event2 = new Event(new Address("XYZ"), "Harry");

        List<Event> events = Arrays.asList(event1, event2);

        List<Event> filteredEvents = filter(query, events);

        System.out.println(filteredEvents.size()); // 1
    }

    /**
     * Filter the list of objects based on the query
     * 
     * @param query
     * @param objects
     * @return
     */
    private static <T> List<T> filter(String query, List<T> objects) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(query);

        return objects.stream().filter(obj -> {
            return exp.getValue(obj, Boolean.class);
        }).collect(Collectors.toList());

    }

    public static class Event {
        private Address address;
        private String name;

        public Event(Address address, String name) {
            this.address = address;
            this.name = name;
        }

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

    }

    public static class Address {
        private String city;

        public Address(String city) {
            this.city = city;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }

    }
}
1
Sourabh

Ich habe beschlossen, die Ansätze # 1/# 2 (1) und # 3 (2) zu vergleichen, und bin zu dem Schluss gekommen, dass (1) bevorzugt wird (zumindest für die Java-Serverseite).

Angenommen, ein Parameter a muss gleich 10 oder 20 sein. Unsere URL-Abfrage muss in diesem Fall wie folgt aussehen: ?a.eq=10&a.eq=20 für (1) und ?a=eq:10&a=eq:20 für (2). In Java gibt HttpServletRequest#getParameterMap() die nächsten Werte zurück: { a.eq: [10, 20] } für (1) und { a: [eq:10, eq:20] } für (2). Später müssen wir zurückgegebene Maps beispielsweise in die SQL-Klausel where konvertieren. Und wir sollten erhalten: where a = 10 or a = 20 für beide (1) und (2). Kurz gesagt sieht es so aus:

1) ?a=eq:10&a=eq:20 -> { a: [eq:10, eq:20] } -> where a = 10 or a = 20
2) ?a.eq=10&a.eq=20 -> { a.eq: [10, 20] }    -> where a = 10 or a = 20

Wir haben also die nächste Regel: Wenn wir durch die URL-Abfrage zwei Parameter mit demselben Namen übergeben, müssen wir den Operanden OR in SQL verwenden .

Aber nehmen wir einen anderen Fall an. Der Parameter a muss größer als 10 und kleiner als 20 sein. Bei Anwendung der obigen Regel erhalten wir die nächste Konvertierung:

1) ?a.gt=10&a.ls=20 -> { a.gt: 10, a.lt: 20 } -> where a > 10 and a < 20
2) ?a=gt:10&a=ls:20 -> { a: [gt.10, lt.20] }  -> where a > 10 or(?!) a < 20

Wie Sie sehen können, haben wir in (1) zwei Parameter mit verschiedenen Namen: a.gt und a.ls. Dies bedeutet, dass unsere SQL-Abfrage den Operanden AND enthält. Aber für (2) haben wir immer noch die gleichen Namen und es muss mit dem Operanden OR in SQL konvertiert werden!

Dies bedeutet, dass wir für (2) anstelle von #getParameterMap() die URL-Abfrage direkt analysieren und wiederholte Parameternamen analysieren müssen.

0
pto3

Ich weiß, das ist alte Schule, aber wie wäre es mit einer Art Bedienerüberladung?

Dies würde das Parsen der Abfrage erheblich erschweren (und nicht das Standard-CGI), aber den Inhalt einer SQL-WHERE-Klausel ähneln.

/events?properties.name=Harry&properties.address.city+neq=Washington

würde werden

/events?properties.name=='Harry'&&properties.address.city!='Washington'||properties.name=='Jack'&&properties.address.city!=('Paris','New Orleans ')

paranthesis würde eine Liste starten. Das Festhalten von Zeichenfolgen in Anführungszeichen würde das Parsen vereinfachen.

Die obige Abfrage würde sich also auf Ereignisse für Harry beziehen, die nicht in Washington oder für Jacks, die nicht in Paris oder New Orleans sind.

Es wäre eine Menge Arbeit zu implementieren ... und die Datenbankoptimierung zum Ausführen dieser Abfragen wäre ein Albtraum, aber wenn Sie nach einer einfachen und leistungsstarken Abfragesprache suchen, imitieren Sie einfach SQL :)

-k

0
schuttek