Schon die Beatles besangen ein uraltes Problem in ihrem Song „Strawberry JSON Fields Forever“ : Wie lässt sich mit der GraphQL Library Strawberry für Python nach Werten in JSON-Feldern einer PostgreSQL-Datenbank filtern?
Setup
Um das zu zeigen, braucht es dreierlei: eine PostgreSQL-Datenbank, die eine Tabelle mit JSON-Feldern enthält, eine Python-App, die via SQLAlchemy auf die Datenbank zugreifen kann und die zudem die GraphQL-Library Strawberry unterstützt.
Datenbank
Die PostgreSQL-Datenbank lässt sich minimal mit folgendem Init-Skript beschreiben.
1CREATE TABLE "Members" 2( 3 beatle_id serial CONSTRAINT beatle_id_pk PRIMARY KEY, 4 name VARCHAR, 5 age INTEGER, 6 custom_information jsonb 7); 8 9INSERT INTO public."Members" VALUES (1, 'John Lennon', 82, '{ "favoriteNumber": 3, "height": 182 }'); 10INSERT INTO public."Members" VALUES (2, 'Paul McCartney', 80, '{ "height": 180 }'); 11INSERT INTO public."Members" VALUES (3, 'George Harrison', 79, '{ "height": 181 }'); 12INSERT INTO public."Members" VALUES (4, 'Ringo Starr', 82, '{ "height": 172 }');
Wir speichern Mitglieder der Beatles, die unterschiedliche Zusatzinformationen im Feld custom_information
haben können (Vergleich: John Lennon möchte seine Lieblingszahl speichern, den anderen ist das gar nicht so wichtig).
Python-App
Als Python-Framework wählen wir FastAPI und verbinden uns mit dem ORM-Framework SQLAlchemy zur Datenbank.
Initialisiert wird die App inklusive Datenbankverbindung in wenigen Zeilen.
1from fastapi import FastAPI 2from fastapi_sqlalchemy import DBSessionMiddleware 3 4 5app = FastAPI() 6app.add_middleware(DBSessionMiddleware, db_url="postgresql://postgres:postgres@localhost:5432/postgres")
Das Modell, um auf die Mitglieder in der Datenbank zuzugreifen sieht so aus:
1from sqlalchemy.ext.declarative import declarative_base
2from sqlalchemy import Column, Integer, String, JSON
3
4
5Base = declarative_base()
6
7
8class MemberModel(Base):
9 __tablename__ = "Members"
10
11 beatle_id = Column(Integer, primary_key=True, index=True)
12 name = Column(String)
13 age = Column(Integer)
14 custom_information = Column(JSON)
Nun könnten wir schon per ORM aus der Datenbank lesen. Das Ziel ist allerdings, mit GraphQL via Strawberry Daten abzufragen und zu filtern.
Strawberry-Schema
Dafür installieren wir die Dependency strawberry-graphql
und erzeugen uns einen Type, einen Filter und eine Query. Auf die einzelnen Teile wird im Teil „Die Lösung“ noch mehr eingegangen.
1from typing import Optional
2
3import strawberry
4from fastapi_sqlalchemy import db
5from sqlalchemy import text
6from strawberry.scalars import JSON
7
8from app.models import MemberModel
9
10
11@strawberry.input
12class Filters:
13 custom_field: Optional[str] = None
14 custom_value: Optional[int] = None
15
16
17@strawberry.type
18class Member:
19 beatle_id: int
20 name: str
21 age: int
22 custom_information: JSON
23
24 @classmethod
25 def marshal(cls, model: MemberModel) -> "Member":
26 return Member(beatle_id=strawberry.ID(model.beatle_id),
27 name=model.name,
28 age=model.age,
29 custom_information=model.custom_information)
30
31
32@strawberry.type
33class Query:
34
35 @strawberry.field(name="members")
36 def resolve_members(self, custom_information_filter: Filters = None) -> list[Member]:
37 if custom_information_filter:
38 field = custom_information_filter.custom_field
39 value = custom_information_filter.custom_value
40 members = db.session \
41 .query(MemberModel)\
42 .filter(text(f"CAST(custom_information->'{field}' AS INTEGER) = {value}"))\
43 .all()
44 else:
45 members = db.session.query(MemberModel).all()
46 return [Member.marshal(m) for m in members]
47
48
49schema = strawberry.Schema(Query)
Das Schema wird der FastAPI-App zugewiesen.
1from fastapi import FastAPI 2from fastapi_sqlalchemy import DBSessionMiddleware # middleware helper 3from strawberry.fastapi import GraphQLRouter 4 5from app.schema import schema 6 7 8app = FastAPI() 9app.add_middleware(DBSessionMiddleware, db_url="postgresql://postgres:postgres@localhost:5432/postgres") 10app.include_router(GraphQLRouter(schema), prefix="/graphql")
Die Lösung
Wo wird nun das Ursprungsproblem behoben? Dafür sind abermals drei Punkte notwendig.
Filter
Wir definieren eine Filtermöglichkeit über einen @strawberry.input
. Das ermöglicht es dem Nutzer, frei anzugeben, nach welchem konkreten Feld und Wert im JSON-Feld gesucht werden soll.
1@strawberry.input
2class Filters:
3 custom_field: Optional[str] = None
4 custom_value: Optional[int] = None
Marshaller
Die marshal
-Methode am @strawberry.type
sorgt dafür, dass das DB-Model in einen GraphQL-Type umgewandelt werden kann. Die Methode wird im Resolver aufgerufen.
1@strawberry.type
2class Member:
3 id: int
4 name: str
5 age: int
6 custom_information: JSON
7
8 @classmethod
9 def marshal(cls, model: MemberModel) -> "Member":
10 return Member(id=strawberry.ID(model.beatle_id),
11 name=model.name,
12 age=model.age,
13 custom_information=model.custom_information)
Resolver
Herzlich willkommen in der Essenz der Lösung: dem Resolver. Ihm wird der Filter übergeben. Somit wissen wir, nach welchem Wert und Feld wir im JSON-Feld filtern sollen. Mit diesen Informationen bauen wir unsere Query zusammen.
1@strawberry.type
2class Query:
3
4 @strawberry.field(name="members")
5 def resolve_members(self, custom_information_filter: Filters = None) -> list[Member]:
6 if custom_information_filter:
7 field = custom_information_filter.custom_field
8 value = custom_information_filter.custom_value
9 members = db.session \
10 .query(MemberModel)\
11 .filter(text(f"CAST(custom_information->'{field}' AS INTEGER) = {value}"))\
12 .all()
13 else:
14 members = db.session.query(MemberModel).all()
15 return [Member.marshal(m) for m in members]
Nachdem wir die Query ausgeführt haben, müssen wir die erhaltenen Modelle der Members noch in GraphQL-Typen umwandeln. Hier kommt der Marshaller ins Spiel.
Hinweis
Der Resolver kann beliebig komplex werden. In diesem Beispiel wird nur auf Gleichheit eines Felds auf oberster Ebene abgefragt. Sollte das JSON-Feld tief verschachtelt sein oder sollten die Werte auf Ranges verglichen werden, ist das hier der richtige Ort, um das zu lösen. Auch ein ORDER BY
lässt sich hier umsetzen. Entsprechend müsste man die Filter erweitern oder anpassen. Here is the place to go nuts.
Abfrage
Nun lässt sich mit GraphiQL explizit nach Beatles-Mitgliedern filtern, deren Lieblingszahl 3 ist.
query MyQuery {
members(customInformationFilter: {customField: "favoriteNumber", customValue: 3}) {
age
customInformation
id
name
}
}
und man erhält nur John Lennon mit all seinen Informationen.
{
"data": {
"members": [
{
"age": 82,
"customInformation": {
"height": 182,
"favoriteNumber": 3
},
"id": 1,
"name": "John Lennon"
}
]
}
}
Codebeispiel
Das komplette Codebeispiel findet ihr auf meinem GitHub-Account (mymindwentblvnk) .
Kennt ihr noch andere Möglichkeiten, um JSON-Felder in PostgreSQL oder anderen Datenbanken mit GraphQL abzufragen?
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Michael Eichenseer
IT Consultant & Feelgood Ambassador
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.