Der RestJdbc Treiber ermöglicht das Lesen und Schreiben von Daten aus bzw. in REST-APIs über JDBC mit SQL.
Es gelten folgende Konventionen:
jdbc:rest: verwendetpublic verwendet (Standard)users, orders)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:
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
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: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).
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);
| 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
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 → basicpassword oder token gesetzt → bearernoneprops.setProperty("auth", "none");
Oder einfach keine Auth-Properties setzen.
props.setProperty("auth", "bearer");
props.setProperty("password", "eyJhbGciOiJIUzI1NiIs...");
// alternativ:
props.setProperty("token", "eyJhbGciOiJIUzI1NiIs...");
Der Treiber sendet: Authorization: Bearer <token>
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.
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.
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" }
]
}
]
}
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
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
Alle weiteren Einträge sind optionale Ergänzungen.
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.
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.
| 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.
none)Die API liefert alle Datensätze in einem Request:
"pagination": { "type": "none" }
SELECT id, name FROM users;
→ ein Aufruf: GET /users
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.
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"}] |
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.
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).
Der Pfad und die Methode können zum Beispiel beim UPDATE überschrieben werden:
{
"name": "posts",
"path": "/posts",
"update": { "path": "/v2/orders/{id}", "method": "PATCH" }
}
Das Schreiben kann so deaktiviert werden:
{
"name": "users",
"path": "/users",
"write": false
}
Das geht auch einzeln: "insert": false, "update": false, "delete": false.
SELECT id, name, email
FROM users
Das entspricht: GET {baseUrl}/users.
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.
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.
Unquoted Identifier werden vom SQL-Parser in Kleinbuchstaben normalisiert (userId → userid). 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 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 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).
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.
Ü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 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;
Felder wie "address": { "city": "Berlin" } werden nicht automatisch zu Spalte address.city. Nur Top-Level-Felder jedes Array-Elements sind Spalten.
HTTP-Fehler (4xx, 5xx) führen zu SQLException mit Statuscode und Response-Body.