HTTP: OData v4 (Referenz)
Die OData-v4-Oberfläche unter /odata/v4/ – für Access über den ODBC-Treiber, Excel und Power BI über
Power Query und jeden anderen OData-Client. Sie liest Tabellen und Sichten, schreibt einzelne Sätze,
fasst über $apply zusammen und beschreibt sich selbst im Schemadokument.
Syntax
Eine zweite Oberfläche über denselben Katalog, für Clients, die OData v4 sprechen statt des
eigenen Vertrags: Power Query in Excel und Power BI, und ein ODBC-Treiber für OData, über den
Access Tabellen verknüpft. Ein solcher Client lernt alles aus dem Schemadokument und folgt
den Anmerkungen des Protokolls. Die Abmachung window oder den Schlüssel im Abfrageteil
des eigenen Vertrags kennt er nicht. Anmeldung, Freigaben, Ratenbegrenzung, Audit und Fehlerform
sind dieselben wie auf dem eigenen Vertrag (siehe HTTP: Fehlerform
und HTTP: Anmeldung).
| Methode | Pfad | Bedeutung |
|---|---|---|
GET | /odata/v4/ | Liste der Datenbanken (Aliasse) |
GET | /odata/v4/{alias}/ | Liste ihrer Schemata |
GET | /odata/v4/{alias}/{schema}/ | Dienstdokument |
GET | /odata/v4/{alias}/{schema}/$metadata | Schemadokument (XML) |
GET | /odata/v4/{alias}/{schema}/{menge} | $select $filter $orderby $top $skip $count $apply |
GET | /odata/v4/{alias}/{schema}/{menge}({schlüssel}) | ein Satz; nur $select |
GET | /odata/v4/{alias}/{schema}/{menge}/$count | Anzahl; nur $filter |
POST | /odata/v4/{alias}/{schema}/{menge}[/$count]/$query | dasselbe, Optionen im Rumpf |
POST | /odata/v4/{alias}/{schema}/$transactions | Transaktion öffnen (Erweiterung, siehe unten) |
POST | /odata/v4/{alias}/{schema}/$transactions/{id}/commit | Transaktion abschließen |
POST | /odata/v4/{alias}/{schema}/$transactions/{id}/rollback | Transaktion zurücknehmen |
QUERY | /odata/v4/{alias}/{schema}/{menge}[/$count] | wie POST …/$query, Optionen im Rumpf |
POST | /odata/v4/{alias}/{schema}/{menge} | Rumpf: Felder; Antwort 201 + Satz |
PATCH | /odata/v4/{alias}/{schema}/{menge}({schlüssel}) | Rumpf: Felder; Antwort 200 + Satz |
DELETE | /odata/v4/{alias}/{schema}/{menge}({schlüssel}) | Antwort 204 |
POST | /odata/v4/{alias}/{schema}/{menge}/UpdateByOriginal | nur Tabellen ohne Schlüssel, siehe unten |
POST | /odata/v4/{alias}/{schema}/{menge}/DeleteByOriginal | nur Tabellen ohne Schlüssel, siehe unten |
Parameter
PUT beantwortet der Dienst mit 405, wie auf dem eigenen Vertrag auch.
Welche Datenbanken und Schemata ein Aufrufer nutzen kann, sagen die beiden Listen über den
Dienstwurzeln – für Clients, die eine Auswahl anbieten, etwa den Verbindungsdialog des
ODBC-Treibers wie die Datenbankliste im Dialog von SQL Server. Es ist ein gewöhnliches
Dienstdokument des Protokolls, dessen Einträge die Art ServiceDocument haben – die Art, die
OData für den Verweis auf einen weiteren Dienst vorsieht (OData JSON Format 4.01, Abschnitt „Service
Document“). Ein generischer Client kann ihnen
folgen, ohne diesen Dienst zu kennen:
{"@odata.context":"https://daten.example.com/odata/v4/$metadata",
"value":[{"name":"fachdaten","kind":"ServiceDocument","url":"fachdaten/"}]}
| Regel | Warum |
|---|---|
| Gelistet wird nur, was der Aufrufer lesen darf: ein Alias, wenn eine Regel ihm Tabellen oder Sichten dort erlaubt; ein Schema, wenn es mindestens eine lesbare Tabelle oder Sicht enthält | Die Liste verrät nichts, was der Aufrufer nicht ohnehin erreicht |
Wer nichts lesen darf, erhält eine leere Liste der Aliasse; ein Alias ohne Lesbares antwortet 404 | Wie eine Dienstwurzel ohne Lesbares |
| Die Liste der Aliasse kommt allein aus der Konfiguration; die der Schemata aus dem Katalog | Die erste öffnet keine Datenbank |
Mit einer SQL-Anmeldung allein antwortet die Liste der Aliasse 401 | Das Kennwort lässt sich nur am Server eines Alias prüfen; die Schemata eines genannten Alias sind erreichbar |
Beide Listen haben ein $metadata ohne Entitätsmengen | Ein Dienstdokument verweist auf eines |
Geschrieben wird in Tabellen und Sichten, und immer genau ein Satz. Die Anweisung entsteht
aus den Spalten des Katalogs, wie beim eigenen Vertrag; dieselben
Regeln gelten unverändert. Der Aufrufer bestimmt den Satz (über den Schlüssel in der Adresse) und
die Werte (über den Rumpf), nie eine Bedingung. Fehlt der Schlüssel in der Adresse, wird die
Anfrage abgewiesen, statt die ganze Menge zu treffen. Eine Sicht nimmt eine Änderung an, wenn
die Regeln des Aufrufers Sichten mit diesem Verb erlauben und SQL Server die Sicht für
änderbar hält – wie beim ODBC-Treiber von SQL Server, über den Access verknüpfte Sichten
bearbeitet; lehnt SQL Server ab, antwortet der Dienst 422. Spalten einer Sicht, die SQL Server
nicht aktualisieren kann – eine skalare Unterabfrage, ein Ausdruck, eine Zusammenfassung –,
meldet der Dienst als schreibgeschützt (Core.Computed); eine Änderung, die eine von ihnen nennt,
weist er mit 400 und dem Namen der Spalte ab. Ausgenommen sind Sichten mit einem
INSTEAD OF-Trigger: Dort entscheidet der Trigger, was eine solche Spalte bedeutet.
Access übernimmt diese Angabe nicht: Eine verknüpfte Sicht bietet eine solche Spalte weiter
zum Bearbeiten an, mit diesem Treiber wie mit dem ODBC Driver 18 von SQL Server, und wer sie
ändert, verliert mit der Ablehnung auch die übrigen Änderungen des Satzes. Wo Benutzer in
Access bearbeiten, verknüpfen Sie deshalb die Basistabelle zum Schreiben und die Sicht nur zum
Lesen. Der eigene Vertrag (cf) schreibt nur in Tabellen.
Jeder Schreibvorgang nennt einen Satz über seinen vollständigen Schlüssel. Die einzige Ausnahme sind freigegebene Tabellen ohne Schlüssel: Dort nennt der Aufrufer den Satz über seine alten Werte, und auch dann wird genau ein Satz geschrieben.
Die Antwort ist der gespeicherte Satz mit seinem neuen ETag; beim Anlegen zusätzlich die
Adresse des neuen Satzes im Kopf Location. So kennt der Aufrufer Identitätsschlüssel,
Vorgabewerte und berechnete Spalten, ohne den Satz noch einmal lesen zu müssen.
Ein Schlüssel, den die Datenbank selbst vergibt – als Identität oder per Vorgabe wie
NEWSEQUENTIALID() –, darf beim Anlegen fehlen; die Antwort und Location nennen den vergebenen
Wert.
| Antwort | Bedeutung |
|---|---|
201 / 200 + Satz | angelegt / geändert |
204 | gelöscht |
400 | Schlüssel unvollständig, Feld unbekannt oder nicht schreibbar, Wert passt nicht zum Typ |
403 | der Aufrufer darf in dieser Datenbank diese Tabelle oder Sicht nicht schreiben |
404 | kein Satz mit diesem Schlüssel |
409 | Schlüssel beim Anlegen schon vergeben; bei den Aktionen für Tabellen ohne Schlüssel: die alten Werte treffen mehrere Sätze |
412 | If-Match nennt eine veraltete Zeilenversion; nichts wurde geschrieben |
422 | die Datenbank lehnt ab (Einschränkung, Trigger, Berechtigungen unter Service), siehe Fehlerform |
429 | eine Grenze des Dienstes |
503 | Dienst oder Datenbank gestört. Ob die Änderung gespeichert ist, ist dann unbekannt – den Satz neu lesen |
504 | Zeitgrenze überschritten; ob gespeichert wurde, ist ebenfalls unbekannt |
Ein Zeitpunkt mit Zone wird für eine Spalte ohne Zone in koordinierte Weltzeit umgerechnet und als
Wanduhrzeit gespeichert: …T17:00:00Z als 17:00, …T17:00:00+02:00 als 15:00. Abgewiesen wird er
hier nicht (anders als auf der eigenen Route); siehe
Zeitpunkte, Zahlen und Namen.
Felder mit
@im Namen –@odata.etag,@odata.type– werden übergangen statt abgewiesen. Ein Client, der einen gelesenen Satz zurückschickt, führt sie mit; sie sind keine Spalten.
Ein Dienststamm je Alias und Schema. Eine Entitätsmenge heißt in OData wie ein einfacher
Bezeichner, und die Objekte einer Datenbank sind nur je Schema eindeutig. Mit dem Schema im
Stamm heißt die Menge genau wie das Objekt (APPARAT), ohne ein Trennzeichen, das auch in
einem echten Namen vorkommen könnte.
Das Schemadokument gilt je Aufrufer. Es nennt Tabellen, wenn der Aufrufer Tabellen des
Alias lesen darf, und Sichten, wenn er Sichten lesen darf – nichts, was ihm verweigert würde.
Funktionen und Prozeduren erscheinen nicht. Ein Schema ohne lesbares Objekt, auch ein
vertipptes, antwortet mit 404 und nicht mit einem leeren Dienst.
let
Quelle = OData.Feed("https://gateway.example.com/odata/v4/fachdaten/dbo/", null,
[Implementation = "2.0", Headers = [#"X-Api-Key" = "…"]]),
Apparate = Quelle{[Name = "APPARAT"]}[Data],
Keller = Table.SelectRows(Apparate, each Text.Contains([Benennung], "Keller"))
in
Keller
Der Schlüssel steht in diesem Beispiel im Klartext in der Abfrage und damit in jeder Kopie der Mappe. Das ist eine Grenze der Anmeldung per Kopfzeile, nicht dieser Oberfläche.
Mit der Windows-Anmeldung entfällt die Kopfzeile:
OData.Feed("https://gateway.example.com/odata/v4/fachdaten/dbo/", null, [Implementation = "2.0"]),
dazu im Anmeldedialog von Power Query die Anmeldeart „Windows“.
Schlüssel. Schlüssel einer Menge ist der Primärschlüssel, ersatzweise der erste eindeutige
Index – aber nur, wenn keine seiner Spalten leer sein darf und alle einen Typ haben, den das
Protokoll als Schlüssel zulässt. Eine Sicht ohne eigenen Index bekommt den Schlüssel, den SQL
Server für ihr Ergebnis nennt (sp_describe_first_result_set im Browse-Modus,
is_part_of_unique_key) – dieselbe Angabe, aus der der ODBC-Treiber von SQL Server den
Schlüssel einer Sicht ableitet. Übernommen wird er nur, wenn die Sicht alle diese Spalten zeigt:
Muss SQL Server eine Schlüsselspalte einer Basistabelle ergänzen, die die Sicht nicht auswählt
(ein Verbund), identifizieren die sichtbaren allein keinen Satz. Eine Sicht ohne solchen Schlüssel wird ohne Schlüssel angeboten: lesbar und filterbar,
aber kein einzelner Satz adressierbar. Das Protokoll erwartet eigentlich einen; ohne diese
Ausnahme wären gerade die Sichten unerreichbar, über die ein Client lesen soll.
Beschrieben wird eine Sicht nach ihrem Ergebnis, nicht nach ihrer gespeicherten
Spaltenliste. Eine Sicht über SELECT *, deren Tabellen sich seither geändert haben, behält die
alte Liste, bis jemand sp_refreshview ausführt, und SQL Server liefert die neuen Spalten unter
den alten Namen; Werte kommen dann unter falschen Spaltennamen und mit falschem Typ an. Der
Dienst beschreibt deshalb jede Sicht über sys.dm_exec_describe_first_result_set, wie der
ODBC-Treiber ein Ergebnis beschreibt, und schreibt einmal je Sicht eine Warnung ins Protokoll.
Was der Dienst über die Datenbank sagt. Was OData nicht kennt, ein Client aber braucht, um
sich gegenüber einer Anwendung wie der Treiber des Produkts zu verhalten, steht als Annotation
eines eigenen Vokabulars in $metadata. Das Vokabular (Namensraum Wkl.Store.V1, Alias
Store) liegt ohne Anmeldung unter /odata/v4/$vocabulary/Wkl.Store.V1.xml; ein Client, der
es nicht kennt, übergeht die Annotationen und verliert nichts.
| Annotation | Wo | Inhalt |
|---|---|---|
Store.Product, Store.ProductVersion | Container | Microsoft SQL Server, 17.00 – Haupt- und Nebenversion, ohne Build |
Store.Schema | Container | Schema des Dienststamms, wie die Datenbank es nennt (dbo); der ODBC-Treiber meldet es als TABLE_SCHEM, woraus Access den Quellnamen verknüpfter Tabellen bildet |
Store.PassThrough | Container | Adresse für eigene Anweisungen; nur für einen Aufrufer, der sie benutzen darf: Alias mit AllowPassThrough und Regel mit sql oder Anweisung unter eigener Anmeldung, oder Alias mit AllowCheckedStatements (Vorgabe) für geprüfte Anweisungen |
Store.Type | Eigenschaft | Typname der Datenbank, bei einer Identitätsspalte mit identity (int identity) |
Store.Indexes | Entitätstyp | alle Zeilenspeicher-Indizes mit Name, Unique, Primary, Clustered und den Spalten in Schlüsselfolge; gefilterte und abgeschaltete nicht |
Store.Rows, Store.Pages | Entitätstyp | Zeilen und Datenseiten der Tabelle bzw. indizierten Sicht (Heap oder gruppierter Index, In-Row-Seiten), dieselben Zahlen, die sp_statistics an ODBC meldet; Stand beim Laden des Katalogs, also Schätzwerte. Fehlen bei Sichten ohne Index |
Core.Computed | Eigenschaft | nicht schreibbar: Identität, berechnete Spalte, Zeilenversion, Zeilenstempel und Spalten einer Sicht, die SQL Server als nicht aktualisierbar meldet |
Power Query in Excel für Microsoft 365 liest das erweiterte Schemadokument unverändert.
Zeilen und Seiten sind Planungshilfen, keine Zählung. Access liest sie beim Verknüpfen über
SQLStatistics und plant danach seine Verbünde. Ohne Seitenzahl schlägt Access bei einem
Verbund die Zeilen einer Tabelle unter Umständen einzeln über den Schlüssel nach, je eine Anfrage
für jede Zeile der anderen Seite; mit Seitenzahl schickt es den Verbund in einer Anweisung. Die
Zeilenzahl allein ändert den Plan nicht. Beide Zahlen stammen aus Katalogsichten, die SQL Server laufend
führt; ihr Lesen kostet unabhängig von der Tabellengröße gleich wenig und braucht kein
zusätzliches Recht.
Die Schlüsselangabe Menge(4711) oder Menge(Auftrag=7,Zeile='a') wird mit der
Filtergrammatik gelesen und danach auf ihre Form geprüft: genau ein Gleichheitsvergleich je
Schlüsselspalte, sonst nichts. APPARAT(1 or nummer ne 0) ist als Ausdruck gültig und träfe
jede Zeile; es wird mit 400 abgewiesen.
Typen und kodierte Namen: Zeitpunkte, Zahlen und Namen.
Was Power Query zeigt (Excel, OData.Feed mit Implementation="2.0"):
| Anzeige | |
|---|---|
| Spaltennamen | wie gespeichert – Power Query dekodiert selbst, etwa F&E, Prüf-Kennung, auch mit ", . und [ |
| Namen von Tabellen und Sichten in der Navigation | kodiert, etwa gw_x0020_Tabelle_x0020__x0026__x0020_Co; umbenennen lässt sich die Abfrage in Power Query |
| Filter über solche Spalten | an den Dienst gereicht, etwa [#"F&E"] <> null and [#"Prüf-Kennung"] = "F", statt die ganze Tabelle zu laden |
Zeilenversion. Führt eine Tabelle eine, trägt jeder Satz sie als @odata.etag
(W/"<Base64>"), ein Einzelsatz zusätzlich im Kopf ETag. Das Schemadokument kündigt die
Spalte mit Core.OptimisticConcurrency an. Der Wert ist derselbe, den If-Match beim
Schreiben annimmt. Für bearbeitbare Tabellen ist die Spalte keine Kür: Ein Client, der einen
Satz ändern will, hat sonst nichts, woran er eine Vorbedingung knüpfen kann. Der OData-ODBC-Treiber
dieses Produkts weist deshalb Ändern und Löschen ohne Zeilenversion ab, statt eine fremde Änderung
stillschweigend zu überschreiben. Für eine Tabelle ohne Zeilenversion tritt
der Zeilenstempel an ihre Stelle, wenn er für
sie freigeschaltet ist.
Folgeseiten bietet der Dienst nach denselben Regeln wie nextLink im eigenen Vertrag an,
hier als @odata.nextLink und als absolute Adresse, wie das Protokoll sie vorsieht. Hinter
einem vorgeschalteten Proxy muss dieser deshalb Schema und Host weiterreichen
(X-Forwarded-* mit der zugehörigen Middleware); sonst zeigen Kontext- und Folgeadressen auf den
Host, den dieser Prozess sieht.
Ist die Menge nach ihrem Schlüssel geordnet – kein $orderby des Aufrufers, ein Schlüssel aus
ganzen Zahlen, Text oder uniqueidentifier, alle Schlüsselspalten ausgewählt –, setzt die
Folgeseite nach dem Schlüssel der letzten Zeile fort ($skiptoken, etwa ?$skiptoken=3691
oder ?$skiptoken=Auftrag=7,Zeile='a') statt nach einer Zeilenzahl ($skip). $skip wird zu
OFFSET … ROWS, und SQL Server zählt für jede Seite alle vorigen Zeilen erneut ab; jede Seite
dauert dann länger als die vorige. Mit $skiptoken bleibt die Zeit je Seite gleich. Das Token ist für den Client undurchsichtig; der Dienst liest es wie die
Schlüsselangabe einer Adresse und weist es für eine anders geordnete Leseanfrage mit 400 ab.
In allen anderen Fällen bleibt es bei $skip. Ein Dezimalschlüssel ist bewusst ausgenommen:
Über die Filtergrammatik gelesen, könnte sein Wert gerundet werden.
Folgeseiten zählen nicht gegen die Anfragengrenze (Gateway:Security:RateLimit) – hier wie
im eigenen Vertrag. Jede Folgeadresse trägt dazu die Option wkl-page, einen Prüfwert über
Aufrufer, Pfad, alle übrigen Optionen und einen Ablaufzeitpunkt (15 Minuten). Der Schlüssel dafür
entsteht zufällig beim Start des Prozesses und verlässt ihn nie. Grund: Ein Client liest eine
große Menge Seite für Seite, jede Seite ist eine Anfrage, und ein Abbruch mitten im Lesen mit
429 ließe Access eine Tabelle unvollständig mit „ODBC-Aufruf fehlgeschlagen“ zurück. Gezählt
wird deshalb nur der Beginn eines Lesevorgangs. Die Regeln:
| Fall | Wirkung |
|---|---|
| Erster Abruf einer Folgeadresse, die der Dienst diesem Aufrufer ausgegeben hat | Zählt nicht |
| Derselbe Abruf noch einmal (Wiederholung nach Verbindungsabbruch, Schleife) | Zählt wie jede Anfrage |
Selbst gebautes $skip/$skiptoken, veränderte Option, verfälschter oder abgelaufener Prüfwert | Zählt wie jede Anfrage |
| Prüfwert eines anderen Prozesses (nach einem Neustart, andere Instanz hinter einem Lastverteiler) | Zählt wie jede Anfrage |
Abgewiesen wird wegen wkl-page nie: Ein ungültiger Prüfwert hebt nur die Befreiung auf;
Anmeldung und Rechte prüft der Dienst bei jeder Seite wie sonst. Die Option erscheint nur in
Adressen, die der Dienst selbst bildet; ein Client reicht sie ungelesen weiter (sie beginnt nicht
mit $).
Eine Abweichung vom Protokoll und eine Ergänzung gehen auf das Verhalten von Power Query zurück:
| Festlegung | Grund |
|---|---|
| Abweichung: Binärwerte als gewöhnliches Base64 statt base64url, das OData JSON Format 4.01 im Abschnitt „Primitive Value“ vorsieht | Die OData-Bibliothek der Office-Clients liest nur das gewöhnliche Alphabet; mit base64url bricht das Laden einer Tabelle mit Zeilenversion mit einem Umwandlungsfehler nach Edm.Binary ab |
Ergänzung: Capabilities.FilterFunctions nennt contains, startswith, endswith | Ohne diese Angabe wertet Power Query Text.Contains selbst aus und lädt dafür die ganze Tabelle; mit ihr geht die Bedingung als $filter an den Dienst. Vergleiche, Bereiche und Zeitpunkte gehen auch ohne sie als $filter hinaus |
Transaktionen
Erweiterung dieses Dienstes, nicht Teil von OData. OData sieht für mehrere Änderungen in einem
Schritt den Stapel $batch mit Änderungsgruppen vor; eine Transaktion über mehrere einzelne Anfragen,
wie sie ODBC-Anwendungen erwarten, kennt es nicht. Ein generischer OData-Client nutzt diese Erweiterung
nicht; sie ist für den ODBC-Treiber und Programme gedacht, die sie ausdrücklich ansprechen.
Ein Client klammert mehrere Anfragen in einer Transaktion, wie eine Anwendung über den Treiber von SQL
Server mit BeginTrans/CommitTrans/Rollback. Der Dienst kündigt das im Schemadokument an
(Store.Transactions am Entitätscontainer, die Adresse unten); der ODBC-Treiber meldet Transaktionen
nur dann.
| Anfrage | Antwort |
|---|---|
POST {Dienststamm}/$transactions | 201 mit {"id": "…", "idleSeconds": 120} |
jede Anfrage mit der Kopfzeile Wkl-Transaction: <id> – Lesen, Schreiben, Prozeduren, eigene Anweisungen (sql.json) | läuft in der Transaktion; sieht ihre eigenen, noch nicht gespeicherten Änderungen |
POST {Dienststamm}/$transactions/<id>/commit | 204; 409, wenn die Datenbank die Transaktion schon zurückgenommen hat – dann ist nichts davon gespeichert; 503, wenn die Verbindung zur Datenbank beim Abschluss abriss – dann ist der Ausgang unbekannt |
POST {Dienststamm}/$transactions/<id>/rollback | 204 |
| eine unbekannte, beendete oder fremde Kennung | 404 |
| Festlegung | Grund |
|---|---|
Die Transaktion hält eine eigene Verbindung, als die Anmeldung des Aufrufers, Isolation READ COMMITTED | So verhält sie sich wie über den Treiber von SQL Server: eigene Änderungen sichtbar, Sperren bis zum Ende |
| Nur der Aufrufer, der sie geöffnet hat, und nur auf demselben Alias | Eine Kennung ist kein Recht; jede Anweisung darin wird nach den Regeln des Aufrufers beurteilt wie ohne Transaktion |
Eine Anweisung, die scheitert – an der Versionsprüfung (412) oder weil die Datenbank sie ablehnt (Einschränkung, Meldung eines Triggers) –, scheitert allein; die Transaktion besteht weiter | Wie über den Treiber von SQL Server. Die erzeugte Anweisung rollt bis zu ihrem eigenen Sicherungspunkt zurück |
Nach TransactionIdleSeconds ohne Anfrage nimmt der Dienst sie zurück | Ein verschwundener Client hielte sonst Verbindung und Sperren für immer |
Höchstens MaxTransactionsPerCaller offene je Aufrufer (429) | Jede hält eine Verbindung des Pools |
| Beim Beenden des Dienstes werden offene Transaktionen zurückgenommen | – |
Tabellen ohne Schlüssel: Aktionen
{ "Name": "fachdaten", "Provider": "sqlserver", "ConnectionString": "…",
"KeylessWriteTables": [ "dbo.Protokoll" ] }
Die Formen der Einträge sind dieselben wie bei RowStampTables (schema.tabelle,
schema.*, *), ebenso die Prüfung beim Start. Eine gelistete Tabelle, die doch einen
Schlüssel hat, wird weiter über ihn geschrieben und im Protokoll genannt.
Über das Protokoll geschieht das mit zwei gebundenen Aktionen, die das Schemadokument nur für freigegebene Tabellen deklariert. Ein Client erkennt daran, ob die Tabelle beschreibbar ist:
POST /odata/v4/fachdaten/dbo/Protokoll/UpdateByOriginal
{ "original": { "Nummer": 2, "Text": "zwei", "Betrag": null, "Aktiv": false },
"changes": { "Text": "neu" } }
POST /odata/v4/fachdaten/dbo/Protokoll/DeleteByOriginal
{ "original": { … } }
Der Aktionsname darf mit dem Namensraum des Schemas stehen (fachdaten.dbo.UpdateByOriginal),
wie das Protokoll ihn schreibt. Geprüft wird als das, was die Aktion tut: Ändern braucht das
Recht für PATCH, Löschen das für DELETE. Ein Recht nur zum Anlegen öffnet diese Tür nicht.
| Regel | Grund |
|---|---|
original nennt jede vergleichbare Spalte – oder die Zeilenversion bzw. den Zeilenstempel | Eine Bedingung über weniger Spalten träfe Sätze, die nur in diesen Werten übereinstimmen |
Vergleichbar ist jede angebotene Spalte begrenzter Länge; nicht dazu gehören Langtext und lange Binärwerte (max), text, ntext, image, xml | text bis xml kann die Engine nicht mit = vergleichen; Langtext nennt Access nicht – eine Pflicht dazu ließe jede Änderung aus Access scheitern |
Ein fehlender alter Wert wird zu IS NULL | = NULL ist nie wahr; ein Ersatzwert wie ISNULL(x, 0) fiele mit echten Nullen zusammen und träfe die Zeile mit 0 mit |
Geändert wird genau ein Satz; treffen die alten Werte mehrere, wird zurückgenommen und mit 409 abgewiesen | Mehrere Treffer sind Sätze, die in allen vergleichbaren Spalten gleich sind; sie können sich in Langtext oder nicht angebotenen Spalten unterscheiden. Access direkt gegen SQL Server meldet in diesem Fall Fehler 3362 und nimmt die Änderung zurück; der Dienst verhält sich ebenso |
Nichts getroffen: 412 | Der Satz wurde geändert oder gelöscht; ohne Schlüssel ist beides nicht unterscheidbar |
Antwort bei Erfolg: 200 mit {"value": 1}.
Gruppieren und Zusammenfassen ($apply)
Eine Menge lässt sich gruppiert und zusammengefasst lesen, nach OData Data Aggregation 4.0.
Grund ist Access: Das Datenbankmodul schickt SUM, COUNT(Spalte), MIN, MAX, GROUP BY,
HAVING, DISTINCT und für eine Standardabweichung SUM(x * x) über eine verknüpfte Tabelle
an den ODBC-Treiber, gleich was dieser über sich meldet, und rechnet nicht selbst, wenn der
Treiber ablehnt – die Abfrage scheitert dann mit „ODBC-Aufruf fehlgeschlagen“. Ohne
Durchreiche, also für jeden Schlüssel ohne das Recht sql,
führen $apply und – wo der Alias sie annimmt – geprüfte Anweisungen zu Summen und Gruppen. Er ist ein Lesen: Es gelten die Leserechte
der Menge, und eine Summe über eine Million Zeilen reist als eine Zahl.
GET …/Auftrag?$apply=groupby((Werk),aggregate($count as Anzahl,Menge with sum as Summe)) GET …/Auftrag?$apply=filter(Werk eq 3)/aggregate(Menge with average as Mittel) GET …/Rolle?$apply=groupby((Abteilung),aggregate($count as N))&$filter=N gt 1&$orderby=N desc&$top=10 GET …/Rolle?$apply=compute(Id mul Id as Q)/aggregate(Q with sum as Quadratsumme) GET …/Rolle?$apply=groupby((Abteilung)) verschiedene Werte (DISTINCT)
{"@odata.context":"…/$metadata#Auftrag(Werk,Anzahl,Summe)",
"value":[{"Werk":1,"Anzahl":2140,"Summe":18350.5}, …]}
Was $apply ausführt – in dieser Reihenfolge, jede Transformation höchstens einmal:
| Transformation | Bedeutung | SQL |
|---|---|---|
filter(Bedingung) | Zeilen vor dem Gruppieren; Grammatik wie $filter | WHERE |
compute(Ausdruck as Alias, …) | Rechnen mit add, sub, mul über Zahlenspalten und Zahlen | Ausdruck im Aggregat |
groupby((Eigenschaft, …)[,aggregate(…)]) | Gruppen; ohne aggregate die verschiedenen Werte | GROUP BY |
aggregate(…) | Zusammenfassen ohne Gruppen: eine Zeile | Aggregat ohne GROUP BY |
| Aggregat | Ergebnis | SQL | Typ des Ergebnisses |
|---|---|---|---|
$count as A | Zeilen | COUNT_BIG(*) | Edm.Int64 |
x with sum as A | Summe | SUM; ganze Zahlen über bigint | ganze Zahlen Edm.Int64, Dezimal Edm.Decimal, Gleitkomma Edm.Double |
x with average as A | Mittelwert | AVG; ganze Zahlen über float | ganze Zahlen und Gleitkomma Edm.Double, Dezimal Edm.Decimal |
x with min as A, x with max as A | kleinster, größter Wert | MIN, MAX | Typ der Spalte |
x with countdistinct as A | verschiedene Werte ohne fehlende | COUNT_BIG(DISTINCT x) | Edm.Int64 |
x with Wkl.Store.V1.count as A | Werte, die nicht fehlen | COUNT_BIG(x) | Edm.Int64 |
Nach $apply beziehen sich die übrigen Optionen auf die Gruppen, wie das Protokoll es
vorsieht: $filter ist SQL HAVING und darf einen Alias nennen ($filter=N gt 1), $orderby
ordnet die Gruppen, $top, $skip und $count=true fenstern und zählen sie. Sie nennen nur,
was das Ergebnis hat – eine Gruppierungseigenschaft oder einen Alias; eine andere Spalte der
Menge weist der Dienst mit 400 ab. Ohne $orderby sind die Gruppen nach ihren
Gruppierungseigenschaften geordnet, sodass aufeinander folgende Fenster sich nicht überschneiden.
| Festlegung | Grund |
|---|---|
Summe und Mittelwert ganzer Zahlen werden vorher je Spalte nach bigint bzw. float umgewandelt | SQL Server summiert int als int und bricht jenseits von 2^31 mit einem Überlauf ab; den Mittelwert ganzer Zahlen rechnet es ohne Rest (Mittel von 1 und 2 ist 1). Die Summe des Protokolls ist so groß, wie sie ist, sein Mittelwert hat Nachkommastellen. Je Spalte, nicht am Ergebnis, damit x mul x nicht schon vorher überläuft |
div und mod gibt es in compute nicht | SQL teilt ganze Zahlen ohne Rest, OData nicht – ein Operator ergäbe zwei Antworten |
Wkl.Store.V1.count statt einer Standardmethode | Das Protokoll hat keine Methode, die Werte ohne fehlende zählt (COUNT(Spalte)); $count zählt Zeilen, countdistinct verschiedene Werte. Eine eigene Methode muss es mit Namensraum benennen; das Schemadokument kündigt sie mit Aggregation.CustomAggregationMethods an |
| Ein Alias darf nicht wie eine Eigenschaft der Menge heißen, gespeichert oder kodiert | Das Ergebnis trüge sonst zwei Werte unter einem Namen, und eine Bedingung auf die Gruppen wüsste nicht, welchen sie meint |
Nicht gruppiert, verglichen oder verschieden gezählt werden xml, text, ntext, image; nicht summiert wird Text; min/max nicht über Ja/Nein; der berechnete Zeilenstempel gar nicht | SQL Server vergleicht diese Typen nicht bzw. lehnt die Funktion ab; die Ablehnung nennt den Grund, statt einen Datenbankfehler weiterzugeben |
Ohne $top liefert eine Seite bis zu MaxTop Gruppen, nicht nur DefaultTop | Jede weitere Seite gruppiert in der Datenbank wieder alle Zeilen, bevor sie zur Seite springen kann; die Gruppen selbst sind wenige und klein. Eine Seite statt mehrerer erspart diese Wiederholung |
$select neben $apply und $skiptoken für ein gruppiertes Ergebnis lehnt der Dienst ab | Das Ergebnis hat die Eigenschaften, die $apply nennt; eine Gruppe hat keinen Schlüssel, nach dem fortgesetzt werden könnte – geblättert wird mit $skip |
Jede andere Transformation (concat, topcount, rollup …), Methode oder Stellung wird mit 400 abgewiesen, nie übergangen | Eine übergangene Transformation beantwortet eine andere Frage mit einem Ergebnis, das wie die Antwort aussieht |
Das Schemadokument kündigt an, was ausgeführt wird: Aggregation.ApplySupported mit den vier
Transformationen und ohne Rollup, dazu die eigene Zählmethode. Der ODBC-Treiber gruppiert nur
über einen Dienst, der das ankündigt. Auf der eigenen Route (/api/v1/cf/…) bleibt $apply
abgewiesen.
Die Anweisung ist eine abgeleitete Tabelle, aus der die Gruppen wie Zeilen gelesen werden:
SELECT TOP (100) * FROM ( SELECT [Kunde], COUNT_BIG(*) AS [Anzahl] FROM [dbo].[Umsatz] WHERE [Nr] > @p0 GROUP BY [Kunde] ) AS [gw_g] WHERE [Anzahl] > @p1 ORDER BY [Anzahl] DESC
Nur so kann die Bedingung auf die Gruppen einen Alias nennen und gilt für das Fenster dieselbe
Logik wie beim gewöhnlichen Lesen. Werte gehen wie überall als Parameter hinein; Zahlen aus
compute schreibt der Dienst aus dem gelesenen Wert, nie aus dem Text des Aufrufers.