Pagination mit FastAPI

Wie man Pagination für FastAPI einfach und effizient implementiert

Wir haben uns überlegt, welche Anforderungen wir an eine Pagination haben, und eine Lösung entwickelt, die flexibel, typisiert und OpenAPI-freundlich ist, ohne unnötig komplexe Typdefinitionen im openapi.json.

Bei jedem Projekt überlegen wir uns zunächst, welche Backend-Technologie am besten passt. Neben einem klassischen CMS für Webseiten setzen wir für APIs meist auf Laravel oder FastAPI. FastAPI überzeugt uns durch die schnelle und einfache Möglichkeit, eine typisierte API aufzusetzen. Dank der automatischen OpenAPI-Generierung haben wir außerdem eine hohe Sicherheit bei der Entwicklung der Frontends.

Ein Thema, das in praktisch jeder API vorkommt, ist Pagination: Anstatt dass ein List-Endpunkt alle Datensätze auf einmal zurückliefert, werden die Ergebnisse in Seiten mit begrenzter Größe unterteilt.
So bleiben Antworten klein, schnell und für Clients gut verarbeitbar.

Daher haben wir eine Pagination Lösung implementiert, die wir bei verschiedenen Projekten wiederverwenden können.

Ein Code Repository mit dem vollen Beispiel kann hier gefunden werden.

Der technische Stack #

Obwohl die Lösung relativ generisch ist, sind einige Teile speziell auf die von uns gewählten Libraries zugeschnitten. Darum hier ein kurzer Überblick über unseren Stack:

Bei Verwendung einer anderen ORM-Library (außer SQLAlchemy) müssen die Hilfsfunktionen angepasst werden. Die Konzepte selbst bleiben aber übertragbar.

Anforderungen #

Vor jedem Softwareprojekt steht die Frage: Welche Anforderungen haben wir eigentlich?
Da es sich bei Pagination um ein klassisches Thema der Softwareentwicklung handelt, sind diese schnell bestimmt:

  • Pagination soll streng typisiert sein (Pydantic)
  • Filter und Sortierung müssen im Backend erfolgen
  • Filter und Sortierung sollen auch für verschachtelte Objekte funktionieren
    (z. B. person.contact_info.first_name)
  • Effiziente Datenbank Abfragen
  • Die Antwort soll eine Seite + Gesamtanzahl der Ergebnisse enthalten
  • Die definierten Klassen sollen einfach wiederverwendbar sein (Generics)
  • Das für OpenAPI generierte Schema soll gut lesbar bleiben
    (kein Literal["id", "name", "email"])
  • Endpunkte sollen in Swagger UI benutzbar sein
  • Wir halten uns an REST-Standards → GET-Endpunkte haben keine Body-Parameter

Generische Klassen #

Auf Basis der oben definierten Anforderungen haben wir einige generische Klassen entwickelt, die auf der BaseModel-Klasse von Pydantic basieren.

Pagination #

types/pagination.py
class Pagination[T](BaseModel):
    page: list[T]
    total: int

Die Pagination-Klasse definiert den generischen Rückgabewert eines List-Endpunkts.

  • page ist ein Array der jeweiligen Platzhalter-Klasse (T).
  • total gibt die Gesamtanzahl aller Treffer zurück.

ListInput, Filter und Sort #

types/pagination.py
class ListInput[T](BaseModel):
    limit: int | None = Field(100, gt=0, le=100)
    offset: int | None = Field(0, ge=0)
    sort: list[Sort[T]] | None = None
    filter: list[Filter[T]] | None = None

Die ListInput-Klasse definiert, welche Argumente an einen List-Endpunkt übergeben werden können. Da sie generisch ist, lassen sich die erlaubten Argumente pro Endpunkt einfach anpassen. Folgende Parameter stehen zur Verfügung:

  • limit bestimmt die maximale Anzahl an Werten pro Seite
  • offset is die Anzahl an Werten, die übersprungen werden sollen
  • sort und filter sind die Sortierung und Einschränkung der Ergebnisse
types/pagination.py
class Sort[T](BaseModel):
    sort_field: T | None = None
    sort_order: Literal["asc", "desc"] | None = None

Die Sort-Klasse ermöglicht es:

  • ein Feld (sort_field) festzulegen, nach dem sortiert wird,
  • sowie die Sortierreihenfolge (sort_order) mit den Werten asc oder desc anzugeben.
types/pagination.py
filter_functions = Literal["eq", "lt", "lte", "gt", "gte", "not", "like", "isnull"]

class Filter[T](
    BaseModel,
):
    filter_field: T
    filter_function: filter_functions
    filter_value: str | int | float | bool | datetime

    @field_validator("filter_value", mode="before")
    def parse_datetime(cls, v):
        if isinstance(v, str):
            try:
                # Try parsing the string as a datetime
                return datetime.fromisoformat(v)
            except ValueError:
                pass  # If it can't be parsed, return it as a string
        return v

Die Filter-Klasse erlaubt es:

  • ein Feld (filter_field) zu wählen,
  • eine Vergleichsfunktion (filter_function) auszuwählen,
  • und einen Wert (filter_value) zu übergeben.

Da Datumswerte als ISO-String übertragen werden, prüft der field_validator, ob es sich um ein Datum handelt, und wandelt es gegebenenfalls um. Ohne diese Konvertierung wären echte Datumsvergleiche auf Datenbankebene nicht möglich. Die verfügbaren Filterfunktionen haben wir bewusst eingeschränkt, um im Frontend eine klare und passende Auswahl anbieten zu können.

Probleme im Swagger UI #

Auch wenn FastAPI ein OpenAPI-Schema generiert und die Eingaben im Swagger UI auf den ersten Blick korrekt aussehen, gibt es ein Problem, sobald man Endpunkte mit den generischen Klassen ausprobiert:

response.json
{
  "detail": [
    {
      "type": "model_attributes_type",
      "loc": [
        "query",
        "sort",
        0
      ],
      "msg": "Input should be a valid dictionary or object to extract fields from",
      "input": "{\"sort_field\":\"id\",\"sort_order\":\"asc\"}"
    }
  ]
}

Das Problem ist, dass Swagger Objektfelder als JSON-Strings überträgt. Diese Strings kann Pydantic aber nicht automatisch in Objekte umwandeln.

Es gibt verschiedene Möglichkeiten, um dieses Problem zu beheben.
Wir haben uns für einen Kompromiss entschieden:

  • Swagger bleibt voll funktionsfähig.
  • Im Frontend müssen die Argumente allerdings mit JSON.stringify() übergeben werden.

Custom Validator als Lösung #

validators.py
def convert_items_to_json(x: list[str]) -> list[dict]:
    items = []
    try:
        for item in x:
            items.append(json.loads(item))
    except Exception:
        return items
    return items

StringDictValidator = BeforeValidator(lambda x: convert_items_to_json(x) if x is not None else [])

Mit diesem Validator können String-Inputs als JSON interpretiert werden.

Angepasste ListInput -Klasse #

types/pagination.py
class ListInput[T](BaseModel):
    limit: int | None = Field(100, gt=0, le=100)
    offset: int = Field(0, ge=0)
    sort: Annotated[list[Sort[T]] | None, StringDictValidator] = Field(default=None, validate_default=True)
    filter: Annotated[list[Filter[T]] | None, StringDictValidator] = Field(default=None, validate_default=True)

Wir fügen den Validator über eine Annotation hinzu, dadurch versucht Pydantic beim Validieren, die Elemente als JSON-String zu interpretieren. Dadurch bleibt das Swagger UI nutzbar mit einer kleinen Einschränkung im Frontend.

Konkretes Beispiel #

Schauen wir uns an einem einfachen Beispiel an, wie sich die Pagination- und ListInput-Klassen in einem Endpoint verwenden lassen:

example_router.py
router = APIRouter(prefix="/example", tags=["example"])

class ContactInfo(BaseModel):
    name: str
    email: str
    phone: str

class ExampleOut(BaseModel):
    id: int
    contact: ContactInfo

example_fields = Literal["id", "contact.name", "contact.email", "contact.phone"]

@router.get(
    "/",
    response_model=Pagination[ExampleOut],
    # don't forget to add dependencies (database, user, scopes,...)
)
def list_examples(
    list_input: Annotated[ListInput[example_fields], Query()],
):
    return ExampleService().list(list_input)  # We ignore service implementation for now

Das Codesnippet beinhaltet folgende wichtigen Elemente:

  • list_input: Annotated[ListInput[example_fields], Query()]
    Wir definieren einen ListInput, bei dem der generische Typ T eine Literal-Variable (example_fields) erhält.
    Durch die Annotation mit Query() wird außerdem klar, dass die Werte als Query-Parameter und nicht als Body-Parameter erwartet werden (siehe unsere Anforderungen oben)

  • response_model=Pagination[ExampleOut]
    Als response_model verwenden wir die Pagination-Klasse und über geben für T die ExampleOut-Klasse.

Mit dieser einfachen Setup ist im alles vorbereitet. Wir müssen nur noch die Implementierung im Service ergänzen. Schon jetzt findet man im Swagger UI bzw. im generierten openapi.json eine fertig dokumentierte Schnittestelle für einen List-Endpunkt mit Pagination.

Wäre da nicht... #

Ein Problem beim Einsatz von Literal zur Definition der erlaubten Felder. Das wirkt sich auf das generierte Schema aus in dem die Namen darin unhandlich werden:

Filter[Literal['id', 'contact.name', 'contact.email', 'contact.phone']]

Das ist zwar formal korrekt, aber kaum lesbar. Sobald mehrere solcher Namen existieren, wird es schnell unübersichtlich, vor allem bei Endpunkten mit vielen Optionen.

Bessere Lösung: Enum #

Statt Literal verwenden wir eine Enum-Klasse:

example_router.py
class ExampleFields(str, Enum):
    id = "id"
    name = "contact.name"
    email = "contact.email"
    phone = "contact.phone"

@router.get(
    "/",
    response_model=Pagination[ExampleOut],
)
def list_examples(
    list_input: Annotated[ListInput[ExampleFields], Query()],
):
    return ExampleService().list(list_input)

Vorteile dieser Lösung:

  • Wir behalten die Möglichkeit, die erlaubten Felder klar zu definieren.
  • Im Code können wir Felder direkt über field.name ansprechen.
  • Im openapi.json wird der Typ sauber als Filter[ExampleFields] dargestellt, das ist klar benannt und eindeutig zuordenbar.

Integration in einen Service #

Nachdem das Interface steht, muss noch die Service-Implementierung folgen, damit auch wirklich die richtigen Ergebnisse aus der Datenbank zurückgegeben werden. Ein Beispiel für eine Service-Klasse könnte so aussehen:

example_service.py
class Example(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    contact: ContactInfo = Relationship(back_populates="example")

class ExampleService:
    # add joins to prevent lazy loading for big pages
    query_options: ClassVar[list[Options]] = [
       joinedload(Example.contact)
    ]

    # pass the db session directly to the Service or collect it from a ContextVar
    def __init__(self, db: Session | None = None):
        if db is None:
            self.db = db_session.get()
        else:
            self.db = db

    # a simple example on how to convert from SQLModel to our response_model
    @classmethod
    def to_out(cls, example: Example) -> ExampleOut:
        return ExampleOut.model_validate(example, from_attributes=True)

    def list(self, list_input: ListInputExample) -> Pagination[ExampleOut]:
        page, total = get_page(Example, list_input, self.query_options)
        return Pagination(total=total, page=[self.to_out(example) for example in page])

So weit sollte alles nachvollziehbar sein, die eigentliche Logik steckt allerdings in der get_page-Funktion. Dort wird entschieden, wie Filter, Sortierung und Pagination auf die Datenbank-Abfragen angewendet werden.

Helferfunktionen #

Unser Ziel ist es, Hilfsfunktionen zu schreiben, die möglichst unabhängig von der konkreten SQLModel-Klasse und den verwendeten Filtern überall wiederverwendbar sind. Beginnen wir mit der zentralen Funktion get_page.

get_page #

Hier die vollständige Funktion:

pagination.py
def get_page[T](model: type[T], list_input: ListInput, options: list[Options] | None = None) -> tuple[Sequence[T], int]:
    session = db_session.get()
    wheres, sorts = get_where_and_sort(model, list_input)

    filter_and_sort_fields = list(
        set(
            [f.filter_field for f in list_input.filter or [] if f.filter_field and "." in f.filter_field]
            + [s.sort_field for s in list_input.sort or [] if s.sort_field and "." in s.sort_field]
        )
    )

    statement = select(model)
    statement = apply_joins(statement, model, filter_and_sort_fields)

    if wheres:
        statement = statement.where(*wheres)
    if sorts:
        statement = statement.order_by(*sorts)

    total = get_total(statement)

    # options are initial joins to prevent lazy loading sub-tables they are not needed for the total query
    if options:
        statement = statement.options(*options)

    statement = statement.offset(list_input.offset).limit(list_input.limit)
    rows = session.exec(statement).all()
    return rows, total

Funktionssignatur #

def get_page[T](model: type[T], list_input: ListInput, options: list[Options] | None = None) -> tuple[Sequence[T], int]:

Die Funktion ist generisch (T) und erhält:

  • model: die SQLModel-Klasse (Typinformation für Query/Mapping),
  • list_input: die Client-Parameter (limitoffsetfiltersort),
  • optional options: SQLAlchemy Options (z. B. joinedload), um Lazy-Loading zu vermeiden.
  • Rückgabewert: ein Tupel aus der Ergebnisliste (Sequence[T]) und der Gesamtanzahl (int).

Datenbank Session und Parsing von Filtern und Sorts #

session = db_session.get()
wheres, sorts = get_where_and_sort(model, list_input)

Wir holen die aktuelle DB-Session (bei uns über eine ContextVar) und extrahieren mit get_where_and_sort die WHERE- und ORDER_BY-Klauseln aus dem list_input. Diese Hilfsfunktion kapselt die Logik für Filter- und Sort-Parsing.

Notwendige Joins finden #

filter_and_sort_fields = list(
    set(
        [f.filter_field for f in list_input.filter or [] if f.filter_field and "." in f.filter_field]
        + [s.sort_field for s in list_input.sort or [] if s.sort_field and "." in s.sort_field]
    )
)

Wir sammeln alle Feldpfade (dot-Notation), die Beziehungen betreffen, z.B. contact.address.country. Aus diesen Feldern ermitteln wir später, welche Joins nötig sind, damit Filter/Sort auf verschachtelte Felder funktionieren.

Basis-Statement und Joins #

statement = select(model)
statement = apply_joins(statement, model, filter_and_sort_fields)

Nach dem die Aufbereitung der Input Daten abgeschlossen sind wird das initiale Select Statement erstellt statement = select(model).
Anschließend fügt apply_joins die nötigen JOINs für alle Beziehungen hinzu, die in filter_and_sort_fieldsvorkommen (mehr dazu hier).

WHERE und ORDER BY anwenden #

if wheres:
    statement = statement.where(*wheres)
if sorts:
    statement = statement.order_by(*sorts)

Filter- und Sort-Klauseln werden auf das Statement angewendet. wheres und sorts sind SQLAlchemy-Expressions, die von get_where_and_sort erzeugt wurden.

Gesamanzahl ermitteln: get_total #

total = get_total(statement)
def get_total(statement: Select) -> int:
    session = db_session.get()
    count_statement = select(func.count()).select_from(statement.order_by(None).subquery())
    return session.exec(count_statement).one()

get_total erstellt eine Zählabfrage aus dem aktuellen Statement (ohne order_by) und liefert die Gesamtanzahl der Treffer. Wichtig: order_by wird bei der Zählung entfernt, weil es die Performance beeinträchtigen kann und für das Ergebnis irrelevant ist.

Eager-loading #

if options:
    statement = statement.options(options)

Falls vorab definierte options (z. B. joinedload) übergeben wurden, fügen wir sie jetzt hinzu, das verhindert das N+1-Problem beim Zugriff auf Beziehungen in den geladenen Zeilen (siehe hier für weitere Details).

Limit, Offset und Ausfürhung #

statement = statement.offset(list_input.offset).limit(list_input.limit)
rows = session.exec(statement).all()

Schließlich setzen wir offset und limit, führen die Abfrage aus und holen die Ergebniszeilen.

Rückgabe #

return rows, total

Wir geben die Ergebnisse und die Gesamtanzahl zurück, genau die Werte, die später in unser Pagination-Objekt kommen.

get_where_and_sort #

Die Funktionen, die hier aufgerufen werden, bilden das Herzstück unserer Pagination-Implementierung:

pagination.py
from sqlalchemy.sql._typing import _ColumnExpressionArgument, _ColumnExpressionOrStrLabelArgument

Where = _ColumnExpressionArgument[bool] | bool
OrderBy = _ColumnExpressionOrStrLabelArgument[Any]

def get_where_and_sort[T](model: type[T], list_input: ListInput) -> tuple[list[Where], list[OrderBy]]:
    wheres: list[Where] = add_filter(model, list_input)
    sort: list[OrderBy] = add_order_by(model, list_input)
    return wheres, sort

Die Funktion bekommt eine SQLModel-Klasse als ersten Parameter und unseren ListInput als zweiten. Zurückgegeben werden zwei Listen von SQLAlchemy-ColumnExpressions. Um den Code leichter lesbar zu machen, wurden die internen SQLAlchemy-Typen in einfachere Aliase übersetzt (WhereOrderBy).

add_filter #

pagination.py
filter_functions_map = {
    "eq": lambda col, val: col == val,
    "not": lambda col, val: col != val,
    "lt": lambda col, val: col < val,
    "lte": lambda col, val: col <= val,
    "gt": lambda col, val: col > val,
    "gte": lambda col, val: col >= val,
    "like": lambda col, val: col.ilike(f"%{val}%"),
    "isnull": lambda col, val: col.is_(None) if val else col.is_not(None),
}

def add_filter[T](model: type[T], list_input: ListInput) -> list[Where]:
    wheres = []
    if list_input.filter is not None:
        for filter_in in list_input.filter:
            if filter_in.filter_field and filter_in.filter_function and filter_in.filter_value is not None:
                column = resolve_column(model, filter_in.filter_field)
                if column is not None and filter_in.filter_function in filter_functions_map:
                    clause = filter_functions_map[filter_in.filter_function](column, filter_in.filter_value)
                    wheres.append(clause)
    return wheres

Diese Funktion geht alle Filter der list_input-Variable durch und generiert mithilfe der filter_function_map die passenden WHERE-Statements. In dieser Variante wird Groß- und Kleinschreibung immer berücksichtigt. Soll die Schreibweise ignoriert werden, braucht es eine spezielle Behandlung von Enums. Denn SQLAlchemy bietet für solche Spalten z. B. keine .ilike-Funktion an.

add_order_by #

pagination.py
def resolve_column(model: type, path: str) -> ColumnElement | None:
    parts = path.split(".")
    attr = getattr(model, parts[0], None)

    for part in parts[1:]:
        if attr is None:
            return None
        attr = getattr(attr.property.mapper.class_, part, None)

    return attr

def add_order_by[T](model: type[T], list_input: ListInput) -> list[OrderBy]:
    order_bys = []
    if list_input.sort:
        for sort in list_input.sort:
            if sort.sort_field:
                column = resolve_column(model, sort.sort_field)
                if column is not None:
                    order_clause = asc(column) if sort.sort_order == "asc" else desc(column)
                    order_bys.append(order_clause)
    return order_bys

Die add_order_by-Funktion geht alle Sortierungen im list_input durch und versucht, die richtige Spalte anhand der Dot-Notation zu finden. Über asc() bzw. desc() wird dann die entsprechende Sortierreihenfolge erzeugt.

apply_joins #

Die längste Funktion in unserer Pagination-Implementierung kommt zum Schluss:

pagination.py
def apply_joins(statement: Select, model: type[SQLModel], filter_and_sort_fields: list[str]) -> Select:
    joined = set()
    current_path = []

    for field in filter_and_sort_fields:
        parts = field.split(".")
        if len(parts) < 2:
            continue  # Not a relationship field

        current_model = model
        current_path.clear()
        for part in parts[:-1]:  # all but last part are relationships
            current_path.append(part)
            path_str = ".".join(current_path)

            if path_str in joined:
                continue

            relationship_attr = getattr(current_model, part, None)
            if relationship_attr is None:
                break

            rel_prop = getattr(relationship_attr, "property", None)
            if not isinstance(rel_prop, RelationshipProperty):
                break

            statement = statement.join(relationship_attr)
            joined.add(path_str)
            current_model = rel_prop.mapper.class_

    return statement

Im Prinzip ähnelt diese Funktion der add_order_by-Implementierung, aber hier werden sowohl Filter als auch Sortierungen berücksichtigt. Auf Basis der Dot-Notation wird geprüft, welche Felder über Relationen aufgelöst werden müssen. Die entsprechenden Tabellen werden dann mittels .join() in das bestehende Statement eingefügt.

Fazit #

Damit ist unsere Implementierung einer flexiblen und effizienten Pagination-Lösung für FastAPI abgeschlossen. Mit nur 4 Klassen und 7 Funktionen haben wir ein System geschaffen, das sich nahtlos in jede Art von List-Endpunkt integrieren lässt. Die Lösung ist generisch, erweiterbar und nutzt die volle Stärke von SQLAlchemy und SQLModel, ohne dabei unnötig komplex zu werden. Ein weiterer Vorteil: Wir behalten die volle Kontrolle über Performance, Lesbarkeit und Erweiterbarkeit. Gerade in Projekten, in denen Pagination ein zentraler Bestandteil vieler Endpunkte ist, zahlt sich diese Investition schnell aus.

Alternativen #

Natürlich stellt sich bei so einer Funktionalität die Frage, warum man nicht auf eine bestehende Library zurückgreifen sollte. Diese sind jedoch oft weniger flexibel und effizient.

  • Viele Libraries müssen verschiedene ORMs unterstützen und können dadurch nicht so präzise arbeiten wie eine maßgeschneiderte Implementierung.
  • Manche sind ineffizient: Beispielsweise lädt FastAPI Pagination immer erst das gesamte Ergebnis und schneidet es dann in Seiten zurecht – statt die Paginierung bereits auf Datenbankebene durchzuführen.
  • Andere Projekte wie fastapi-paginate werden nicht mehr aktiv gepflegt und bringen somit langfristig Wartungsrisiken mit sich.

Mit einer eigenen, schlanken Implementierung haben wir:

  • eine saubere API für Filter, Sortierung und Pagination,
  • eine optimierte Datenbankabfrage, die unnötige Last vermeidet,
  • und eine Lösung, die zukunftssicher bleibt, weil wir sie komplett unter eigener Kontrolle haben. So entsteht eine Pagination, die nicht nur funktioniert, sondern auch verstehbar, erweiterbar und performant ist.

Exkurs: Warum helfen options bei einer besseren Performance #

Im Verlauf dieses Blog-Artikels wurde mehrfach die Frage der Performance angesprochen, insbesondere, warum die options-Variable dabei eine entscheidende Rolle spielen kann. Zum Abschluss möchte ich das Thema etwas genauer beleuchten. SQLAlchemy (und damit auch SQLModel) unterstützt standardmäßig Lazy Loading für Beziehungen. Das bedeutet: Sobald ein Objekt im Speicher liegt und auf ein Feld zugegriffen wird, das noch nicht geladen wurde, holt SQLAlchemy die benötigten Daten automatisch nach. Das ist bequem, solange nicht zwingend auf das Feld zugegriffen wird. Bei Endpunkten, die diese Daten aber immer benötigen (und das möglicherweise auch noch in einer Schleife für viele Objekte) führt Lazy Loading allerdings zu einer erheblichen Anzahl zusätzlicher Datenbankabfragen.

Beispiel #

Nehmen wir eine Tabelle Person. Diese ist mit Kontakt verknüpft, Kontakt wiederum mit Adresse, und Adresseschließlich mit Land. Wenn wir nun alle Personen laden und zusätzlich Kontakt, Adresse und Land ausgeben möchten, ergibt sich folgender Unterschied:

  • Ohne options (Lazy Loading)
    • Eine Abfrage für die Gesamtanzahl an Personen
    • Eine Abfrage für die Seite mit den Personen
    • Pro Person: eine Abfrage für den Kontakt, eine für die Adresse und eine für das Land
  • Das ergibt bei einer Seite mit nur einem Eintrag bereits 5 Abfragen.
  • Mit joinedload in den options
    • Eine Abfrage für die Gesamtanzahl
    • Eine Abfrage für die Seite mit Personen inklusive Kontakt, Adresse und Land
  • Insgesamt also nur 2 Abfragen.

Der Unterschied mag bei einer einzelnen Person noch überschaubar wirken. Bei einer Seite mit 100 Ergebnissen reden wir aber nicht mehr über 5 vs. 2 Abfragen, sondern über 302 vs. 2. Dieser Unterschied ist nicht nur theoretisch, sondern auch in der Praxis sofort in den Ladezeiten spürbar und das bei einem noch recht simplen Beispiel.

Die gezielte Verwendung von options erlaubt es uns also, unnötige Abfragen zu vermeiden und die Performance von List-Endpunkten massiv zu verbessern.

Wenn Sie Unterstützung bei Ihrem Projekt benötigen oder Hilfe beim Optimieren von langsamen Queries wünschen, kontaktieren Sie uns gerne unter hello@bitperfect.at.

veröffentlicht am