select() und select_lol()
Beide Methoden geben einen verkettbaren Query-Builder für dieselben typisierten Modelle zurück, über zwei unterschiedliche Abfrageformate des Servers:
select()(HOL, Hierarchical Object List)-
Gibt einen
ECMModelQuerySync/ECMModelQueryAsynczurück. Der Server antwortet mit einem objektbasierten XML-Baum, der die Hierarchie und alle optionalen Zusatzdaten trägt: Dateiinformationen, Basisparameter, Varianten, Notizen, unter- und übergeordnete Objekte. select_lol()(LOL, Linear Object List)-
Gibt einen
ECMModelQueryLolSync/ECMModelQueryLolAsynczurück. Der Server antwortet mit einer kompakten, spaltenorientierten Trefferliste. Sie ist bei großen, flachen Ergebnismengen in der Regel schneller, trägt aber weder die Hierarchie noch diese Zusatzdaten.
Bedingungen, Sortierung, Paginierung, Feldbeschränkung, Volltextsuche, Rechte und der Transport der Trefferliste verhalten sich in beiden Modi gleich. Alles auf dieser Seite gilt daher für beide, sofern es nicht mit nur HOL oder nur LOL gekennzeichnet ist.
ECM-Modell-Referenz — vollständige Beschreibung aller system-Eigenschaften
(system.id, system.rights, system.base_params, system.file_properties usw.) und der
Änderungsverfolgung.
1. Modus wählen
| Fähigkeit | select() (HOL) |
select_lol() (LOL) |
|---|---|---|
Performance |
Langsamer |
Schneller bei großen, flachen Mengen |
Sortierung |
Verfügbar |
Verfügbar |
|
Verfügbar |
Nicht verfügbar |
|
Verfügbar (strukturiert) |
Nicht verfügbar (nur via Systemfelder) |
|
Verfügbar |
Nicht verfügbar |
|
Verfügbar |
Ignoriert (Server liefert keine Icons) |
|
Verfügbar |
Nicht verfügbar |
Tabellenfeld-Werte |
Typisiert, mit |
Untypisierte Rohstrings, |
|
Wenn Dateiinformationen, Basisparameter, Varianten oder hierarchische Abfragen benötigt werden, ist |
In der Praxis folgt der Modus daraus, was das Ergebnis tragen muss:
-
Sync
-
Async
# Empfohlen für: große Mengen, nur Indexdaten (ohne Varianten/Dateiinfos/Hierarchie)
for doc in (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.order_by(InvoiceDocument.Title.ASC)
.pagesize(500)
.stream()
):
print(doc.system.id, doc.Title)
# Empfohlen für: Dateiinfos, Basisparameter, Varianten, Hierarchien → select() verwenden
for doc in (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.file_properties()
.stream()
):
print(doc.system.file_properties.extension)
# Empfohlen für: große Mengen, nur Indexdaten (ohne Varianten/Dateiinfos/Hierarchie)
async for doc in (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.order_by(InvoiceDocument.Title.ASC)
.pagesize(500)
.stream()
):
print(doc.system.id, doc.Title)
# Empfohlen für: Dateiinfos, Basisparameter, Varianten, Hierarchien → select() verwenden
async for doc in (
ecm.dms.select(InvoiceDocument)
.where(InvoiceDocument.Year == 2024)
.file_properties()
.stream()
):
print(doc.system.file_properties.extension)
2. Signatur
Beide Builder werden synchron erzeugt, auch in der async-Variante. Das await gehört an den
abschließenden Aufruf (.execute()) oder an die Iteration (async for … .stream()).
-
Sync
-
Async
ecm.dms.select(model_class: type[T]) -> ECMModelQuerySync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolSync[T]
ecm.dms.select(model_class: type[T]) -> ECMModelQueryAsync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolAsync[T]
3. Parameter
| Name | Typ | Beschreibung |
|---|---|---|
|
|
Die Modelklasse, die den Objekttyp beschreibt. Kann auch über |
|
Wenn nur der interne Typname bekannt ist (und nicht vorab feststeht, ob es sich um Ordner, Register oder Dokument handelt), liefert |
4. Query-Builder Methoden
Die Builder-Methoden sind verkettbar. Die Spalte Modus sagt, wo eine Methode existiert:
| Methode | Modus | Beschreibung |
|---|---|---|
|
beide |
Filterbedingungen hinzufügen. Mehrere Argumente werden mit AND verknüpft. Bedingungen lassen sich mit |
|
beide |
Sortierung festlegen. Die Argumentposition bestimmt die Sortierpriorität. Jedes Argument ist ein |
|
beide |
Maximale Gesamtzahl der Treffer über alle Seiten (LOL: wird auf |
|
beide |
Anzahl Objekte pro Serveranfrage (Standard: 1000). Beeinflusst die Effizienz bei großen Ergebnismengen. |
|
beide |
Nullbasierte Startposition. Überspringt die ersten |
|
beide |
Nur die angegebenen Felder zurückgeben (setzt |
|
beide |
Fügt eine |
|
beide |
Nur Objekte aus dem Papierkorb zurückgeben. |
|
beide |
Rechteinformationen mitladen (füllt |
|
beide |
Trefferliste als Antwortdatei statt im |
|
beide |
Abfrage ausführen und alle Treffer als Liste zurückgeben (alle Seiten im Speicher). |
|
beide |
Abfrage seitenweise ausführen und einen Generator zurückgeben. Empfohlen für große Ergebnismengen. |
|
nur HOL |
Audit-Metadaten mitladen (füllt |
|
nur HOL |
Dateiinformationen mitladen (füllt |
|
nur HOL |
Varianten-Daten von Dokumenten mitladen. |
|
nur HOL |
Notizen der Objekte mitladen. |
|
nur HOL |
Icon-IDs der Objekte mitladen. Im LOL-Modus wird der Aufruf ignoriert, weil der Server dort keine Icons zurückgibt. |
|
nur HOL |
Wechselt zu einer HOL-Abfrage mit unter- bzw. übergeordneten Objekten. Gibt einen |
|
|
5. 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 &.
|
Jeder Vergleich braucht eigene Klammern
Die Schlüsselwörter
Dasselbe gilt für |
|
Der Bedingungswert muss zum deklarierten Feldtyp passen
Bei einem generierten Modell sind die Vergleichsoperatoren gegen den Feldtyp typisiert: ein
Zwei Kalibrierungen halten funktionierenden Code gültig: ein Dynamische Modelle aus |
5.1. Vergleichsoperatoren
| Syntax | Operator | Beispiel |
|---|---|---|
|
Gleichheit |
|
|
Ungleichheit |
|
|
Kleiner als |
|
|
Kleiner oder gleich |
|
|
Größer als |
|
|
Größer oder gleich |
|
|
Enthält einen der Werte |
|
|
Enthält keinen der Werte |
|
|
Zwischen zwei Werten (inklusiv) |
|
5.2. AND-Verknüpfung
Mehrere Argumente in .where() werden mit AND verknüpft. Alternativ kann & verwendet werden:
# 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")
)
5.3. OR-Verknüpfung
Bedingungen werden mit | zu einer OR-Gruppe kombiniert:
ecm.dms.select(InvoiceFolder).where(
(InvoiceFolder.Status == "Offen") | (InvoiceFolder.Status == "In Bearbeitung")
)
Für Wertemengen ist .in_() kürzer:
ecm.dms.select(InvoiceFolder).where(
InvoiceFolder.Status.in_("Offen", "In Bearbeitung")
)
5.4. Gemischte AND/OR-Gruppen
& und | können beliebig verschachtelt werden. Python-Klammern steuern die Auswertungsreihenfolge:
-
Sync
-
Async
# (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 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)
5.5. between()
.between(lower, upper) ist eine kompakte Alternative zu >= + ⇐:
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year.between(2020, 2024))
.stream()
):
print(folder.Title)
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(InvoiceFolder.Year.between(2020, 2024))
.stream()
):
print(folder.Title)
5.6. Tabellenfeld-Spaltenbedingungen
Die Bedingungen funktionieren in beiden Modi. Unterschiedlich ist das Ergebnis: LOL liefert Tabellenfeldwerte als untypisierte Rohstrings und row.row_id ist immer None, HOL liefert sie typisiert. Tabellenfeld-Werte werden als untypisierte Rohstrings geliefert und nicht in den deklarierten Python-Typ konvertiert. row_id ist für alle Tabellenzeilen aus LOL-Abfragen immer None.
|
Bedingungen auf eine Tabellenfeld-Spalte (Spalte einer Untertabelle / eines Mehrfachfeldes) werden
über den Zugriff auf die Spalte am ECMTableField des Models geschrieben (z. B. Invoice.Positions).
Der Builder erzeugt dann eine <TableCondition> / <TableColumn> statt einer flachen Feldbedingung,
sodass der Server auf die Tabellenspalte filtert, anstatt ein unbekanntes Objektfeld abzulehnen.
# beliebige Zeile, deren ArticleNo == "A-100"
ecm.dms.select(Invoice).where(Invoice.Positions.ArticleNo == "A-100").execute()
ergibt:
<ConditionObject internal_name="Invoice">
<TableCondition internal_name="Positions">
<TableColumn internal_name="ArticleNo" operator="=">
<Value>A-100</Value>
</TableColumn>
</TableCondition>
</ConditionObject>
Alle Vergleichs- und Mengenoperatoren funktionieren (==, !=, <, ⇐, >, >=, .in_(),
.not_in(), .between()), und eine Tabellenspalten-Bedingung lässt sich im selben .where()-Aufruf
mit gewöhnlichen Feldbedingungen kombinieren.
Um die Bedingung auf eine einzelne Zeile zu beschränken, wird das Tabellenfeld mit der
Server-Zeilennummer indiziert — Invoice.Positions[3] (der explizite Alias Invoice.Positions.row(3)
ist gleichwertig):
# Quantity in Zeile 3 == 5
ecm.dms.select(Invoice).where(Invoice.Positions[3].Quantity == 5).execute()
<TableCondition internal_name="Positions" row="3">
<TableColumn internal_name="Quantity" operator="="><Value>5</Value></TableColumn>
</TableCondition>
|
Die Indizierung ist Klassenebenen-Abfragesyntax. Auf Instanzebene ist |
5.7. Kombinierte Abfragen über mehrere Objekttypen
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
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.
|
Bei verschachtelten Registern steht nur das unmittelbar übergeordnete Register zur Verfügung, nicht dessen Eltern-Register. |
Dokument suchen — mit Bedingungen auf Register und Ordner:
-
Sync
-
Async
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 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:
-
Sync
-
Async
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 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)
5.8. Systemfelder als Bedingung
Neben den Indexfeldern lassen sich auch Systemfelder in .where() (und .order_by()) verwenden. Das system-Attribut ist dafür zugriffsabhängig:
-
Instanz (
obj.system.id) liefert den geladenen Wert. -
Klasse (
Modell.system.id) liefert einECMField, das wie ein Indexfeld in Bedingungen einsetzbar ist.
Damit funktioniert derselbe punktierte Pfad in einer Abfrage. Häufigster Fall — alle Dokumente in einem bekannten Ordner laden, über eine schrankübergreifende Bedingung auf die Ordner-ID (Kombination mit dem vorigen Abschnitt):
-
Sync
-
Async
docs = (
ecm.dms.select(Unittest_DMS_Document)
.where(Unittest_DMS.system.id == folder.system.id) # links Klasse (ECMField), rechts Instanzwert (int)
.execute()
)
docs = await (
ecm.dms.select(Unittest_DMS_Document)
.where(Unittest_DMS.system.id == folder.system.id)
.execute()
)
Die Bedingung wird an den Objekttyp gebunden, auf dem sie aufgerufen wird (Unittest_DMS), und wie eine Cross-Type-Bedingung in ein eigenes ConditionObject einsortiert. Systemfelder erhalten automatisch das system="1"-Attribut — identisch zu Unittest_DMS["OBJECT_ID"]:
<ConditionObject internal_name="Unittest_DMS">
<FieldCondition internal_name="OBJECT_ID" operator="=" system="1">
<Value>4711</Value>
</FieldCondition>
</ConditionObject>
Abfragbar sind nur Systemfelder mit einer einzelnen Backing-Spalte. Verfügbar auf allen Objekttypen: id, owner_guid, creator, creation_date, last_modifier, last_modified, creation_time, deleted_at sowie die Teilmenge system.base_params.{creator, creation_date, modifier, modified_date, links_count, locked_user_id}. Typabhängig: system.folder_id, system.parent_register_id (Register); system.folder_id, system.register_id, system.register_type_id, system.system_id, system.foreign_id, system.lock_user_id, system.archivist, system.archiving_date und system.file_properties.{count, size} (Dokument).
Generische Alternative — beliebiges Systemfeld per Item-Zugriff: Modell["NAME"] (Subscript auf der Modellklasse) liefert ein ECMField für jeden Feldnamen. Ist NAME ein SystemFields-Member, wird automatisch system="1" gesetzt. Das deckt auch Systemfelder ohne .system-Eigenschaft ab (OBJECT_FLAGS, OBJECT_RETENTION, …) und funktioniert bei dynamischen Modellen ohne deklarierte Felder:
ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_ID"] == folder.system.id) # = Unittest_DMS.system.id
ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_FLAGS"] == 0)
Der Item-Zugriff ist untypisiert (ECMField[str]); Modell.system.<feld> ist typisiert und entdeckbar, aber auf die kuratierte Teilmenge beschränkt. Beide erzeugen dieselbe Bedingung.
Auch als Sortierfeld: Dieselben Systemfelder funktionieren in .order_by() (HOL und LOL). Im Ergebnisfeld-Element wird system="1" automatisch gesetzt:
ecm.dms.select(Unittest_DMS_Document).order_by(Unittest_DMS_Document.system.id.DESC)
<Field internal_name="OBJECT_ID" system="1" sortpos="1" sortorder="DESC"/>
|
Aggregierte Werte ohne abfragbare Serverspalte ( |
6. Volltextsuche
Die hier gezeigte Variante pro Objekttyp funktioniert in beiden Modi. Die archivweite Variante (<FulltextQuery>, Treffer über mehrere Objekttypen) ist nur HOL.
|
.fulltext(term) ergänzt eine <Fulltext>-Bedingung am ConditionObject des abgefragten Objekttyps. Sie ist eine zusätzliche Restriktion und kann mit .where(…)-Bedingungen kombiniert werden. Voraussetzung: am enaio®-Server ist eine Volltext-Engine konfiguriert und der Objekttyp ist indexiert.
|
Wird kein Suchtext geliefert oder ist keine Suchmaschine konfiguriert, antwortet der Server mit einer Fehlermeldung im |
-
Sync
-
Async
for doc in (
ecm.dms.select(MedicalLetter)
.fulltext("Meningitis")
.where(MedicalLetter.SeniorPhysician == "Müller")
.stream()
):
print(doc.system.id, doc.Type)
async for doc in (
ecm.dms.select(MedicalLetter)
.fulltext("Meningitis")
.where(MedicalLetter.SeniorPhysician == "Müller")
.stream()
):
print(doc.system.id, doc.Type)
6.1. RetrievalWare-Engine-Parameter
Optional können RetrievalWare-Engine-Attribute als Keyword-Argumente gesetzt werden. Sie wirken nur, wenn am Server RetrievalWare als Volltext-Engine im Einsatz ist. Nicht gesetzte Parameter verwenden die Server-Defaults.
| Parameter | Server-Default | Beschreibung |
|---|---|---|
|
|
Suchmodus. Erlaubte Werte: |
|
|
Wort-Expansionslevel für Thesaurus-Lookups. |
|
|
Fuzzy-Spelling für Halbwörter aktivieren. |
|
|
Ähnlichkeitsschwelle für Fuzzy-Spelling. |
|
|
Maximale Anzahl Fuzzy-Spelling-Treffer. |
|
|
Maximale Anzahl Expansionen regulärer Ausdrücke. |
|
|
Warnung ausgeben, wenn das Expansionslimit erreicht ist. |
|
|
Maximale Anzahl Wort-Expansionen. |
ecm.dms.select(MedicalLetter).fulltext(
"Meningitis OR Encephalitis",
mode="BOOLEAN",
expansion_level=2,
).execute()
7. 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.
-
Sync
-
Async
# 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 for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
.stream()
):
print(folder.Year, folder.Title)
7.1. Keyset-Pagination über Systemfelder
.fields() und .order_by() akzeptieren auch Systemfelder über den Klassen-Zugriff
(Modell.system.<feld>, siehe Modell-Referenz).
Systemfelder erhalten im erzeugten <Field>-Element automatisch das system="1"-Attribut;
ein Feld, das gleichzeitig selektiert und sortiert wird, erzeugt nur einen <Field>-Eintrag.
Damit lässt sich z. B. eine stabile Keyset-Pagination über die Objekt-ID bauen:
-
Sync
-
Async
cursor: int | None = None
batch = 200
while True:
query = (
ecm.dms.select_lol(InvoiceDocument)
.fields(InvoiceDocument.system.id)
.order_by(InvoiceDocument.system.id.DESC)
.limit(batch)
.pagesize(batch)
)
if cursor is not None:
query = query.where(InvoiceDocument.system.id < cursor)
docs = query.execute()
if not docs:
break
for doc in docs:
... # Batch verarbeiten
cursor = docs[-1].system.id
cursor: int | None = None
batch = 200
while True:
query = (
ecm.dms.select_lol(InvoiceDocument)
.fields(InvoiceDocument.system.id)
.order_by(InvoiceDocument.system.id.DESC)
.limit(batch)
.pagesize(batch)
)
if cursor is not None:
query = query.where(InvoiceDocument.system.id < cursor)
docs = await query.execute()
if not docs:
break
for doc in docs:
... # Batch verarbeiten
cursor = docs[-1].system.id
8. Paginierung
Mit .limit(), .pagesize() und .offset() wird die Ergebnismenge eingeschränkt und die interne Seitennavigation gesteuert.
| Methode | Standard | Beschreibung |
|---|---|---|
|
unbegrenzt |
Maximale Gesamtanzahl Treffer über alle Seiten. |
|
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. |
|
0 |
Überspringt die ersten |
-
Sync
-
Async
# 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 for folder in (
ecm.dms.select(InvoiceFolder)
.order_by(InvoiceFolder.Year.DESC)
.limit(20)
.offset(40)
.stream()
):
print(folder.system.id, folder.Title)
|
Eine zu große Als Richtwert gilt: Den Standard von 1000 beibehalten und nur bei belegten Performanceproblemen anpassen — dann eher verkleinern. |
9. Felder einschränken (fields)
| Die Einschränkung betrifft die Indexfelder. Die Standard-Systemfelder, darunter die Ablagefelder des Objekts, bleiben in beiden Modi in der Anfrage, siehe Ordner und Register eines Treffers. |
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.
-
Sync
-
Async
# 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 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:
-
Sync
-
Async
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 for folder in (
ecm.dms.select(InvoiceFolder)
.fields("Title", "Year")
.stream()
):
print(folder["Title"], folder["Year"])
10. Transport der Trefferliste
dms.GetResultList liefert die Trefferliste auf zwei Wegen: inline als BASE64-Parameter
XML (Flags=0) oder als Antwortdatei (Flags=16). Der Client fragt standardmässig den
Parameter an, weil das eine Netzwerk-Rundreise pro Seite spart: der Server schreibt
Parameterblock und Ergebnis in einem Durchgang, während er bei der Datei zweimal schreibt
und der Client auf den 32-Byte-Dateiheader warten muss.
Gemessen gegen einen Testserver mit 20 ms RTT:
| Szenario | Antwortdatei | Parameter | Differenz |
|---|---|---|---|
Erste Seite, |
94,5 ms |
53,3 ms |
-44 % |
1471 Zeilen, |
13 020 ms |
8 238 ms |
-37 % |
1471 Zeilen, |
339 ms |
278 ms |
-18 % |
Die Seitengrösse wirkt dabei deutlich stärker als der Transport: dieselben 1471 Zeilen
kosten mit pagesize(10) 146 Server-Anfragen, mit pagesize(1000) nur zwei. Der Standard
von 1000 ist bewusst hoch gewählt; ihn zu senken ist der teuerste Fehler in einer
Listenabfrage.
.result_as_file() bleibt für den Fall, dass eine einzelne Seite grösser wird als
file_cache_byte_limit des Pool-Clients (Standard 32 MiB). Die RPC-Schicht legt die Antwort
dann als temporäre Datei ab, und .stream() bzw. .execute() lesen sie inkrementell von
dort, sodass die Trefferliste nie vollständig in den Speicher muss. Unterhalb dieser Grenze
wird die Datei ohnehin im Speicher gepuffert und die Option kostet nur die zusätzliche
Rundreise.
# Standard: inline, eine Rundreise pro Seite
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(1000).stream():
...
# Sehr grosse Einzelseite: über eine temporäre Datei, konstanter Speicherbedarf
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(200_000).result_as_file().stream():
...
|
Nicht verfügbar ist beim inkrementellen Lesen alles, was im Dokument hinter den Treffern
steht, also |
11. 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.
.rights() fordert zusätzlich die Einfüge-Kontingente (object_inserts) an, weil der Server das Einfügerecht sonst für jedes Objekt als nicht gewährt meldet. Das kostet weitere Serverabfragen pro Objekt und ist bei grossen Seiten mit einzuplanen.
| Attribut | Beschreibung |
|---|---|
|
Darf Kindobjekte anlegen (Register/Dokumente in einem Ordner, Dokumente in einem Register). |
|
Darf Indexfelder des Objekts ändern. |
|
Darf die Datei des Dokuments lesen. |
|
Darf die Datei des Dokuments ändern. |
|
Darf das Objekt löschen. |
-
Sync
-
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")
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")
12. Basisparameter
Im LOL-Modus sind dieselben Werte über Systemfelder erreichbar:
.base_params() ist nicht verfügbar — obj.system.base_params ist immer None. Audit-Metadaten sind nur zugänglich, wenn die entsprechenden Systemfelder über ecm_system_fields am Modell deklariert sind:
| Systemfeld | Bedeutung |
|---|---|
|
Ersteller-ID |
|
Erstellungsdatum |
|
GUID des Eigentümers |
|
Letzter Bearbeiter |
|
Datum der letzten Änderung |
|
Anzahl Verlinkungen |
|
Anzahl Textnotizen |
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
Mit .base_params() werden Verwaltungsinformationen des Servers für jedes Objekt abgerufen. Die Daten stehen in obj.system.base_params als ECMModelBaseParams bereit.
| Attribut | Beschreibung |
|---|---|
|
Benutzername des Erstellers. |
|
Datum der Erstellung. |
|
Aktueller Eigentümer des Objekts. |
|
Benutzername der letzten Änderung. |
|
Zeitstempel der letzten Änderung. |
|
Anzahl der Verlinkungen. |
|
Anzahl der Textnotizen. |
-
Sync
-
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}")
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}")
13. Dateiinformationen
Im LOL-Modus beschränken sich Dateiinformationen auf das, was die Systemfelder tragen:
.file_properties() steht nicht zur Verfügung. Teilweise Datei-Metadaten sind nur über Systemfelder zugänglich, die explizit im Modell deklariert werden müssen:
| Systemfeld | Bedeutung |
|---|---|
|
Dateigröße in Bytes |
|
Anzahl der Dateien |
|
Anzahl der Dokumentseiten |
Dateiendung, MIME-Typ und MIME-Gruppe werden im LOL-Modus vom Server grundsätzlich nicht zurückgegeben.
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
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.
| Attribut | Beschreibung |
|---|---|
|
Anzahl der Dateien (Haupt- und Nebendateien). |
|
Dateigröße in Bytes. |
|
Dateiendung (z.B. |
|
MIME-Typ (z.B. |
|
MIME-Gruppe (z.B. |
|
ID des Dateityp-Icons. |
|
Anzahl der Seiten (sofern bekannt). |
-
Sync
-
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")
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")
14. Varianten
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
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, children und level (Verschachtelungstiefe, 0 für die Einträge in obj.system.variants).
-
Sync
-
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})")
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})")
15. Notizen
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
Mit .remarks() werden die Notizen eines Objekts mitgeliefert. Die Daten stehen in obj.system.remarks bereit.
-
Sync
-
Async
for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
for remark in folder.system.remarks:
print(remark)
async for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
for remark in folder.system.remarks:
print(remark)
16. Papierkorb
.garbage_mode() schränkt die Abfrage auf gelöschte Objekte ein. Ohne diesen Aufruf werden nur nicht gelöschte Objekte zurückgegeben.
-
Sync
-
Async
# 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 for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(f"Gelöscht: {folder.system.id} – {folder.Title}")
17. Ordner und Register eines Treffers
Die Ablage-Systemfelder eines Dokuments (SDSTA_ID, SDREG_ID, SDREG_TYPE) gehören in beiden Modi
zum Standardfeldsatz, brauchen also keinen .fields()-Eintrag und sind über den system-Namespace
jedes Treffers lesbar:
-
Sync
-
Async
for document in ecm.dms.select_lol(InvoiceDocument).stream():
print(document.system.id, document.system.folder_id, document.system.register_id)
async for document in ecm.dms.select_lol(InvoiceDocument).stream():
print(document.system.id, document.system.folder_id, document.system.register_id)
Das gilt auch, wenn .fields() das Ergebnis einschränkt: die Einschränkung betrifft die Indexfelder,
der Systemblock bleibt unverändert. Bei einem Register-Modell sind die entsprechenden Felder
REG_STAID (system.folder_id) und REG_PARID (system.parent_register_id).
Dieselben Felder funktionieren als Bedingung und als Sortierkriterium, etwa für alle Dokumente in einem bekannten Ordner:
-
Sync
-
Async
documents = (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.system.folder_id == folder.system.id)
.order_by(InvoiceDocument.system.id.ASC)
.execute()
)
documents = await (
ecm.dms.select_lol(InvoiceDocument)
.where(InvoiceDocument.system.folder_id == folder.system.id)
.order_by(InvoiceDocument.system.id.ASC)
.execute()
)
Zu beachten ist die Asymmetrie: links vom Vergleich steht der Klassenzugriff, der ein ECMField
liefert, rechts ein Instanzwert. Ein Systemfeld ohne .system-Eigenschaft ist über den
Item-Zugriff erreichbar, InvoiceDocument["SDSTA_ID"].
18. Untergeordnete Objekte (with_children)
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
.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
-
Sync
-
Async
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}")
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}")
19. Übergeordnete Objekte (with_parents)
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
.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).
-
Sync
-
Async
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}")
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}")
20. Beispiele
20.1. Einfache Abfrage mit Filter und Sortierung
-
Sync
-
Async
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)
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)
20.2. Kombinierte Bedingungen (AND / OR)
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Title == "Rechnung",
)
.stream()
):
print(folder.Title, folder.Year)
async for folder in (
ecm.dms.select(InvoiceFolder)
.where(
(InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
InvoiceFolder.Title == "Rechnung",
)
.stream()
):
print(folder.Title, folder.Year)
20.3. Limit, Offset und Seitengröße
-
Sync
-
Async
# 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 for folder in (
ecm.dms.select(InvoiceFolder)
.limit(50)
.offset(100)
.pagesize(25)
.stream()
):
print(folder.system.id)
20.4. Rechte und Audit-Metadaten
-
Sync
-
Async
for folder in (
ecm.dms.select(InvoiceFolder)
.rights()
.base_params()
.stream()
):
print(folder.system.rights)
print(folder.system.base_params)
async for folder in (
ecm.dms.select(InvoiceFolder)
.rights()
.base_params()
.stream()
):
print(folder.system.rights)
print(folder.system.base_params)
20.5. Selektive Felder
Mit .fields() werden nur die angegebenen Felder vom Server geladen. Das reduziert
die Datenmenge erheblich, wenn nur einzelne Felder benötigt werden.
-
Sync
-
Async
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 for folder in (
ecm.dms.select(InvoiceFolder)
.fields(InvoiceFolder.Title, InvoiceFolder.Year)
.stream()
):
print(folder.Title, folder.Year)
20.6. Papierkorb abfragen
-
Sync
-
Async
# Nur gelöschte Objekte
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(folder.system.id, folder.Title)
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
print(folder.system.id, folder.Title)
20.7. Generisches Model (ohne Klassendefinition)
Wenn der Objekttyp erst zur Laufzeit bekannt ist, kann ein Model dynamisch erzeugt werden:
-
Sync
-
Async
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"])
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"])
20.8. Kombinierte Abfrage über Dokument, Register und Ordner
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
-
Sync
-
Async
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)
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)
20.9. Ordner suchen, der ein passendes Unterobjekt enthält
Nur HOL. select_lol() bietet kein Äquivalent, siehe Modus wählen.
|
-
Sync
-
Async
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)
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)
20.10. Eine flache LOL-Abfrage
-
Sync
-
Async
for folder in (
ecm.dms.select_lol(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)
async for folder in (
ecm.dms.select_lol(InvoiceFolder)
.where(InvoiceFolder.Year >= 2020)
.order_by(InvoiceFolder.Year.DESC)
.stream()
):
print(folder.system.id, folder.Title)