Anforderungen an einen OData-Dienst (Referenz)
Der Treiber spricht jeden OData-v4-Dienst, der die Pflichtmerkmale unten anbietet. Bietet ein Dienst
zusätzlich eine der Erweiterungen an, nutzt der Treiber sie; ohne sie verhält er sich, als gäbe es
sie nicht. Diese Seite brauchen Sie, wenn Sie prüfen, ob ein Dienst mit dem Treiber zusammenarbeitet,
oder wenn Sie selbst einen Dienst für Access-Anwendungen bauen. Der Datendienst Wkl.Data.Gateway
bietet alle Merkmale dieser Seite an.
Syntax
GET <Dienststamm>/$metadata Schemadokument (CSDL, XML) GET <Dienststamm>/<Menge>?$select=…&$filter=… Lesen, Antwort JSON POST/PATCH/DELETE <Dienststamm>/<Menge>[(<Schlüssel>)] Schreiben
Parameter
Die Merkmale sind nach ihrer Verbindlichkeit für den Treiber gegliedert: Ohne die
Pflicht zum Lesen verbindet der Treiber nicht oder liest falsch; ohne die Pflicht zum Schreiben
bleiben alle Mengen schreibgeschützt; die Erweiterungen sind Empfehlungen, deren Fehlen eine
Funktion abschaltet, aber nichts verfälscht. Die Grundlagen von OData v4 beschreiben die Normen von
OASIS: OData Version 4.01 Part 1: Protocol,
Part 2: URL Conventions
(Abfrageoptionen, Literale), OData Common Schema Definition Language (CSDL) XML Representation 4.01
($metadata) und OData JSON Format 4.01.
Der Treiber verlangt nur Merkmale, die schon OData 4.0 kennt; $query (4.01) nutzt er, wenn vorhanden.
Pflicht zum Lesen
| Merkmal | Wofür der Treiber es braucht |
|---|---|
$metadata (CSDL) mit Entitätsmengen, Eigenschaften, Typen, Facetten und Schlüsseln | Katalogfunktionen, Typabbildung (siehe Escape-Klauseln und Datentypen) |
Nur die für den Aufrufer freigegebenen Entitätsmengen in $metadata | Access bietet an, was der Benutzer auch lesen darf. Das ist eine Anforderung an die Sicherheit des Dienstes, nicht an die Technik: Der Treiber verbindet auch ohne sie, zeigt dann aber Tabellen an, deren Lesen scheitert |
$select, $filter, $orderby, $top, $skip | Übersetzung von SELECT |
In $filter die Literale von OData v4 ohne Typnamen, auch für Edm.Guid (Id eq 3333…), und Vergleiche zweier Eigenschaften (ParentId eq Id) | Bedingungen auf uniqueidentifier-Spalten; Wurzeln eines Baums. Ein Dienst ohne Feld-gegen-Feld antwortet 400, der Treiber meldet 42000 – nur diese eine Bedingung scheitert |
$count | COUNT(*) ohne Laden der Zeilen |
@odata.nextLink | große Mengen seitenweise lesen |
Prefer: odata.maxpagesize (empfohlen) | größere Seiten, wenn nur schmale Spalten gelesen werden, etwa die Schlüssel. Ein Dienst, der den Wunsch nicht beachtet, liefert seine eigene Seitengröße; der Treiber liest dann mehr Seiten, sonst ändert sich nichts |
| Antworten als JSON | der Treiber liest kein Atom/XML |
Pflicht zum Schreiben
| Merkmal | Wofür der Treiber es braucht |
|---|---|
Schlüssel im Entitätstyp und Adressierung Menge(Schlüssel) | PATCH, DELETE (gelesen wird über $filter, nie über die Schlüsseladresse); eine Menge ohne Schlüssel ist schreibgeschützt (außer mit den Aktionen für Tabellen ohne Schlüssel, unten) |
POST, PATCH, DELETE mit JSON-Rumpf; Antwort mit dem gespeicherten Satz | Anlegen, Ändern, Löschen |
If-Match, Antwort 412 bei veralteter Version, Ankündigung mit Core.OptimisticConcurrency | Schreibkonflikte erkennen; ohne diese Ankündigung weist der Treiber Ändern und Löschen ab (Anlegen bleibt möglich). Der Treiber bildet das ETag selbst aus der angekündigten Spalte (Binärwert als W/"<Base64>"); der Dienst muss diese Form annehmen, siehe Was der Treiber beim Schreiben voraussetzt |
Erweiterungen (empfohlen), die der Treiber nutzt, wenn der Dienst sie anbietet
| Merkmal | Woran der Treiber es erkennt | Wofür |
|---|---|---|
Adresse für eigene Anweisungen: nimmt {"sql": …, "parameters": {…}} an, antwortet {"columns": […], "data": […], "affectedRowCount": …} | die Verbindung nennt sie (PASSTHROUGH), oder $metadata nennt sie in Store.PassThrough | nicht übersetzbare Anweisungen, Pass-Through-Abfragen |
Org.OData.Aggregation.V1.ApplySupported mit den Transformationen filter, compute, groupby, aggregate; Aggregation.CustomAggregationMethods mit Wkl.Store.V1.count | Ankündigung in $metadata | Summen, Gruppen, HAVING, DISTINCT ohne eigene Anweisungen; COUNT(Spalte) |
Gebundene Aktionen UpdateByOriginal und DeleteByOriginal an einer Menge ohne Schlüssel | $metadata deklariert beide für den Typ der Menge | Ändern und Löschen über die alten Werte |
POST <Ressource>/$query mit den Abfrageoptionen im Rumpf (OData 4.01) | ausprobiert, sobald eine Adresse länger als MAXURLLENGTH wäre; ohne diese Form antwortet der Dienst 404, 405 oder 501, und die Anfrage geht doch als GET | lange Bedingungen |
| Anmeldung über HTTP Negotiate | die Verbindung schaltet sie ein (AUTH=WINDOWS); der Dienst verlangt sie mit 401 und WWW-Authenticate: Negotiate | Windows-Anmeldung ohne Geheimnis |
| Anmeldung über HTTP Basic | die Verbindung nennt UID und PWD | Anmeldung mit Name und Kennwort |
| Schlüssel in einer Kopfzeile | APIKEY/APIKEYENV oder AUTH=KEY; Kopfzeile APIKEYHEADER, Vorgabe X-Api-Key | Dienstschlüssel und persönliche Schlüssel |
Zweiter Faktor: 401 mit WWW-Authenticate: Wkl-Totp … endpoint="<Pfad>"; POST <Pfad> mit {"code": "…"} liefert 201 mit einer Sitzung | Herausforderung in der Antwort | Codeabfrage, Sitzung in der Kopfzeile X-Wkl-Session |
Identitätswahl: Kopfzeile X-Wkl-Role; Ablehnung 403 mit X-Wkl-Roles (prozentkodiertes UTF-8, durch Kommas getrennt) | Kopfzeile in der Antwort | Auswahl der Datenbank-Identität |
Idempotency-Key an POST, PATCH, DELETE: eine Wiederholung erhält die gespeicherte erste Antwort | – (der Treiber schickt den Schlüssel immer) | Wiederholung nach Verbindungsabbruch ohne doppelten Satz |
Vokabular Wkl.Store.V1 (Begriffe unten) | Annotationen in $metadata | Auftreten als Datenbankprodukt, Schema als TABLE_SCHEM, Typnamen, Indizes, Statistik für die Planung von Access |
Transaktionen: Store.Transactions am Entitätscontainer nennt eine Adresse; POST dorthin liefert 201 mit {"id": "…"}; Anfragen mit der Kopfzeile Wkl-Transaction: <id> laufen in ihr; POST <Adresse>/<id>/commit bzw. /rollback schließt sie (204); eine unbekannte oder beendete Kennung 404, eine vom Dienst zurückgenommene 409 | Annotation in $metadata | BeginTrans/CommitTrans/Rollback, BeginTransaction; SQL_TXN_CAPABLE = SQL_TC_ALL |
Meldungen der Datenbank in error.details einer Ablehnung: je Eintrag code = Meldungsnummer als Text, message, dazu sqlState (fünf Zeichen) | Einträge mit Zahl als code | ein Diagnosesatz je Meldung mit der Nummer als native Fehlernummer und dem genannten SQLSTATE (fehlt er: 42000), wie beim Treiber von SQL Server; ohne solche Einträge ein Satz mit dem HTTP-Status als Nummer |
Core.Computed | Annotation an der Eigenschaft | nicht schreibbare Spalten (Identität, berechnet, Zeilenversion); der Treiber meldet sie über SQLColAttribute (SQL_DESC_UPDATABLE) als schreibgeschützt, alle übrigen als „unbekannt“ |
Begriffe des Vokabulars Wkl.Store.V1. Das Vokabular ist eine Erweiterung dieses Produkts,
kein Teil von OData. Ein Dienst, der es anbietet, legt es unter einer eigenen Adresse ab und bindet es
mit edmx:Reference ein (Beispiel unten); der Treiber erkennt die Begriffe am Namensraum, mit oder
ohne Alias.
| Begriff | Typ | Gilt für | Bedeutung für den Treiber |
|---|---|---|---|
Product | Edm.String | Entitätscontainer | Name des Datenbankprodukts, gemeldet über SQLGetInfo(SQL_DBMS_NAME); ohne den Begriff meldet der Treiber OData und Version 4.0 |
ProductVersion | Edm.String | Entitätscontainer | Haupt- und Nebenversion (17.00), gemeldet als SQL_DBMS_VER in der Form 17.00.0000 |
Schema | Edm.String | Entitätscontainer | Schema des Dienststamms, gemeldet als TABLE_SCHEM; ohne es speichert Access bei einer als dbo.Tabelle verknüpften Tabelle einen leeren Quellnamen |
PassThrough | Edm.String | Entitätscontainer | Adresse für eigene Anweisungen; nur am selben Ursprung wie der Dienststamm übernommen |
Transactions | Edm.String | Entitätscontainer | Adresse, die eine Transaktion öffnet |
Type | Edm.String | Eigenschaft | Typname der Datenbank, bei einer selbst nummerierten Spalte mit identity |
Indexes | Liste von Index (Name, Unique, Primary, Clustered, Columns) | Entitätstyp | Indizes für SQLStatistics |
Rows, Pages | Edm.Int64 | Entitätstyp | Zeilen und Datenseiten für SQLStatistics (CARDINALITY, PAGES); Schätzwerte |
Zusagen je Art des Dienstes
Welche Zusage des Treibers gilt, hängt davon ab, was der Dienst anbietet und einhält. Der Treiber
erkennt Ankündigungen in $metadata, nicht aber, ob ein Dienst eine Kopfzeile wie
Idempotency-Key tatsächlich beachtet. Was er nicht erkennen kann, müssen Sie vor dem Einsatz
beim Anbieter des Dienstes klären oder selbst prüfen.
| Zusage | beliebiger OData-v4-Dienst mit den Pflichtmerkmalen | Dienst mit den Erweiterungen dieser Seite | Datendienst Wkl.Data.Gateway |
|---|---|---|---|
| Lesen, Filtern, Sortieren, Zählen auf dem Dienst | ja | ja | ja |
| Schreiben mit Schreibkonflikt | ja, mit Core.OptimisticConcurrency | ja | ja |
| Wiederholung eines Lesens nach Verbindungsabbruch | ja | ja | ja |
| Wiederholung einer Änderung ohne doppelten Satz | nein: der Treiber wiederholt in der Vorgabe trotzdem; ein Dienst, der Idempotency-Key übergeht, kann einen Satz doppelt anlegen. Abhilfe: RETRY=OFF | nur, wenn der Dienst die Kopfzeile einhält – nicht erkennbar, beim Anbieter klären | ja: der Dienst merkt sich die Antwort zehn Minuten im Arbeitsspeicher |
| Transaktionen | nein (HYC00; DAO ohne Wirkung) | mit Store.Transactions | ja |
| Summen und Gruppen ohne Durchreiche | nein | mit Aggregation.ApplySupported | ja |
| Nicht übersetzbare Anweisungen | nein, 42000 | mit Durchreicheadresse | ja: geprüfte Anweisungen für jeden Aufrufer, dessen Regel den Alias nennt; ungeprüftes SQL nur mit Freigabe |
| Zweiter Faktor, Identitätswahl | nein | mit den Herausforderungen dieser Seite | ja |
Was ein Dienst für die Wiederholung zusagen muss. Der Treiber wiederholt eine Änderung höchstens
zweimal, nach 1 und nach 3 Sekunden, mit demselben Idempotency-Key. Der Dienst muss deshalb die
erste Antwort zu einem Schlüssel mindestens einige Sekunden aufbewahren und auf die Wiederholung
genau diese Antwort geben, ohne die Änderung noch einmal auszuführen. Eine überschrittene Zeitgrenze
(HYT00) wiederholt der Treiber nicht: Ob die Änderung ausgeführt wurde, ist dann unbekannt, und die
Anwendung muss den Satz neu lesen. Den Abschluss einer Transaktion (commit, rollback) wiederholt er
ebenfalls nicht.
Was ein Dienst aus Sicherheitsgründen zusagen muss. Er lehnt eine Anfrage ohne gültige Anmeldung oder zweiten Faktor ab, bevor er etwas ausführt; der Treiber sendet sie danach erneut. Und er leitet Adressen unter dem Dienststamm nicht auf einen anderen Ursprung um (siehe Restrisiken).
Rückgabewert
Keiner. Der Treiber prüft die Merkmale beim Verbinden und beim jeweiligen Gebrauch, nicht vorab.
Bemerkungen
- Ohne
Core.OptimisticConcurrencystellt Access beim Ändern die alten Werte aller Spalten in die Bedingung. Das lässt sich in OData nicht ausdrücken; der Treiber weist es ab, statt die Prüfung fallen zu lassen. Abhilfe schafft der Dienst, etwa mit einer berechneten Nebenläufigkeitseigenschaft. - Ohne Gruppieren und ohne Adresse für eigene Anweisungen scheitern Summenabfragen und Verbünde aus Access mit „ODBC-Aufruf fehlgeschlagen“; siehe Fehlerbehebung.
- Ohne
Idempotency-Key-Unterstützung kann eine Wiederholung nach einem Verbindungsabbruch einen Satz doppelt anlegen oder einen Schreibkonflikt melden, obwohl die Änderung durchging. Ein Dienst, der die Kopfzeile nicht kennt, übergeht sie; der Treiber erkennt das nicht. Die Kopfzeile beschreibt ein abgelaufener Entwurf der IETF, kein Standard (siehe Normen und Spezifikationen). - Eigene Anweisungen auf der Leitung. Der Treiber schickt
{"sql": …, "parameters": {…}}und erwartet{"columns": […], "data": […], "affectedRowCount": …}. Gebundene Werte bleiben Werte: Die Platzhalter?, die ODBC nach Position zählt, werden zu benannten Platzhaltern – ein Fragezeichen in einer Zeichenkette, einem geklammerten Namen oder einem Kommentar bleibt dabei unangetastet. Eine Antwort ohnecolumnswird abgewiesen. Die Typen aus den Werten der ersten Zeile zu erschließen, hieße: eine ganze Zahl in einer Dezimalspalte wird zur Ganzzahl, ein Zeitpunkt wird Text, und ein leeres Ergebnis hat gar keine Form – während die Anwendung ihre Puffer bindet, bevor sie die erste Zeile liest. - Ausgabeparameter, weitere Ergebnismengen, Beschreiben. Bindet eine Anwendung Ausgabeparameter, schickt der Treiber zusätzlich
"outputs": {"p1": "Edm.Int32", "p2": {"type": "Edm.String", "maxLength": 20}}und liest die Werte ausoutputsder Antwort; weitere Ergebnismengen liest er ausresults(je Mengecolumns,data,truncated). Fragt eine Anwendung vor dem Ausführen nach den Spalten, schickt er"describe": trueund erwartet die Spalten der ersten Menge, ohne dass der Dienst etwas ausführt. Das Beschreiben verlangt er nur von einem Dienst, der die Adresse selbst inStore.PassThroughnennt: Ein Dienst, derdescribeübergeht, würde die Anweisung sonst ausführen. Liefert ein Dienst keinoutputs, erhält die AnwendungNULLals Ausgabewert. - Beschreibung der Spalten. Neben
name,typeund den Facetten (maxLength,precision,scale) liest der Treiber drei optionale Angaben je Spalte:storeType(der Typname der Datenbank, wieStore.Typein$metadata),identity(truefür einen Wert, den die Datenbank vergibt) undlabel(der Name, den die Datenbank der Spalte gibt, wo er vonnameabweicht). Zeilen sind JSON-Objekte; für eine Spalte ohne Namen oder mit einem schon vergebenen Namen muss der Dienst deshalb einen eigenen Schlüssel vergeben. Mitlabelzeigt der Treiber der Anwendung den Namen der Datenbank – leer oder doppelt, wie der ODBC Driver 18 – und liest die Werte untername. Ohnelabelsieht die Anwendung diesen Schlüssel. Store.PassThroughgilt nur am selben Ursprung (Schema, Rechner, Port) wie der Dienststamm.- Felder einer Antwort, die der Treiber nicht kennt, übergeht er. Auf dem Durchreichepfad liest er
columns,data,affectedRowCount,truncated,resultsundoutputs(siehe Grenzen). - Keine Umleitung an einen anderen Ursprung. Der Treiber folgt einer Umleitung nur beim Lesen und nur am selben Schema, Rechner und Port; leitet der Dienst anderswohin um, scheitert die Anfrage mit
08S01.
Beispiel
Ausschnitt eines Schemadokuments mit den Erweiterungen, die Access zum Bearbeiten und Gruppieren braucht:
<edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml"> <edmx:Include Namespace="Org.OData.Aggregation.V1" Alias="Aggregation" /> </edmx:Reference> <edmx:Reference Uri="https://daten.example.com/odata/v4/$vocabulary/Wkl.Store.V1.xml"> <edmx:Include Namespace="Wkl.Store.V1" Alias="Store" /> </edmx:Reference> … <EntityContainer Name="Container"> <Annotation Term="Store.Product" String="Microsoft SQL Server" /> <Annotation Term="Store.ProductVersion" String="17.00" /> <Annotation Term="Store.Schema" String="dbo" /> <Annotation Term="Aggregation.ApplySupported"> <Record> <PropertyValue Property="Transformations"> <Collection><String>filter</String><String>compute</String><String>groupby</String><String>aggregate</String></Collection> </PropertyValue> <PropertyValue Property="Rollup" EnumMember="Aggregation.RollupType/None" /> </Record> </Annotation> <Annotation Term="Aggregation.CustomAggregationMethods"> <Collection><String>Wkl.Store.V1.count</String></Collection> </Annotation> </EntityContainer>
So liefert es der Datendienst (gekürzt). Die Begriffe stehen mit dem Alias, den die
edmx:Reference vergibt; der Treiber liest beide Schreibweisen.
Gilt für
Treiber Wkl OData, beide Bitbreiten. OData v2 und v3 sowie Dienste ohne $metadata oder ohne JSON
werden nicht unterstützt.
Ausnahmen
| SQLSTATE | Bedingung |
|---|---|
42000 | Eine Anweisung lässt sich nicht übersetzen, und es gibt keine Adresse für eigene Anweisungen |
HYC00 | Die Anwendung verlangt eine Transaktion, und der Dienst bietet keine an (kein Store.Transactions) |
40001 | SQLEndTran mit Commit: Der Dienst hat die Transaktion schon zurückgenommen (nach einem Fehler, der sie unbrauchbar machte, oder nach seiner Leerlaufzeit); nichts davon ist gespeichert |
08S01 | Der Dienst ist nicht erreichbar, oder ein Dokument bricht ab (etwa eine gestreamte Antwort) |