Wie entwerfe ich eine komplexe REST-API unter Berücksichtigung der DB-Leistung?

8

Ich habe einige Tutorials zum Entwerfen von REST-APIs befolgt, aber ich habe immer noch einige große Fragezeichen. Alle diese Tutorials zeigen Ressourcen mit relativ einfachen Hierarchien, und ich würde gerne wissen, wie die darin verwendeten Prinzipien auf komplexere angewendet werden. Darüber hinaus bleiben sie auf einem sehr hohen / architektonischen Niveau. Sie zeigen kaum relevanten Code, geschweige denn die Persistenzschicht. Ich bin besonders besorgt über das Laden / die Leistung der Datenbank, wie Gavin King sagte :

Sie sparen sich Mühe, wenn Sie in allen Entwicklungsphasen auf die Datenbank achten

Angenommen, meine Bewerbung bietet Schulungen für Companies. Companieshaben Departmentsund Offices. Departmentshaben Employees. Employeeshaben Skillsund Courses, und bestimmte LevelFähigkeiten sind erforderlich, um für einige Kurse unterschreiben zu können. Die Hierarchie ist wie folgt, aber mit:

-Companies
  -Departments
    -Employees
      -PersonalInformation
        -Address
      -Skills (quasi-static data)
        -Levels (quasi-static data)
      -Courses
        -Address
  -Offices
    -Address

Pfade wären etwas wie:

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

Eine Ressource abrufen

Also ok, wenn ein Unternehmen zurückkehrt, kehre ich natürlich nicht die ganze Hierarchie companies/1/departments/1/employees/1/courses/1+ companies/1/offices/../. Ich kann eine Liste mit Links zu den Abteilungen oder den erweiterten Abteilungen zurückgeben und muss auf dieser Ebene dieselbe Entscheidung treffen: Gebe ich eine Liste mit Links zu den Mitarbeitern der Abteilung oder den erweiterten Mitarbeitern zurück? Das hängt von der Anzahl der Abteilungen, Mitarbeiter usw. ab.

Frage 1 : Ist mein Denken richtig? Ist "Wo soll ich die Hierarchie abschneiden?" Eine typische technische Entscheidung, die ich treffen muss?

Nehmen wir nun an GET companies/id, ich entscheide mich auf Anfrage , eine Liste mit Links zur Abteilungssammlung und den erweiterten Büroinformationen zurückzugeben. Meine Unternehmen haben nicht viele Büros, also sollten Sie sich an die Tische setzen Officesund Addressessollten keine große Sache sein. Beispiel für eine Antwort:

GET /companies/1

200 OK
{
  "_links":{
    "self" : {
      "href":"http://trainingprovider.com:8080/companies/1"
      },
      "offices": [
            { "href": "http://trainingprovider.com:8080/companies/1/offices/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/3"}
      ],
      "departments": [
            { "href": "http://trainingprovider.com:8080/companies/1/departments/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/3"}
      ]
  }
  "name":"Acme",
  "industry":"Manufacturing",
  "description":"Some text here",
  "offices": {
    "_meta":{
      "href":"http://trainingprovider.com:8080/companies/1/offices"
      // expanded offices information here
    }
  }
}

Auf Codeebene bedeutet dies, dass ich (mit Hibernate bin ich mir nicht sicher, wie es mit anderen Anbietern ist, aber ich denke, das ist ziemlich dasselbe) ich eine Sammlung von nicht Departmentals Feld in meine CompanyKlasse aufnehmen werde, weil:

  • Wie gesagt, ich Companylade es nicht mit , also möchte ich es nicht eifrig laden
  • Und wenn ich es nicht eifrig lade, kann ich es auch entfernen, da der Persistenzkontext nach dem Laden einer Firma geschlossen wird und es keinen Sinn macht, danach zu versuchen, es zu laden ( LazyInitializationException).

Dann werde ich eine Integer companyIdin die DepartmentKlasse aufnehmen, damit ich einer Firma eine Abteilung hinzufügen kann.

Außerdem muss ich die IDs aller Abteilungen erhalten. Ein weiterer Treffer für die DB, aber kein schwerer, sollte also in Ordnung sein. Der Code könnte folgendermaßen aussehen:

@Service
@Path("/companies")
public class CompanyResource {

    @Autowired
    private CompanyService companyService;

    @Autowired
    private CompanyParser companyParser;

    @Path("/{id}")
    @GET
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findById(@PathParam("id") Integer id) {
        Optional<Company> company = companyService.findById(id);
        if (!company.isPresent()) {
            throw new CompanyNotFoundException();
        }
        CompanyResponse companyResponse = companyParser.parse(company.get());
        // Creates a DTO with a similar structure to Company, and recursivelly builds
        // sub-resource DTOs such as OfficeDTO
        Set<Integer> departmentIds = companyService.getDepartmentIds(id);
        // "SELECT id FROM departments WHERE companyId = id"
        // add list of links to the response
        return Response.ok(companyResponse).build();
    }
}
@Entity
@Table(name = "companies")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String industry;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "companyId_fk", referencedColumnName = "id", nullable = false)
    private Set<Office> offices = new HashSet<>();

    // getters and setters
}
@Entity
@Table(name = "departments")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private Integer companyId;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "departmentId", referencedColumnName = "id", nullable = false)
    private Set<Employee> employees = new HashSet<>();

    // getters and setters
}

Aktualisieren einer Ressource

Für den Aktualisierungsvorgang kann ich einen Endpunkt mit PUToder verfügbar machen POST. Da ich möchte PUT, dass ich idempotent bin, kann ich keine Teilaktualisierungen zulassen . Wenn ich jedoch das Beschreibungsfeld des Unternehmens ändern möchte, muss ich die gesamte Ressourcendarstellung senden. Das scheint zu aufgebläht. Das gleiche gilt für die Aktualisierung eines Mitarbeiters PersonalInformation. Ich denke nicht, dass es Sinn macht, alle Skills+ Courseszusammen damit zu senden .

Frage 2 : Wird PUT nur für feinkörnige Ressourcen verwendet?

Ich habe in den Protokollen gesehen, dass Hibernate beim Zusammenführen einer Entität eine Reihe von SELECTAbfragen ausführt . Ich denke, das ist nur, um zu überprüfen, ob sich etwas geändert hat, und um alle benötigten Informationen zu aktualisieren. Je höher die Entität in der Hierarchie ist, desto schwerer und komplexer sind die Abfragen. Einige Quellen empfehlen jedoch, grobkörnige Ressourcen zu verwenden. Daher muss ich erneut überprüfen, wie viele Tabellen zu viel sind, und einen Kompromiss zwischen Ressourcengranularität und Komplexität der DB-Abfragen finden.

Frage 3 : Ist dies nur eine weitere technische Entscheidung, bei der Sie wissen, wo Sie schneiden müssen, oder fehlt mir etwas?

Frage 4 : Ist dies oder wenn nicht, was ist der richtige "Denkprozess", wenn Sie einen REST-Service entwerfen und nach einem Kompromiss zwischen Ressourcengranularität, Abfragekomplexität und Netzwerk-Chat suchen?

user3748908
quelle
1
1. Ja; Da REST-Aufrufe teuer sind, ist es wichtig, die richtige Granularität zu erreichen.
Robert Harvey
1
2. Nein. Das PUT-Verb hat per se nichts mit Granularität zu tun.
Robert Harvey
1
3. Ja. Nein, dir fehlt nichts.
Robert Harvey
1
4. Das richtige Denken lautet: "Tun Sie, was Ihren Anforderungen an Skalierbarkeit, Leistung, Wartbarkeit und andere Probleme am besten entspricht." Dies erfordert möglicherweise einige Experimente, um den Sweet Spot zu finden.
Robert Harvey
4
Zu lang. Nicht gelesen. Kann dies in 4 aktuelle Fragen aufgeteilt werden?
MetaFight

Antworten:

7

Ich denke, Sie haben Komplexität, weil Sie mit Überkomplikation beginnen:

Pfade wären etwas wie:

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

Stattdessen würde ich ein einfacheres URL-Schema wie das folgende einführen:

GET companies/
    Returns a list of companies, for each company 
    return short essential info (ID, name, maybe industry)
GET companies/1
    Returns single company info like this:

    {
        "name":"Acme",
        "description":"Some text here"
        "industry":"Manufacturing"
        departments: {
            "href":"/companies/1/departments"
            "count": 5
        }
        offices: {
            "href":"/companies/1/offices"
            "count": 3
        }
    }

    We don't expand the data for internal sub-resources, 
    just return the count, so client knows that some data is present.
    In some cases count may be not needed too.
GET companies/1/departments
    Returns company departments, again short info for each department
GET departments/
    Here you need to decide if it makes sense to expose 
    a list of departments or not. 
    If not - leave only companies/X/departments method.

    Note, that you can also use query string to make this 
    method "searchable", like:
        /departments?company=1 - list of all departments for company 1
        /departments?type=support - all 'support' departments for all companies
GET departments/1
    Returns department 1 data

Auf diese Weise werden die meisten Ihrer Fragen beantwortet - Sie "schneiden" die Hierarchie sofort und binden Ihr URL-Schema nicht an die interne Datenstruktur. Wenn wir beispielsweise die Mitarbeiter-ID kennen, würden Sie erwarten, dass Sie sie wie employees/:IDoder wie abfragen companies/:X/departments/:Y/employees/:ID?

In Bezug auf PUTvs- POSTAnfragen geht aus Ihrer Frage hervor, dass Sie der Meinung sind, dass die teilweisen Aktualisierungen für Ihre Daten effizienter sind. Also würde ich einfach POSTs verwenden.

In der Praxis möchten Sie tatsächlich Datenlesevorgänge ( GETAnforderungen) zwischenspeichern, und dies ist für Datenaktualisierungen weniger wichtig. Und Updates können oft nicht zwischengespeichert werden, unabhängig davon, welche Art von Anforderung Sie ausführen (z. B. wenn der Server die Aktualisierungszeit automatisch festlegt - sie ist für jede Anforderung unterschiedlich).

Update: In Bezug auf den richtigen "Denkprozess" - da er auf HTTP basiert, können wir beim Entwerfen der Website-Struktur die normale Denkweise anwenden. In diesem Fall können wir oben eine Liste von Unternehmen haben und jeweils eine kurze Beschreibung mit einem Link zur Seite "Unternehmen anzeigen" anzeigen, auf der Unternehmensdetails und Links zu Büros / Abteilungen usw. angezeigt werden.

Boris Serebrov
quelle
5

IMHO, ich denke, Sie verpassen den Punkt.

Erstens hängen die REST-API und die DB-Leistung nicht zusammen .

Die REST-API ist nur eine Schnittstelle , sie definiert überhaupt nicht, wie Sie Dinge unter der Haube tun. Sie können es jeder DB-Struktur zuordnen, die Sie dahinter mögen. Deshalb:

  1. Entwerfen Sie Ihre API so, dass es für den Benutzer einfach ist
  2. Entwerfen Sie Ihre Datenbank so, dass sie angemessen skaliert werden kann:
    • Stellen Sie sicher, dass Sie die richtigen Indizes haben
    • Wenn Sie Objekte speichern, stellen Sie einfach sicher, dass diese nicht zu groß sind.

Das ist es.

... und schließlich riecht es nach vorzeitiger Optimierung. Halten Sie es einfach, probieren Sie es aus und passen Sie es gegebenenfalls an.

dagnelies
quelle
2

Frage 1: Ist mein Denken richtig? Ist "Wo soll ich die Hierarchie abschneiden?" Eine typische technische Entscheidung, die ich treffen muss?

Vielleicht - ich würde mir Sorgen machen, dass Sie es rückwärts machen.

Also ok, wenn ich ein Unternehmen zurückgebe, gebe ich offensichtlich nicht die gesamte Hierarchie zurück

Ich denke nicht, dass das überhaupt offensichtlich ist. Sie sollten Unternehmensrepräsentationen zurücksenden, die für die von Ihnen unterstützten Anwendungsfälle geeignet sind. Warum würdest du nicht? Ist es wirklich sinnvoll, dass die API von der Persistenzkomponente abhängt? Ist es nicht Teil des Punktes, dass die Clients bei der Implementierung nicht dieser Auswahl ausgesetzt sein müssen? Werden Sie eine kompromittierte API beibehalten, wenn Sie eine Persistenzkomponente gegen eine andere austauschen?

Wenn Ihre Anwendungsfälle jedoch nicht die gesamte Hierarchie benötigen, müssen Sie sie nicht zurückgeben. In einer idealen Welt würde die API Darstellungen von Unternehmen erstellen, die perfekt auf die unmittelbaren Bedürfnisse des Kunden zugeschnitten sind.

Frage 2: Wird PUT nur für feinkörnige Ressourcen verwendet?

Ziemlich genau - die Kommunikation der idempotenten Natur einer Änderung durch Implementierung als Put ist nett, aber die HTTP-Spezifikation ermöglicht es Agenten, Annahmen darüber zu treffen, was wirklich passiert.

Beachten Sie diesen Kommentar von RFC 7231

Eine auf die Zielressource angewendete PUT-Anforderung kann Nebenwirkungen auf andere Ressourcen haben.

Mit anderen Worten, Sie können eine Nachricht (eine "feinkörnige Ressource") einfügen, die einen Nebeneffekt beschreibt, der auf Ihrer primären Ressource (Entität) ausgeführt werden soll. Sie müssen einige Sorgfalt walten lassen, um sicherzustellen, dass Ihre Implementierung idempotent ist.

Frage 3: Ist dies nur eine weitere technische Entscheidung, bei der Sie wissen, wo Sie schneiden müssen, oder fehlt mir etwas?

Könnte sein. Möglicherweise wird versucht, Ihnen mitzuteilen, dass Ihre Entitäten nicht den richtigen Umfang haben.

Frage 4: Ist dies oder wenn nicht, was ist der richtige "Denkprozess", wenn Sie einen REST-Service entwerfen und nach einem Kompromiss zwischen Ressourcengranularität, Abfragekomplexität und Netzwerk-Chat suchen?

Dies fühlt sich für mich nicht richtig an, da Sie anscheinend versuchen, Ihr Ressourcenschema eng mit Ihren Entitäten zu koppeln, und Ihre Wahl der Persistenz Ihr Design bestimmen lässt und nicht umgekehrt.

HTTP ist im Grunde eine Dokumentanwendung. Wenn die Entitäten in Ihrer Domain Dokumente sind, dann großartig - aber die Entitäten sind keine Dokumente, dann müssen Sie nachdenken. Siehe Jim Webbers Vortrag: REST in der Praxis, insbesondere ab 36:40 Uhr.

Das ist Ihr "feinkörniger" Ressourcenansatz.

VoiceOfUnreason
quelle
Warum sagen Sie in Ihrer Antwort auf Frage 1, dass ich möglicherweise rückwärts gehe?
user3748908
Weil es sich anhörte, als wollten Sie die Anforderungen an die Einschränkung der Persistenzschicht anpassen, anstatt umgekehrt.
VoiceOfUnreason
2

Im Allgemeinen möchten Sie keine Implementierungsdetails in der API verfügbar machen. Die Antworten von msw und VoiceofUnreason kommunizieren dies beide, daher ist es wichtig, darauf einzugehen.

Denken Sie an das Prinzip des geringsten Erstaunens , zumal Sie sich Sorgen um Idempotenz machen. Schauen Sie sich einige der Kommentare in dem Artikel an, den Sie veröffentlicht haben ( https://stormpath.com/blog/put-or-post/ ). Es gibt dort viele Meinungsverschiedenheiten darüber, wie der Artikel Idempotenz darstellt. Die große Idee, die ich aus dem Artikel herausnehmen würde, ist, dass "identische Put-Anfragen identische Ergebnisse verursachen sollten". Dh wenn Sie eine Aktualisierung des Firmennamens PUTEN, ändert sich der Firmenname und nichts anderes ändert sich für dieses Unternehmen infolge dieses PUT. Dieselbe Anfrage 5 Minuten später sollte den gleichen Effekt haben.

Eine interessante Frage, über die Sie nachdenken sollten (lesen Sie den Kommentar von gtrevg im Artikel): Jede PUT-Anforderung, einschließlich einer vollständigen Aktualisierung, ändert dateUpdated, auch wenn ein Client dies nicht angibt. Würde das nicht dazu führen, dass eine PUT-Anfrage die Idempotenz verletzt?

Also zurück zur API. Allgemeine Dinge zum Nachdenken:

  • Implementierungsdetails, die in der API verfügbar gemacht werden, sollten vermieden werden
  • Wenn sich die Implementierung ändert, sollte Ihre API dennoch intuitiv und benutzerfreundlich sein
  • Dokumentation ist wichtig
  • Versuchen Sie, die API nicht zu verzerren, um Leistungsverbesserungen zu erzielen
Gelby
quelle
1
Moll beiseite : Idempotenz ist kontextgebunden. Beispielsweise können Protokollierungs- und Überwachungsprozesse innerhalb eines PUT ausgelöst werden, und diese Aktionen sind nicht idempotent. Dies sind jedoch interne Implementierungsdetails, die sich nicht auf Darstellungen auswirken, die durch die Dienstabstraktion verfügbar gemacht werden. daher so weit wie die API betrifft, die PUT ist idempotent.
K. Alan Bates
0

Wie wäre es, wenn Sie in Ihrem ersten Quartal festlegen, wo die technischen Entscheidungen getroffen werden sollen, die eindeutige ID einer Entität ermitteln, die Ihnen auf andere Weise die erforderlichen Details zum Backend liefert? Zum Beispiel hat "Unternehmen / 1 / Abteilung / 1" eine eigene eindeutige Kennung (oder wir können eine haben, die dieselbe darstellt), um Ihnen die Hierarchie zu geben. Sie können diese verwenden.

Für Ihr Q3 auf PUT mit vollständig aufgeblähten Informationen können Sie die aktualisierten Felder markieren und diese zusätzlichen Metadateninformationen an den Server senden, damit Sie diese Felder allein überprüfen und aktualisieren können.

itsraghz
quelle