REST-Jdbc Treiber

Einleitung

Der RestJdbc Treiber ermöglicht das Lesen und Schreiben von Daten aus bzw. in REST-APIs über JDBC mit SQL.

Es gelten folgende Konventionen:

  • als Datenbank wird die Verbindung jdbc:rest: verwendet
  • die API wird mit Hilfe einer Spec-Datei (JSON oder YAML) spezifiziert
  • die JDBC-URL enthält die Basis-URL der REST-API (kann mit Hilfe der Spec überschrieben werden)
  • als Schema wird public verwendet (Standard)
  • als Tabellen werden Entity-Namen in der Spec angegeben (z.B. users, orders)
  • die Spalten werden ebenfalls in der Spec definiert und müssen flachen JSON-Felder der API-Antwort entsprechen
  • der Treiber unterstützt SELECT für das Lesen und INSERT, UPDATE und DELETE für das Schreiben

Features

Die Festlegung der Details einer REST API erfolgt über eine Spec-Datei in JSON oder YAML. Diese Datei definiert mögliche Tabellen, Spalten, Typen, Primärschlüssel usw.

Es stehen drei Authentifizierungsverfahren zur Verfügung:

  • none - keine Authentifizierung
  • basic - HTTP Basic-Auth mit User und Passwort
  • bearer - Authentifizierung mit Bearer Token

Ein Lesen mit einem SELECT über den Treiber wird auf ein HTTP GET mit einer JSON-Antwort abgebildet. Dabei wird ein Array in der JSON-Antwort erwartet, dass die Ergebniszeilen enthält.

Der Treiber unterstützt verschiedene Paginierungsverfahren

  • none - kein Paginierung
  • offset - Paginierung mit Limit und Offset
  • page - Paginierung mit Angabe einer Seitennummer

Das Einfügen von Datensätzen mit INSERT, resultiert in einem HTTP POST.

Das Löschen von Datensätzen kann mit DELETE erfolgen. Es führt zu einem HTTP DELETE.

Und schließlich unterstützt der Treiber UPDATEs. Diese münden in HTTP PUT oder HTTP PATCH Aufrufe.

Mit Hilfe einer FILTER-Klausel können Daten bei SELECT, UPDATE oder DELETE serverseitig gefiltert werden.

Der Treiber bietet Systemtabellen für verfügbare Tabellen, Spalten und Primärschlüssel.

JDBC URL

jdbc:rest:https://api.example.com/v1

Die Teile der JDBC-URL nach jdbc:rest: ist die Base-URL der REST-API. Sie kann in der Spec überschrieben oder ergänzt werden (baseUrl in der Spec hat Vorrang, wenn gesetzt).

Beispiel-Verbindung (Java)

Properties props = new Properties();
props.setProperty("spec", "/pfad/zu/api-spec.json");
props.setProperty("auth", "bearer");
props.setProperty("password", "mein-bearer-token");

Connection conn = DriverManager.getConnection(
    "jdbc:rest:https://api.example.com/v1", props);

Connection Properties

Property Beschreibung Erforderlich
spec Pfad zur Spec-Datei (.json, .yaml oder .yml; Dateisystem oder classpath:...) Ja
auth Auth-Verfahren: none, bearer, basic (siehe unten) Nein
password Bearer-Token oder Passwort (je nach Auth-Typ) Bei bearer/basic
user Benutzername Bei basic
token Alternative zu password für Bearer-Token Bei bearer (optional)

Spec-Pfad:

/pfad/absolut/api-spec.json
/pfad/absolut/api-spec.yaml
classpath:de/softquadrat/jdbc/rest/mock-spec.json

Authentifizierung

Auth-Verfahren und Zugangsdaten werden über Connection Properties konfiguriert.

Die Property auth legt das Verfahren fest: none, bearer oder basic. Ist auth nicht gesetzt, wird automatisch erkannt:

  • user und password gesetzt → basic
  • nur password oder token gesetzt → bearer
  • sonst → none

Keine Authentifizierung

props.setProperty("auth", "none");

Oder einfach keine Auth-Properties setzen.

Bearer Token

props.setProperty("auth", "bearer");
props.setProperty("password", "eyJhbGciOiJIUzI1NiIs...");
// alternativ:
props.setProperty("token", "eyJhbGciOiJIUzI1NiIs...");

Der Treiber sendet: Authorization: Bearer <token>

HTTP Basic

props.setProperty("auth", "basic");
props.setProperty("user", "api-user");
props.setProperty("password", "geheim");

Der Treiber sendet: Authorization: Basic <base64(user:password)>

Hinweis: OAuth2-Token-Beschaffung (Login-Flow) ist noch nicht im Treiber implementiert. Das Token muss extern beschafft und als Property übergeben werden.

Spec-Datei

Jede anzubindende REST-API benötigt eine Spec-Datei im Format JSON oder YAML. Das Format wird an der Dateiendung erkannt: .json → JSON, .yaml / .yml → YAML. Vollständige Beispiele: spec-example.json, spec-example.yaml.

Struktur (JSON)

Beispieldatei: spec-example.json

{
  "baseUrl": "https://api.example.com/v1",
  "entities": [
    {
      "name": "users",
      "path": "/users",
      "pagination": {
        "type": "offset",
        "limitParam": "limit",
        "offsetParam": "offset",
        "defaultLimit": 100
      },
      "columns": [
        { "name": "id", "type": "BIGINT", "primaryKey": true },
        { "name": "name", "type": "VARCHAR" },
        { "name": "email", "type": "VARCHAR" }
      ]
    }
  ]
}

Struktur (YAML)

Beispieldatei: spec-example.yaml

Gleicher Inhalt wie oben — Inhalt und Feldnamen sind identisch:

baseUrl: https://api.example.com/v1
entities:
  - name: users
    path: /users
    pagination:
      type: offset
      limitParam: limit
      offsetParam: offset
      defaultLimit: 100
    columns:
      - name: id
        type: BIGINT
        primaryKey: true
      - name: name
        type: VARCHAR
      - name: email
        type: VARCHAR

Spec-Felder

Die jeweiligen Entities-Einträge definieren eine Tabelle.

Feld Beschreibung
baseUrl Basis-URL der API (optional, Default aus JDBC-URL)
entities Liste der JDBC-„Tabellen“
entities[].name Tabellenname
entities[].columns Spalten mit JDBC-Typ
entities[].path REST-Pfad
entities[].dataPath JSON-Pointer
entities[].orderByParam Query-Parameter für ORDERBY-Klausel
entities[].pagination Paginierung (siehe unten)
entities[].write false = alle Schreiboperationen deaktivieren (Standard: true)
entities[].insert Optional: Override für INSERT, oder false zum Deaktivieren
entities[].update Optional: Override für UPDATE, oder false zum Deaktivieren
entities[].delete Optional: Override für DELETE, oder false zum Deaktivieren

Es ist

  • name - der Tabellenname wie er in SQL verwendet wird
  • columns - Spalten mit Namen, Datentypen und optionale Primärschlüssel Kennzeichnung
  • path - der REST-Pfad für den Aufruf der REST-API
  • dataPath - JSON-Pointer zur Zeilen-Liste im Ergebnis einer Abfrage (siehe unten)

Alle weiteren Einträge sind optionale Ergänzungen.

Spalten-Typen

Unterstützte Typen in der Spec: BIGINT, INTEGER, VARCHAR, BOOLEAN, DOUBLE, DECIMAL, TIMESTAMP, DATE.

Primärschlüssel werden durch ein "primaryKey": true an der Spalte gekennzeichnet.

dataPath – wo stehen die Zeilen im JSON?

Zum Parsen der JSON-Antwort muss per JSON-Pointer zum Array mit den Datensätzen navigiert werden. Zur Syntax siehe die RFC zu Json-Pointer.

API-Antwort dataPath
Flaches Array [{...},{...}] "" (oder Feld weglassen)
{ "data": [{...}] } /data
{ "items": [{...}] } /items

Der leere JSON-Pointer "" bezeichnet laut RFC 6901 das Wurzelelement — bei einer direkten Array-Antwort also das Array selbst. / ist nicht die Wurzel.

Flaches Array – Antwort ist direkt ein JSON-Array (Standard, dataPath weglassen oder ""):

[
  { "id": 1, "title": "Hello" },
  { "id": 2, "title": "World" }
]

Verschachtelt unter data (dataPath: "/data"):

{
  "data": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}

Verschachtelt unter items (dataPath: "/items"):

{
  "items": [
    { "sku": "A-100", "qty": 5 },
    { "sku": "B-200", "qty": 12 }
  ]
}

Es werden nur flache Felder auf oberster Ebene jedes Array-Elements als Spalten gelesen. Verschachtelte Objekte und Arrays werden als String serialisiert.

API-Antwort:

[
  {
    "id": 1,
    "name": "Alice",
    "address": { "city": "Berlin", "zip": "10115" },
    "tags": ["vip", "beta"]
  }
]

Ergebnis als JDBC-Zeile (eine Spalte pro Top-Level-Feld):

id name address tags
1 Alice {"city":"Berlin","zip":"10115"} ["vip","beta"]

Die Felder city und zip innerhalb von address werden nicht zu eigenen Spalten — dafür müssten sie in der Spec als eigene Spalten modelliert und die API entsprechend angepasst werden.

Pagination

type Bedeutung Parameter
none Ein Request, alle Zeilen
offset Limit/Offset limitParam, offsetParam, defaultLimit
page Seitennummer (1-basiert) limitParam, pageParam, defaultLimit

defaultLimit ist die Seitengröße (Anzahl Zeilen pro Request). Bei offset und page holt der Treiber automatisch weitere Seiten, bis die API eine leere Liste liefert oder weniger Zeilen als defaultLimit zurückkommen.

Ohne Paginierung (none)

Die API liefert alle Datensätze in einem Request:

"pagination": { "type": "none" }
SELECT id, name FROM users;

→ ein Aufruf: GET /users

Offset-Paginierung

Spec, wenn die API limit und offset als Query-Parameter erwartet:

"pagination": {
  "type": "offset",
  "limitParam": "limit",
  "offsetParam": "offset",
  "defaultLimit": 2
}
SELECT id, name FROM users;

Beispiel: 5 Datensätze in der API, Seitengröße 2 — der Treiber führt nacheinander aus:

# HTTP-Request Antwort (Auszug)
1 GET /users?limit=2&offset=0 [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
2 GET /users?limit=2&offset=2 [{"id":3,"name":"Carol"},{"id":4,"name":"Dave"}]
3 GET /users?limit=2&offset=4 [{"id":5,"name":"Eve"}]

Nach Request 3 endet das Nachladen (nur 1 Zeile, weniger als defaultLimit). Das ResultSet enthält alle 5 Zeilen — für die Anwendung wirkt es wie ein einzelnes SELECT.

Seiten-Paginierung

Spec, wenn die API limit und page (ab Seite 1) erwartet:

"pagination": {
  "type": "page",
  "limitParam": "limit",
  "pageParam": "page",
  "defaultLimit": 2
}
SELECT id, name FROM users;

Gleiche 5 Datensätze, Seitengröße 2:

# HTTP-Request Antwort (Auszug)
1 GET /users?limit=2&page=1 [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
2 GET /users?limit=2&page=2 [{"id":3,"name":"Carol"},{"id":4,"name":"Dave"}]
3 GET /users?limit=2&page=3 [{"id":5,"name":"Eve"}]

Beispiel JSONPlaceholder

JSONPlaceholder nutzt _limit und _page:

"pagination": {
  "type": "page",
  "limitParam": "_limit",
  "pageParam": "_page",
  "defaultLimit": 10
}
SELECT id, title FROM posts;

GET /posts?_limit=10&_page=1, dann _page=2, … bis alle 100 Posts geladen sind.

users und albums bei JSONPlaceholder haben dagegen "type": "none" — ein Request reicht.

Schreiben – Defaults und Overrides

Jede Entity braucht einen expliziten path in der Spec (REST-Endpunkt für GET {baseUrl}{path}). Der Treiber leitet ihn nicht aus dem SQL-Tabellennamen (name) ab — name und path können unterschiedlich sein (z. B. "name": "orders", "path": "/api/v2/order").

Derselbe path gilt auch für Schreiben. INSERT, UPDATE und DELETE sind standardmäßig eingeschaltet und der Treiber bildet sie so ab:

SQL Default HTTP
INSERT INTO … POST {path}
UPDATE … FILTER id=1 PUT {path}/{id}
DELETE … FILTER id=1 DELETE {path}/{id}

Wie der Treiber im Standard funktioniert: dass Schreiben ist erlaubt, die HTTP-Methode und das URL-Muster leiten sich aus dem Pfad ab ({path} für INSERT, {path}/{id} für UPDATE/DELETE).

{id} im Pfad wird durch den Wert aus FILTER param=wert ersetzt (z. B. FILTER id=1/posts/1).

Override bei abweichender API

Der Pfad und die Methode können zum Beispiel beim UPDATE überschrieben werden:

{
  "name": "posts",
  "path": "/posts",
  "update": { "path": "/v2/orders/{id}", "method": "PATCH" }
}

Schreiben deaktivieren

Das Schreiben kann so deaktiviert werden:

{
  "name": "users",
  "path": "/users",
  "write": false
}

Das geht auch einzeln: "insert": false, "update": false, "delete": false.

SQL-Syntax

Grundlegende SELECT-Abfrage

SELECT id, name, email
FROM users

Das entspricht: GET {baseUrl}/users.

FILTER-Klausel

Die FILTER-Klausel leitet Filter an die REST-API weiter. Es wird also serverseitig und nicht lokal (wie bei WHERE) gefiltert. Die Syntax ist immer param=wert:

SELECT id, name FROM users FILTER id=2
;

SELECT id, title FROM posts FILTER userId=1
;

DELETE FROM posts FILTER id=1
;

Das erzeugt z.B. GET /users?id=2, GET /posts?userId=1, DELETE /posts/1.

Der Name links vom = ist der HTTP-Query-Parameter bzw. Platzhalter im DELETE-Pfad ({id}). Er muss nicht mit einer SELECT-Spalte übereinstimmen – z. B. FILTER userId=1 filtert Posts nach Autor, obwohl du id (Post-ID) selektierst.

API mit generischem Suchparameter (z. B. q):

SELECT id, name FROM users FILTER q=status = 'active'
;

GET /users?q=status+%3D+%27active%27

Hinweis: WHERE filtert lokal auf bereits geladene Zeilen. Für REST-APIs solltest du FILTER verwenden.

ORDERBY-Klausel

Wenn orderByParam in der Spec gesetzt ist, kann man es in der Query verwenden:

SELECT id, name
FROM users
ORDERBY name ASC

Hinweis: Es heißt ORDERBY (datasqill-Dialekt), nicht ORDER BY.

Spaltennamen und Anführungszeichen

Unquoted Identifier werden vom SQL-Parser in Kleinbuchstaben normalisiert (userIduserid). Spaltennamen in der Spec mit gemischter Schreibweise (z. B. userId aus JSON-APIs) müssen in INSERT und UPDATE in Anführungszeichen stehen:

INSERT INTO posts ("userId", title, body)
VALUES (1, 'Neuer Post', 'Inhalt')
;

UPDATE posts SET title = 'Neuer Titel' FILTER id=1
;

In SELECT gilt dasselbe für Spalten mit gemischter Schreibweise:

SELECT id, "userId", title FROM posts
;

FILTER-Parameter werden als Text an die API weitergegeben und sind von der Spaltennamen-Normalisierung nicht betroffen (FILTER userId=1 bleibt userId).

INSERT

INSERT schreibt neue Datensätze über die REST-API. Im Standard führt dies zu einem POST auf den Entity-path. Dieser kann bei Bedarf überschrieben werden durch:

"insert": { "path": "/v2/posts", "method": "POST" }
INSERT INTO posts ("userId", title, body)
VALUES (1, 'Neuer Post', 'Inhalt')
;

Entspricht: POST {baseUrl}/posts mit JSON-Body aus den Spalten/Werten.

UPDATE

UPDATE aktualisiert Datensätze über die REST-API. Die Angabe eines FILTER ist Pflicht. Im Standard wird ein HTTP PUT verwendet, etwa PUT auf {path}/{id}. Dies kann in der Spec mit PATCH überschrieben werden:

"update": { "method": "PATCH" }
UPDATE posts SET title = 'Neuer Titel', body = 'Neuer Inhalt' FILTER id=1
;

Entspricht: PUT {baseUrl}/posts/1 (bzw. PATCH bei Override).

DELETE

Mit DELETE können Datensätze in der REST-API gelöscht werden. Damit wird ein HTTP DELETE abgesetzt, der im Standard den Pfad aus der Spec verwendet ({path}/{id}). Auch dieser Pfad kann in der Spec überschrieben werden:

"delete": { "path": "/posts/{id}" }
DELETE FROM posts FILTER id=1
;

Der Wert aus id=1 ersetzt {id} im Pfad → DELETE {baseUrl}/posts/1.

System-Tabellen

Über das Schema system stehen virtuelle Tabellen zur Verfügung:

Tabelle Inhalt
table_list Alle Entities aus der Spec
column_list Spalten aller Entities
pk_list Primary Keys aus der Spec
SELECT table_name, table_type, remarks
FROM system.table_list
WHERE table_name = 'users'
;

SELECT column_name, type_name
FROM system.column_list
WHERE table_name = 'users'
;

SELECT table_name, column_name, key_seq
FROM system.pk_list
WHERE table_name = 'users'
;

JSONPlaceholder

JSONPlaceholder ist eine öffentliche Test-API ohne Authentifizierung – gut geeignet, um den Treiber auszuprobieren. Eine fertige Spec mit den Endpunkten users, posts, comments, albums, photos und todos liegt unter jsonplaceholder-spec.json.

Verbindung (Java):

Properties props = new Properties();
props.setProperty("spec", "/pfad/zu/jsonplaceholder-spec.json");

Connection conn = DriverManager.getConnection(
    "jdbc:rest:https://jsonplaceholder.typicode.com", props);

Beispiel-Abfragen:

SELECT id, name, email FROM users
;

SELECT id, title FROM posts FILTER userId=1
;

SELECT id, name, email FROM comments FILTER postId=1
;

FILTER userId=1 auf posts erzeugt GET /posts?userId=1. FILTER postId=1 auf comments liefert Kommentare zu Post 1.

Bei posts, comments, photos und todos ist Pagination über _limit und _page konfiguriert; der Treiber holt alle Seiten automatisch. users und albums kommen in einem Request vollständig (Pagination none).

Verschachtelte Felder bei /users (address, company) sind in der Spec bewusst weggelassen – der Treiber liest nur flache Top-Level-Felder.

INSERT-Beispiel:

INSERT INTO posts ("userId", title, body) VALUES (1, 'Test', 'Body');

UPDATE-Beispiel:

UPDATE posts SET title = 'Test', body = 'Body' FILTER id=1;

DELETE-Beispiel:

DELETE FROM posts FILTER id=1;

Anmerkungen

Verschachteltes JSON

Felder wie "address": { "city": "Berlin" } werden nicht automatisch zu Spalte address.city. Nur Top-Level-Felder jedes Array-Elements sind Spalten.

Fehlerbehandlung

HTTP-Fehler (4xx, 5xx) führen zu SQLException mit Statuscode und Response-Body.