= select()
:description: Gibt einen HOL-Query-Builder (ECMModelQuerySync/Async) zurück. Unterstützt typisierte Modelle, where-Bedingungen, order_by, Paginierung und rights/base_params-Flags.
:keywords: select, HOL, ECMModelQuerySync, ECMModelQueryAsync, where, order_by, Paginierung, rights, base_params, ECMCondition
:page-tags: api, dms

Gibt einen `ECMModelQuerySync` (sync) bzw. `ECMModelQueryAsync` (async) zurück, mit dem Objekte gesucht, gefiltert und sortiert werden können.

== Signatur

[tabs]
====
Sync::
+
[source,python]
----
ecm.dms.select(model_class: type[T]) -> ECMModelQuerySync[T]
----

Async::
+
[source,python]
----
await ecm.dms.select(model_class: type[T]) -> ECMModelQueryAsync[T]
----
====

== Parameter

[cols="1,1,3"]
|===
|Name |Typ |Beschreibung

|`model_class`
|`type[ECMFolderModel \| ECMRegisterModel \| ECMDocumentModel]`
|Die Modelklasse, die den Objekttyp beschreibt. Kann auch über `make_folder_model()` / `make_register_model()` / `make_document_model()` dynamisch erzeugt werden.
|===

== Query-Builder Methoden

Der zurückgegebene Query-Builder unterstützt folgende Methoden (Chaining):

[cols="1,3"]
|===
|Methode |Beschreibung

|`.where(*conditions)`
|Filterbedingungen hinzufügen. Mehrere Argumente werden mit AND verknüpft. Bedingungen können mit `&` (AND) und `\|` (OR) kombiniert werden.

|`.order_by(*sort_orders)`
|Sortierrichtung festlegen. Die Reihenfolge der Argumente bestimmt die Sortierpriorität. Jedes Argument ist ein `ECMSortOrder`, erzeugt über `.ASC` / `.DESC` auf einem `ECMField`.

|`.limit(n)`
|Maximale Gesamtanzahl Treffer über alle Seiten.

|`.pagesize(n)`
|Anzahl Objekte pro Server-Anfrage (Standard: 1000). Beeinflusst die Effizienz bei großen Ergebnismengen.

|`.offset(n)`
|Startposition (0-basiert). Überspringt die ersten `n` Treffer.

|`.rights()`
|Rechte-Informationen mit abrufen (befüllt `obj.system.rights`).

|`.base_params()`
|Audit-Metadaten mit abrufen (befüllt `obj.system.base_params`): Ersteller, Änderungsdatum etc.

|`.file_properties()`
|Datei-Eigenschaften mit abrufen (befüllt `obj.system.file_properties`, nur Dokumente).

|`.variants()`
|Dokumentvarianten mit abrufen.

|`.icons()`
|Icon-IDs der Objekte mit abrufen.

|`.remarks()`
|Notizen der Objekte mit abrufen.

|`.fields(*fields)`
|Gibt nur die angegebenen Felder zurück (setzt `field_schema="MIN"`). Nützlich für performante Abfragen, wenn nur einzelne Felder benötigt werden. Ohne Argumente wird die Einschränkung zurückgesetzt.

|`.garbage_mode()`
|Nur Objekte aus dem Papierkorb zurückgeben.

|`.with_children(*specs)`
|Wechselt zu einer HOL-Abfrage, die Kindobjekte enthält. Gibt einen `ECMModelQueryHolSync` zurück.

|`.with_parents(*specs)`
|Wechselt zu einer HOL-Abfrage, die Elternobjekte enthält. Gibt einen `ECMModelQueryHolSync` zurück.

|`.execute()`
|Führt die Abfrage aus und gibt eine vollständige Liste zurück (alle Seiten im Speicher).

|`.stream()`
|Führt die Abfrage seitenweise aus und gibt einen Generator zurück. Empfohlen für große Ergebnismengen.
|===

[CAUTION]
====
`.execute()` lädt alle Objekte vollständig in den Speicher, bevor die Verarbeitung beginnt.
Bei großen Ergebnismengen kann dies zu Speicherproblemen führen.

`.stream()` arbeitet seitenbasiert — das Paging in enaio ist nicht transaktional.
Werden während des Iterierens Objekte verändert, können Seiten inkonsistente Zustände liefern
oder Objekte doppelt erscheinen. In diesem Fall sollte `.execute()` bevorzugt werden.
====

== Filterbedingungen (where)

Bedingungen werden über `.where()` übergeben. Mehrere Argumente in einem `.where()`-Aufruf werden automatisch mit AND verknüpft. Für OR-Verknüpfungen werden Bedingungen mit dem `|`-Operator kombiniert, für AND mit `&`.

=== Vergleichsoperatoren

[cols="1,1,2"]
|===
|Syntax |Operator |Beispiel

|`Field == value`
|Gleichheit
|`InvoiceFolder.Year == 2024`

|`Field != value`
|Ungleichheit
|`InvoiceFolder.Status != "Archiviert"`

|`Field < value`
|Kleiner als
|`InvoiceFolder.Year < 2024`

|`Field <= value`
|Kleiner oder gleich
|`InvoiceFolder.Year <= 2024`

|`Field > value`
|Größer als
|`InvoiceFolder.Year > 2020`

|`Field >= value`
|Größer oder gleich
|`InvoiceFolder.Year >= 2020`

|`Field.in_(v1, v2, ...)`
|Enthält einen der Werte
|`InvoiceFolder.Year.in_(2022, 2023, 2024)`

|`Field.not_in(v1, v2, ...)`
|Enthält keinen der Werte
|`InvoiceFolder.Status.not_in("Offen", "Entwurf")`

|`Field.between(lower, upper)`
|Zwischen zwei Werten (inklusiv)
|`InvoiceFolder.Year.between(2020, 2024)`
|===

=== AND-Verknüpfung

Mehrere Argumente in `.where()` werden mit AND verknüpft. Alternativ kann `&` verwendet werden:

[source,python]
----
# Variante 1: mehrere Argumente → AND
ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Year >= 2020,
    InvoiceFolder.Status == "Freigegeben",
)

# Variante 2: explizites & → gleiches Ergebnis
ecm.dms.select(InvoiceFolder).where(
    (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Status == "Freigegeben")
)
----

=== OR-Verknüpfung

Bedingungen werden mit `|` zu einer OR-Gruppe kombiniert:

[source,python]
----
ecm.dms.select(InvoiceFolder).where(
    (InvoiceFolder.Status == "Offen") | (InvoiceFolder.Status == "In Bearbeitung")
)
----

Für Wertemengen ist `.in_()` kürzer:

[source,python]
----
ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Status.in_("Offen", "In Bearbeitung")
)
----

=== Gemischte AND/OR-Gruppen

`&` und `|` können beliebig verschachtelt werden. Python-Klammern steuern die Auswertungsreihenfolge:

[tabs]
====
Sync::
+
[source,python]
----
# (Jahr >= 2020 AND Jahr <= 2024) AND (Status = "Offen" OR Status = "In Bearbeitung")
for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Status.in_("Offen", "In Bearbeitung"),
    )
    .stream()
):
    print(folder.Title, folder.Year)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Status.in_("Offen", "In Bearbeitung"),
    )
    .stream()
):
    print(folder.Title, folder.Year)
----
====

=== between()

`.between(lower, upper)` ist eine kompakte Alternative zu `>=` + `<=`:

[tabs]
====
Sync::
+
[source,python]
----
for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year.between(2020, 2024))
    .stream()
):
    print(folder.Title)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year.between(2020, 2024))
    .stream()
):
    print(folder.Title)
----
====

=== Kombinierte Abfragen über mehrere Objekttypen

Bedingungen in `.where()` können gleichzeitig Felder verschiedener Objekttypen desselben Schranks referenzieren. Der Server gibt nur diejenigen Objekte zurück, bei denen alle Bedingungen erfüllt sind — unabhängig davon, auf welchen Objekttyp sich eine Bedingung bezieht.

Folgende Kombinationen sind möglich:

* Suche nach einem *Dokument* mit Bedingungen auf Feldern des Dokuments selbst, seines Registers und seines Ordners.
* Suche nach einem *Register* oder *Ordner*, in dem sich ein Unterobjekt befindet, das einer bestimmten Bedingung entspricht.

[NOTE]
====
Bei verschachtelten Registern steht nur das *unmittelbar* übergeordnete Register zur Verfügung, nicht dessen Eltern-Register.
====

*Dokument suchen — mit Bedingungen auf Register und Ordner:*

[tabs]
====
Sync::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Rechnung",
        Unittest_DMS_Register.Name == "Eingangsrechnungen",
        Unittest_DMS.Name == "Lieferant GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
----

Async::
+
[source,python]
----
async for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Rechnung",
        Unittest_DMS_Register.Name == "Eingangsrechnungen",
        Unittest_DMS.Name == "Lieferant GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
----
====

*Ordner suchen — der ein Dokument mit bestimmten Eigenschaften enthält:*

[tabs]
====
Sync::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Lieferant GmbH",
        Unittest_DMS_Document.StringField == "Rechnung",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Lieferant GmbH",
        Unittest_DMS_Document.StringField == "Rechnung",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
----
====

== Sortierung

Die Sortierung wird über `.order_by()` festgelegt. Jedes Argument ist ein `ECMSortOrder`, erzeugt über `.ASC` oder `.DESC` auf einem `ECMField` auf Klassenebene. Die Reihenfolge der Argumente bestimmt die Sortierpriorität.

[tabs]
====
Sync::
+
[source,python]
----
# Primär nach Jahr absteigend, sekundär nach Titel aufsteigend
for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
    .stream()
):
    print(folder.Year, folder.Title)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
    .stream()
):
    print(folder.Year, folder.Title)
----
====

== Paginierung

Mit `.limit()`, `.pagesize()` und `.offset()` wird die Ergebnismenge eingeschränkt und die interne Seitennavigation gesteuert.

[cols="1,1,3"]
|===
|Methode |Standard |Beschreibung

|`.limit(n)`
|unbegrenzt
|Maximale Gesamtanzahl Treffer über alle Seiten.

|`.pagesize(n)`
|1000
|Anzahl Objekte pro Server-Anfrage. Kleinere Werte reduzieren den Speicherbedarf pro Seite, erhöhen aber die Anzahl der Anfragen. Größere Werte können den Server überlasten — siehe Hinweis unten.

|`.offset(n)`
|0
|Überspringt die ersten `n` Treffer. Nützlich für manuelle Seitennavigation.
|===

[tabs]
====
Sync::
+
[source,python]
----
# Seite 3 mit je 20 Einträgen (Offset = 2 × 20 = 40)
for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC)
    .limit(20)
    .offset(40)
    .stream()
):
    print(folder.system.id, folder.Title)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC)
    .limit(20)
    .offset(40)
    .stream()
):
    print(folder.system.id, folder.Title)
----
====

[WARNING]
====
Eine zu große `pagesize` kann den Server zum Absturz bringen. Der Speicherverbrauch pro Seite steigt proportional zur Anzahl der Objekte **multipliziert** mit den angeforderten Metadaten. Besonders kritisch ist die Kombination großer Seiten mit `.rights()`, `.base_params()`, `.file_properties()` oder `.variants()`, da der Server für jedes Objekt zusätzliche Datenbankabfragen durchführt.

Als Richtwert gilt: Den Standard von 1000 beibehalten und nur bei belegten Performanceproblemen anpassen — dann eher verkleinern.
====

== Rechteinformationen

Mit `.rights()` werden die Zugriffsrechte des angemeldeten Benutzers für jedes Objekt abgerufen. Die Rechte stehen danach in `obj.system.rights` als `ECMModelRights` bereit.

[cols="1,3"]
|===
|Attribut |Beschreibung

|`insert`
|Darf Kindobjekte anlegen (Register/Dokumente in einem Ordner, Dokumente in einem Register).

|`edit_metadata`
|Darf Indexfelder des Objekts ändern.

|`read_file`
|Darf die Datei des Dokuments lesen.

|`edit_file`
|Darf die Datei des Dokuments ändern.

|`delete`
|Darf das Objekt löschen.
|===

[tabs]
====
Sync::
+
[source,python]
----
for folder in ecm.dms.select(InvoiceFolder).rights().stream():
    r = folder.system.rights
    if r.edit_metadata:
        print(f"{folder.system.id}: Bearbeitung erlaubt")
    if not r.delete:
        print(f"{folder.system.id}: Löschen nicht erlaubt")
----

Async::
+
[source,python]
----
async for folder in ecm.dms.select(InvoiceFolder).rights().stream():
    r = folder.system.rights
    if r.edit_metadata:
        print(f"{folder.system.id}: Bearbeitung erlaubt")
    if not r.delete:
        print(f"{folder.system.id}: Löschen nicht erlaubt")
----
====

== Basisparameter

Mit `.base_params()` werden Verwaltungsinformationen des Servers für jedes Objekt abgerufen. Die Daten stehen in `obj.system.base_params` als `ECMModelBaseParams` bereit.

[cols="1,3"]
|===
|Attribut |Beschreibung

|`creator`
|Benutzername des Erstellers.

|`creation_date`
|Datum der Erstellung.

|`owner`
|Aktueller Eigentümer des Objekts.

|`modifier`
|Benutzername der letzten Änderung.

|`modified_date`
|Zeitstempel der letzten Änderung.

|`links_count`
|Anzahl der Verlinkungen.

|`text_notice_count`
|Anzahl der Textnotizen.
|===

[tabs]
====
Sync::
+
[source,python]
----
for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
    bp = folder.system.base_params
    print(f"Erstellt von {bp.creator} am {bp.creation_date}")
    print(f"Zuletzt geändert von {bp.modifier} am {bp.modified_date}")
----

Async::
+
[source,python]
----
async for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
    bp = folder.system.base_params
    print(f"Erstellt von {bp.creator} am {bp.creation_date}")
    print(f"Zuletzt geändert von {bp.modifier} am {bp.modified_date}")
----
====

== Dateiinformationen

Mit `.file_properties()` werden Metadaten zur Datei eines Dokuments abgerufen. Nur für `ECMDocumentModel`-Typen verfügbar; `obj.system.file_properties` liefert eine `ECMModelFileProperties`-Instanz.

[cols="1,3"]
|===
|Attribut |Beschreibung

|`count`
|Anzahl der Dateien (Haupt- und Nebendateien).

|`size`
|Dateigröße in Bytes.

|`extension`
|Dateiendung (z.B. `pdf`, `docx`).

|`mimetype`
|MIME-Typ (z.B. `application/pdf`).

|`mimetypegroup`
|MIME-Gruppe (z.B. `application`).

|`iconid`
|ID des Dateityp-Icons.

|`documentpagecount`
|Anzahl der Seiten (sofern bekannt).
|===

[tabs]
====
Sync::
+
[source,python]
----
for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
    fp = doc.system.file_properties
    print(f"{doc.system.id}: {fp.extension}, {fp.size} Bytes, {fp.documentpagecount} Seiten")
----

Async::
+
[source,python]
----
async for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
    fp = doc.system.file_properties
    print(f"{doc.system.id}: {fp.extension}, {fp.size} Bytes, {fp.documentpagecount} Seiten")
----
====

== Varianten

Mit `.variants()` werden die Versionsverzweigungen eines Dokuments aus dem W-Modul abgerufen. Nur für Dokumenttypen mit aktiviertem W-Modul relevant. Das Ergebnis steht in `obj.system.variants` als Liste von `ECMModelDocumentVariant`-Instanzen bereit.

Jede Variante hat die Attribute `doc_id`, `doc_ver`, `is_active`, `doc_parent` und `children`.

[tabs]
====
Sync::
+
[source,python]
----
for doc in ecm.dms.select(InvoiceDocument).variants().stream():
    for variant in doc.system.variants:
        active = "✓" if variant.is_active else " "
        print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")
----

Async::
+
[source,python]
----
async for doc in ecm.dms.select(InvoiceDocument).variants().stream():
    for variant in doc.system.variants:
        active = "✓" if variant.is_active else " "
        print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")
----
====

== Notizen

Mit `.remarks()` werden die Notizen eines Objekts mitgeliefert. Die Daten stehen in `obj.system.remarks` bereit.

[tabs]
====
Sync::
+
[source,python]
----
for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
    for remark in folder.system.remarks:
        print(remark)
----

Async::
+
[source,python]
----
async for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
    for remark in folder.system.remarks:
        print(remark)
----
====

== Felder einschränken (fields)

Mit `.fields()` werden nur die angegebenen Indexfelder vom Server geladen. Der Server setzt dabei intern `field_schema="MIN"` und überträgt ausschließlich die explizit aufgeführten Felder. Systemfelder (`system.id`, `system.name` etc.) und Sortierfelder werden immer mitgeliefert, unabhängig von dieser Liste.

Das reduziert die übertragene Datenmenge erheblich, wenn nur wenige Felder benötigt werden. Nicht angeforderte Felder sind am Objekt `None`.

Felder können als `ECMField`-Klassenattribut oder als interner Feldname (String) übergeben werden. Ein Aufruf ohne Argumente hebt die Einschränkung wieder auf.

[tabs]
====
Sync::
+
[source,python]
----
# Nur Title und Year laden — alle anderen Felder sind None
for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
    # folder.Status ist None (nicht geladen)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
----
====

Felder können alternativ als Strings übergeben werden, wenn kein typisiertes Model vorliegt:

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields("Title", "Year")
    .stream()
):
    print(folder["Title"], folder["Year"])
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields("Title", "Year")
    .stream()
):
    print(folder["Title"], folder["Year"])
----
====

== Papierkorb

`.garbage_mode()` schränkt die Abfrage auf gelöschte Objekte ein. Ohne diesen Aufruf werden nur nicht gelöschte Objekte zurückgegeben.

[tabs]
====
Sync::
+
[source,python]
----
# Alle gelöschten Rechnungsordner auflisten
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(f"Gelöscht: {folder.system.id} – {folder.Title}")
----

Async::
+
[source,python]
----
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(f"Gelöscht: {folder.system.id} – {folder.Title}")
----
====

== Untergeordnete Objekte (with_children)

`.with_children()` wechselt zu einer HOL-Abfrage, die zusammen mit jedem Hauptobjekt seine Kindobjekte liefert. Der Aufruf gibt einen `ECMModelQueryHolSync` zurück. Vor `execute()` oder `stream()` muss `.limit()` gesetzt sein, da HOL-Antworten keine Paginierung unterstützen.

Jedes Ergebnis ist ein `ECMHolResult` mit:

* `.main` — das Hauptobjekt (z.B. der Ordner)
* `.children_of(ECMChildSpec(ChildModel))` — Liste der Kindobjekte dieses Typs

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMChildSpec

results = (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year == 2024)
    .with_children(ECMChildSpec(InvoiceDocument))
    .limit(100)
    .execute()
)
for r in results:
    print(f"Ordner {r.main.Title}:")
    for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
        print(f"  Dokument {doc.system.id}")
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMChildSpec

results = await (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year == 2024)
    .with_children(ECMChildSpec(InvoiceDocument))
    .limit(100)
    .execute()
)
for r in results:
    print(f"Ordner {r.main.Title}:")
    for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
        print(f"  Dokument {doc.system.id}")
----
====

== Übergeordnete Objekte (with_parents)

`.with_parents()` kehrt die Antwortstruktur um: Der äußerste Elterntyp (erster Spec) wird zum Hauptobjekt `result.main`. Das ursprünglich abgefragte Objekt und alle Zwischenebenen sind über `.children_of()` abrufbar.

Die Specs werden von außen nach innen übergeben (z.B. zuerst `Ordner`, dann `Register`).

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec

results = (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Status == "Freigegeben")
    .with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
    .limit(50)
    .execute()
)
for r in results:
    folder   = r.main                                          # InvoiceFolder
    register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
    doc      = r.children_of(ECMChildSpec(InvoiceDocument))[0]
    print(f"{folder.Title} → {register.Title} → Dok {doc.system.id}")
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec

results = await (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Status == "Freigegeben")
    .with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
    .limit(50)
    .execute()
)
for r in results:
    folder   = r.main
    register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
    doc      = r.children_of(ECMChildSpec(InvoiceDocument))[0]
    print(f"{folder.Title} → {register.Title} → Dok {doc.system.id}")
----
====

== Beispiele

=== Einfache Abfrage mit Filter und Sortierung

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year:  ECMField[int]

for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year:  ECMField[int]

async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
----
====

=== Kombinierte Bedingungen (AND / OR)

[tabs]
====
Sync::
+
[source,python]
----
for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Title == "Rechnung",
    )
    .stream()
):
    print(folder.Title, folder.Year)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Title == "Rechnung",
    )
    .stream()
):
    print(folder.Title, folder.Year)
----
====

=== Limit, Offset und Seitengröße

[tabs]
====
Sync::
+
[source,python]
----
# Maximal 50 Treffer, ab Position 100, in Paketen von 25 pro Server-Anfrage
for folder in (
    ecm.dms.select(InvoiceFolder)
    .limit(50)
    .offset(100)
    .pagesize(25)
    .stream()
):
    print(folder.system.id)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .limit(50)
    .offset(100)
    .pagesize(25)
    .stream()
):
    print(folder.system.id)
----
====

=== Rechte und Audit-Metadaten

[tabs]
====
Sync::
+
[source,python]
----
for folder in (
    ecm.dms.select(InvoiceFolder)
    .rights()
    .base_params()
    .stream()
):
    print(folder.system.rights)
    print(folder.system.base_params)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .rights()
    .base_params()
    .stream()
):
    print(folder.system.rights)
    print(folder.system.base_params)
----
====

=== Selektive Felder

Mit `.fields()` werden nur die angegebenen Felder vom Server geladen. Das reduziert
die Datenmenge erheblich, wenn nur einzelne Felder benötigt werden.

[tabs]
====
Sync::
+
[source,python]
----
for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
    # folder.OtherField wäre None (nicht geladen)
----

Async::
+
[source,python]
----
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
----
====

=== Papierkorb abfragen

[tabs]
====
Sync::
+
[source,python]
----
# Nur gelöschte Objekte
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(folder.system.id, folder.Title)
----

Async::
+
[source,python]
----
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(folder.system.id, folder.Title)
----
====

=== Generisches Model (ohne Klassendefinition)

Wenn der Objekttyp erst zur Laufzeit bekannt ist, kann ein Model dynamisch erzeugt werden:

[tabs]
====
Sync::
+
[source,python]
----
from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder["Title"])
----

Async::
+
[source,python]
----
from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

async for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder["Title"])
----
====

=== Kombinierte Abfrage über Dokument, Register und Ordner

[tabs]
====
Sync::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Rechnung",
        Unittest_DMS_Register.Name == "Eingangsrechnungen",
        Unittest_DMS.Name == "Lieferant GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
----

Async::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

async for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Rechnung",
        Unittest_DMS_Register.Name == "Eingangsrechnungen",
        Unittest_DMS.Name == "Lieferant GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
----
====

=== Ordner suchen, der ein passendes Unterobjekt enthält

[tabs]
====
Sync::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Lieferant GmbH",
        Unittest_DMS_Document.StringField == "Rechnung",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
----

Async::
+
[source,python]
----
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

async for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Lieferant GmbH",
        Unittest_DMS_Document.StringField == "Rechnung",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
----
====
