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 |
|---|---|---|
|
|
Die zu aktualisierende Modell-Instanz. Muss vom Server geladen worden sein, damit |
|
|
Optionale Dateiliste für das Dokument. Nur für |
|
|
Wenn |
|
|
Wenn |
|
|
Wenn |
|
|
Überschreibt die automatische Steuerung der Option |
|
|
Zusätzliche |
|
|
4. Automatisches Überspringen
Wenn alle der folgenden Bedingungen zutreffen, wird der Server-Aufruf ohne Fehler übersprungen:
-
force=False(Standard) -
model.system.is_modifiedistFalse— keine Felder wurden geändert -
filesistNone
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_replacementistTrue), wird das Tabellenfeld komplett neu übertragen und der Server-Aufruf erhält automatischREPLACETABLEFIELDS=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 mitREPLACETABLEFIELDS=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=Falsenach 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 |
|
Tabellenzeilen mit |
wird im APPEND-Modus gesendet, die Zeilen auf dem Server bleiben. Löst ein anderes Tabellenfeld automatisch |
Tabellenfeld zuweisen, leeren oder anders verändern |
|
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 |
|---|---|
|
String-Feld. Insert + Update. Hält die Referenz auf das Dokument im externen Archivsystem; in Kombination mit |
|
Int-Feld. Nur Update. Beim Insert verlangt der Server, dass |
|
String-Feld (Owner). Insert + Update. Der Wert muss der Benutzername sein (z. B. |
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.idistNone(Objekt wurde nicht vom Server geladen) odercheck_mandatory=Trueund 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 aufNonegesetzt wurde, ein nicht geladenes Tabellenfeld ohne Entscheidung überreplace_table_fields(oderoptions) zugewiesen oder geleert wurde, oder an ein nicht geladenes Tabellenfeld angehängt wurde, während ein anderes TabellenfeldREPLACETABLEFIELDS=1automatisch auslöst, siehe Teilweise geladene Modelle. TypeError-
fileswurde 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()undget()in einem Aufruf, wenn die aktualisierte Instanz benötigt wird -
insert() — legt ein neues Objekt an
-
get() — lädt ein einzelnes Objekt vom Server