update()

Aktualisiert ein bestehendes Objekt auf dem Server. Intern wird dms.XMLUpdate verwendet. Nach einem erfolgreichen Update übernimmt die übergebene Modell-Instanz den geschriebenen Stand, siehe Nach dem Update.

ECM-Modell-Referenz — vollständige Beschreibung von system.is_modified, system.modified_fields, system.modified_table_fields, system.has_removed_table_rows, system.requires_table_field_replacement und der Änderungsverfolgung.

Wurden keine Felder geändert und keine Dateien übergeben, wird der Server-Aufruf standardmäßig übersprungen. Mit force=True kann der Aufruf erzwungen werden.

Wird die aktualisierte Instanz direkt nach dem Speichern benötigt, ist update_and_get() die kompaktere Alternative.

1. Signatur

  • Sync

  • Async

ecm.dms.update(
    model: T,
    files: Sequence[JobRequestFile] | None = None,
    replace_files: bool = True,
    force: bool = False,
    *,
    check_mandatory: bool = True,
    replace_table_fields: bool | None = None,
    options: ECMUpdateOptions | None = None,
) -> None
await ecm.dms.update(
    model: T,
    files: Sequence[JobRequestFile] | None = None,
    replace_files: bool = True,
    force: bool = False,
    *,
    check_mandatory: bool = True,
    replace_table_fields: bool | None = None,
    options: ECMUpdateOptions | None = None,
) -> None

2. Parameter

Name Typ Beschreibung

model

ECMFolderModel | ECMRegisterModel | ECMDocumentModel

Die zu aktualisierende Modell-Instanz. Muss vom Server geladen worden sein, damit model.system.id gesetzt ist.

files

Sequence[JobRequestFile] | None

Optionale Dateiliste für das Dokument. Nur für ECMDocumentModel erlaubt — bei Ordner- oder Register-Modellen wird TypeError geworfen. Standard: None.

replace_files

bool

Wenn True (Standard) und files übergeben wurden, ersetzen die neuen Dateien die vorhandenen Dokumentdateien (REPLACEFILES=1). Wenn False, werden die Dateien den bestehenden hinzugefügt. Nur wirksam für ECMDocumentModel mit files.

force

bool

Wenn True, wird der Server-Aufruf immer ausgeführt, auch wenn keine Änderungen erkannt wurden. Standard: False.

check_mandatory

bool

Wenn True (Standard), wird clientseitig geprüft, ob alle als mandatory=True deklarierten ECMField-Felder einen Wert haben. Bei False wird die client- und serverseitige Pflichtfeldprüfung deaktiviert (CHECKOBLIGATION=0). Ein Feld, das die Abfrage nicht geladen hat, wird nicht geprüft, siehe Teilweise geladene Modelle.

replace_table_fields

bool | None

Überschreibt die automatische Steuerung der Option REPLACETABLEFIELDS=1 für Tabellenfelder. None (Standard) behält die Automatik bei (siehe unten): die Option wird nur gesetzt, wenn die Änderungsverfolgung erkennt, dass geladene Zeilen verändert, entfernt oder ersetzt wurden (model.system.requires_table_field_replacement). True erzwingt die Option auch ohne erkannte Änderung; False unterdrückt sie auch dann, wenn eine Änderung erkannt wurde. Für ein nicht geladenes Tabellenfeld, das zugewiesen oder geleert wurde, ist die Angabe Pflicht, siehe Teilweise geladene Modelle.

options

ECMUpdateOptions | None

Zusätzliche dms.XMLUpdate-Joboptionen, z. B. ECMUpdateOptions().check_read_only(False). Hier gesetzte Optionen haben Vorrang vor denen, die aus den getypten Parametern oben abgeleitet werden. Siehe Joboptionen. Standard: None.

replace_table_fields=False hebelt die Schutzautomatik aus. Wurden geladene Zeilen verändert oder entfernt und die Option unterdrückt, fällt der Server auf sein Anhänge-Verhalten zurück und die Änderung wird möglicherweise nicht wie beabsichtigt persistiert. False nur verwenden, wenn die Konsequenzen bekannt sind.

3. Rückgabewert

None

4. Automatisches Überspringen

Wenn alle der folgenden Bedingungen zutreffen, wird der Server-Aufruf ohne Fehler übersprungen:

  • force=False (Standard)

  • model.system.is_modified ist False — keine Felder wurden geändert

  • files ist None

Das Änderungsprotokoll (is_modified, modified_fields, modified_table_fields) wird vom Modell automatisch beim Setzen von Feldwerten geführt. Nur diese tatsächlich geänderten Felder werden serialisiert — unveränderte Felder bleiben dem Server fern und werden nicht überschrieben.

Tabellenfelder werden differenziert behandelt:

  • Wurde eine geladene Zeile verändert, entfernt oder ersetzt (d. h. system.requires_table_field_replacement ist True), wird das Tabellenfeld komplett neu übertragen und der Server-Aufruf erhält automatisch REPLACETABLEFIELDS=1, sodass der Server die gesamte Tabelle ersetzt.

  • Wurden nur neue Zeilen angefügt, werden ausschließlich diese neuen Zeilen übertragen — der Server-Default „APPEND" verhindert Duplikate der geladenen Zeilen.

  • Als neu gilt jede Zeile, die nicht dieses Modell in dieses Tabellenfeld geladen hat, auch eine aus einem anderen Objekt oder Tabellenfeld übernommene; sie wird gesendet (#15523).

  • Tabellenfelder ohne Änderung werden gar nicht in der XML aufgenommen und bleiben serverseitig unangetastet.

  • Ein vom Aufrufer geleertes Tabellenfeld wird als leeres <TableField internal_name="…"/> zusammen mit REPLACETABLEFIELDS=1 übertragen. Dieses leere Element ist der Ersatzinhalt, den der Server benötigt, um die verbliebenen Zeilen zu löschen; siehe Tabellenfeld leeren.

Diese Automatik lässt sich über den Parameter replace_table_fields überschreiben: True erzwingt REPLACETABLEFIELDS=1 (auch bei reinem Anhängen), False unterdrückt es (auch bei erkannter Änderung). None (Standard) belässt die oben beschriebene automatische Erkennung. Wirkt REPLACETABLEFIELDS=1, ob über den Parameter, über options oder automatisch, geht jedes geänderte Tabellenfeld mit allen Zeilen hinaus, auch eines, an das nur angehängt wurde; sonst ersetzten die neuen Zeilen die geladenen (#15523).

5. Nach dem Update

Nach einem erfolgreichen Update übernimmt das Modell den geschriebenen Stand als neuen geladenen Stand (#15523). Ein zweites update() ohne neue Änderung wird deshalb übersprungen, statt dieselben Änderungen noch einmal zu senden; angehängte Zeilen landeten so früher doppelt. Was sich während des laufenden Requests am Modell ändert, bleibt ausstehend.

Für Tabellenfelder hängt der übernommene Stand davon ab, ob die Zeilen auf dem Server danach bekannt sind:

  • Bekannt (geladene Tabelle mit angehängten Zeilen, oder REPLACETABLEFIELDS=1): das Tabellenfeld hält Kopien genau der geschriebenen Zeilen. Zeilenobjekte des Aufrufers bleiben unberührt, auch wenn sie in anderen Modellen stecken; eine vorher gehaltene Referenz auf eine Zeile gehört danach nicht mehr zum Modell. Zeilen, die der Request nicht trug (ohne jeden Wert, oder kein Zeilenobjekt), fallen weg.

  • Unbekannt: das Tabellenfeld ist danach leer und gilt als nicht geladen. Das betrifft Zeilen, die an ein nicht geladenes Tabellenfeld oder an ein von Hand gebautes Modell mit gesetzter ID angehängt wurden, und replace_table_fields=False nach einer Änderung, die eine Ersetzung gebraucht hätte.

Der concurrency_timestamp bleibt der geladene, denn dms.XMLUpdate liefert keine neue Änderungszeit zurück. Ein weiteres Update mit neuen Änderungen scheitert deshalb in der Regel mit -1048 (ECMWrongStateException): der Server sieht das Objekt seit dem Laden geändert. Zum Weiterarbeiten die Instanz aus update_and_get() verwenden oder neu laden.

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Neu"
folder = ecm.dms.update_and_get(folder)   # frische Instanz mit aktuellem Zeitstempel
folder.Year = 2026
ecm.dms.update(folder)

6. Tabellenfeld leeren

Werden alle Zeilen eines geladenen Tabellenfelds entfernt, leert der Client es auf dem Server:

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Positions.clear()     # oder: del folder.Positions[:]
ecm.dms.update(folder)       # sendet ein leeres <TableField/> plus REPLACETABLEFIELDS=1

Der Server entfernt Zeilen nur, wenn er Ersatzinhalt bekommt; das leere Element und die Option gehören also zusammen. Wird die Option mit replace_table_fields=False unterdrückt, bleiben die vorhandenen Zeilen deshalb erhalten — der Server arbeitet dann im APPEND-Modus, in dem ein leeres Tabellenfeld nichts aussagt.

Ein Tabellenfeld, das beim Laden bereits leer war, gilt nicht als Änderung und bleibt aus der XML heraus. Deklarierte, aber ungenutzte Tabellenfelder werden also nie durch ein unbeteiligtes Update geleert.

7. Feld leeren

Wird ein geladenes Feld auf None gesetzt, leert der Client es auf dem Server. Das Feld wird mit field_function="NULL" übertragen, sodass der gespeicherte Wert entfernt und nicht unverändert gelassen wird:

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Year = None       # zuvor gesetztes Feld zurücksetzen
ecm.dms.update(folder)   # der Server leert das Feld (field_function="NULL")

Die Änderungsverfolgung wertet None nur dann als Änderung, wenn der vom Server geladene Wert nicht None war; ein Leeren wird also genau einmal gesendet, ein bereits leeres Feld nicht erneut. Bei insert() wird ein None-Feld einfach weggelassen — bei einer Neuanlage gibt es nichts zu leeren.

8. Teilweise geladene Modelle

Ein Modell aus select().fields(…​), select_lol().fields(…​) oder get(…​, use_result_list=True, fields=…​) kennt nur die angeforderten Felder. Alle anderen lesen sich als None bzw. als leere Tabelle, genau wie leere Felder. update() sendet deshalb nur, was eindeutig ist (#15523):

Änderung an einem nicht geladenen Feld Verhalten

auf einen Wert setzen

wird gesendet

auf None setzen

ValueError: ob geleert oder nur der gelesene Wert zurückgeschrieben werden soll, ist nicht erkennbar. Zum Leeren das Feld in fields() mit anfordern.

Tabellenzeilen mit append(), extend(), insert() oder += anfügen

wird im APPEND-Modus gesendet, die Zeilen auf dem Server bleiben. Löst ein anderes Tabellenfeld automatisch REPLACETABLEFIELDS=1 aus, gibt es ValueError, weil das Anhängen sonst zum Ersetzen würde; mit ausdrücklichem True (Parameter oder options) ersetzen die angehängten Zeilen die Serverzeilen.

Tabellenfeld zuweisen, leeren oder anders verändern

ValueError, solange replace_table_fields fehlt. True ersetzt die Zeilen auf dem Server, macht aber auch jedes andere Tabellenfeld der Anfrage zum Ersetzen; False hängt an.

keine

bleibt auf dem Server unverändert

Der Fehler schützt vor Rundläufen wie folder[name] = folder[name] über alle Felder, die sonst jeden nicht geladenen Wert löschen würden. Ein nicht angefordertes Pflichtfeld hält ein Update dagegen nicht auf: es wird nicht gesendet, und der Server behält seinen Wert auch bei CHECKOBLIGATION=1.

folder = ecm.dms.select(InvoiceFolder).fields(InvoiceFolder.Title).where(InvoiceFolder.Title == "Alt").execute()[0]
folder.Title = "Neu"         # geladen: wird geschrieben
folder.Year = 2026           # nicht geladen, aber ein Wert: wird geschrieben
ecm.dms.update(folder)       # Positions und alle übrigen Felder bleiben unverändert

# eigene Instanz, siehe <<nach-dem-update>>
other = ecm.dms.select(InvoiceFolder).fields(InvoiceFolder.Title).where(InvoiceFolder.Title == "Neu").execute()[0]
other.Positions = [{"Article": "A1"}]
ecm.dms.update(other, replace_table_fields=True)   # ersetzt die Positionen auf dem Server

9. Schreibbare Systemfelder

Systemfelder (SystemFields-Enum) werden standardmäßig aus der dms.XMLUpdate-XML herausgefiltert, da der enaio-Server fast alle mit ECMAccessDeniedException („The specified system field cannot be changed by the user") ablehnt. Eine Allowlist WRITABLE_SYSTEM_FIELDS (in ecmind_blue_client.const) listet die wenigen Felder, die der Server tatsächlich akzeptiert:

Feld Verhalten

OBJECT_FOREIGNID

String-Feld. Insert + Update. Hält die Referenz auf das Dokument im externen Archivsystem; in Kombination mit OBJECT_SYSTEMID wird ein Dokument zum Referenzdokument („grüner Pfeil" wenn system_id=0). Beliebige String-Werte zulässig — manche Archiv-Konfigurationen erzwingen serverseitig eine numerische Spalte und casten dann zu '0'. Über das Attribut field_function="NULL" auf dem <Field> clearbar.

OBJECT_SYSTEMID

Int-Feld. Nur Update. Beim Insert verlangt der Server, dass OBJECT_FOREIGNID zusätzlich gesetzt ist (sonst Fehler -1013 „A system ID was specified but no foreign ID").

OBJECT_USERGUID

String-Feld (Owner). Insert + Update. Der Wert muss der Benutzername sein (z. B. "ROOT"), nicht die GUID-Hex. Der Server löst den Benutzer auf und speichert die GUID, die beim nächsten Read zurückkommt. Eine GUID-Hex direkt zu senden schlägt mit Fehler -1019 („The specified owner could not be determined") fehl.

Nicht über <Field> verfügbar, sondern als XML-Attribute auf <Object>/<ObjectType> gepflegt und über andere APIs (z. B. insert(), move()) gesteuert: OBJECT_MAIN (= maintype), OBJECT_CO (= cotype), OBJECT_ID (= object_id), SDSTA_ID/SDREG_ID/FOLDERID/REGISTERID/… (= folder_id/register_id/…). Diese liefern als <Field> ebenfalls ECMAccessDeniedException zurück.

Für OBJECT_SYSTEMID und OBJECT_FOREIGNID gibt es den typisierten Weg über den system-Namespace; er ist dem Griff auf den internen Feldnamen vorzuziehen:

doc = ecm.dms.get(MyDocument, doc_id)
doc.system.foreign_id = None          # externe Archivreferenz lösen
doc.system.system_id = None
doc = ecm.dms.update_and_get(doc)     # Server schreibt OBJECT_FLAGS um

Das Objekt muss dafür vom Server geladen sein, sonst erkennt die Änderungsverfolgung das Leeren nicht und die Anfrage geht ohne Feld hinaus. OBJECT_USERGUID hat bewusst keinen Setter, weil Lesen eine GUID liefert und Schreiben den Benutzernamen verlangt. Den Eigentümer wechselt doc.system.assign_owner("ROOT"); eine übergebene GUID lehnt die Methode mit ValueError ab.

Weitere bislang nicht entdeckte schreibbare Systemfelder können nachträglich in WRITABLE_SYSTEM_FIELDS ergänzt werden, ohne Modelländerungen.

from ecmind_blue_client import WRITABLE_SYSTEM_FIELDS
# Programmatisch erweitern (Run-time; persistiert nicht):
WRITABLE_SYSTEM_FIELDS.add("OBJECT_AVID")

10. Ausnahmen

ValueError

model.system.id ist None (Objekt wurde nicht vom Server geladen) oder check_mandatory=True und ein Pflichtfeld ist nicht gesetzt, oder ein schreibgeschütztes Feld (ECMField(read_only=…)) wurde geändert — siehe ECM-Modell. Ebenso, wenn ein nicht geladenes Feld auf None gesetzt wurde, ein nicht geladenes Tabellenfeld ohne Entscheidung über replace_table_fields (oder options) zugewiesen oder geleert wurde, oder an ein nicht geladenes Tabellenfeld angehängt wurde, während ein anderes Tabellenfeld REPLACETABLEFIELDS=1 automatisch auslöst, siehe Teilweise geladene Modelle.

TypeError

files wurde für ein Ordner- oder Register-Modell übergeben.

11. Beispiele

11.1. Einfaches Feldupdate

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Rechnung 2024 (aktualisiert)"
ecm.dms.update(folder)
folder = await ecm.dms.get(InvoiceFolder, 12345)
folder.Title = "Rechnung 2024 (aktualisiert)"
await ecm.dms.update(folder)

11.2. Dokument mit Datei ersetzen

  • Sync

  • Async

from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = ecm.dms.get(InvoiceDocument, 42)
doc.Title = "Rechnung Nr. 42 (korrigiert)"
ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/rechnung_v2.pdf")],
    replace_files=True,  # vorhandene Dateien ersetzen (Standard)
)
from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = await ecm.dms.get(InvoiceDocument, 42)
doc.Title = "Rechnung Nr. 42 (korrigiert)"
await ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/rechnung_v2.pdf")],
    replace_files=True,
)

11.3. Datei anhängen statt ersetzen

  • Sync

  • Async

from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = ecm.dms.get(InvoiceDocument, 42)
ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/anhang.pdf")],
    replace_files=False,  # Datei hinzufügen, bestehende behalten
)
from ecmind_blue_client.rpc import JobRequestFileFromPath

doc = await ecm.dms.get(InvoiceDocument, 42)
await ecm.dms.update(
    doc,
    files=[JobRequestFileFromPath("/tmp/anhang.pdf")],
    replace_files=False,
)

11.4. Update erzwingen

Nützlich, wenn externe Logik eine Neuübermittlung erfordert, obwohl keine Felder geändert wurden:

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
# keine Feldänderung — ohne force=True würde der Aufruf übersprungen
ecm.dms.update(folder, force=True)
folder = await ecm.dms.get(InvoiceFolder, 12345)
await ecm.dms.update(folder, force=True)

11.5. Pflichtfeldprüfung deaktivieren

  • Sync

  • Async

folder = ecm.dms.get(InvoiceFolder, 12345)
folder.Title = None  # Pflichtfeld leeren
# check_mandatory=False: kein Fehler, Pflichtfeldprüfung auch serverseitig deaktiviert
ecm.dms.update(folder, check_mandatory=False)
folder = await ecm.dms.get(InvoiceFolder, 12345)
folder.Title = None
await ecm.dms.update(folder, check_mandatory=False)

11.6. Joboptionen: Migration mit noch unbekannten Katalogwerten

Der Server prüft standardmäßig, ob alle übergebenen Katalogwerte im hinterlegten Katalog existieren, und kürzt zu lange Strings nicht, sondern lehnt sie ab. Bei einer Migration aus einem Altsystem sind beides typische Stolpersteine:

  • Sync

  • Async

from ecmind_blue_client.ecm import ECMUpdateOptions

ecm.dms.update(
    folder,
    options=ECMUpdateOptions().check_catalogue(False).truncate_values(True),
)
from ecmind_blue_client.ecm import ECMUpdateOptions

await ecm.dms.update(
    folder,
    options=ECMUpdateOptions().check_catalogue(False).truncate_values(True),
)

check_catalogue(False) nimmt Werte an, die noch nicht im Katalog stehen, truncate_values(True) kürzt zu lange Strings auf die definierte Feldlänge statt den Update abzulehnen. Beide sollten bewusst und möglichst eng begrenzt eingesetzt werden.

Vollständige Optionsliste in Joboptionen.

12. Siehe auch

  • update_and_get() — kombiniert update() und get() in einem Aufruf, wenn die aktualisierte Instanz benötigt wird

  • insert() — legt ein neues Objekt an

  • get() — lädt ein einzelnes Objekt vom Server