Variantenbäume von "enaio A" nach "enaio B" migrieren

Sollen Dokumente von einem enaio zum anderen (oder von einem Schrank in einen neuen migriert werden) stellen sich viele Fragen. Wie umgehen mit Notizen, Notizverweisen, „grünen Pfeilen“ etc. Auch variantierte Dokumente sind ein grosses Thema. Variantenbäume können komplex sein, nicht immer ist das Ursprungselement einer Variante auch dessen Vaterelement. Die letzte Variante muss nicht die aktive Variante sein.

Die Methode zum Variantenimport via dms.XmlInserts bzw. Variantenimport via xml_import() unterstützt leider das Setzen des korrekten „…AUS“-Werts nicht. Dies ist möglich mit std.GetDocVariant, allerdings scheint dies nicht die gleichzeitige Übergabe neuer Metadaten zu erlauben.

Da sich bei Varianten aber Dokumentinhalte und Metadaten unterscheiden können, ist eine nachträgliche Bearbeitung, z. B. mit dms.SetActiveVariant gefolgt von einem dms.XmlUpdate oder ähnlichem nötig. Bei Migrationsszenarien sollen aber nur die nötigen Aktionen ausgeführt werden, um Zeit zu sparen und die Dokumenthistorien schlank zu halten.

Variantenbaum auslesen

Der Variantenbaum lässt sich noch relativ einfach auslesen, hier ein kurzes Beispiel zum oben gezeigten Baum:

from ecmind_blue_client.query_condition_field import QueryConditionField as Field
from ecmind_blue_client.query_condition_group import QueryConditionGroup as Group
from ecmind_blue_client.const import QueryOperators
from ecmind_blue_client import Job
from ecmind_blue_client.tcp_client import TcpClient as Client
from XmlElement import XmlElement as X

client = Client('localhost', 4000, 'TestClient', 'root', 'optimal', False)

xml = X('DMSQuery', 
    a={'requesttype': 'HOL'},
    s=[X('Archive',
        s=[X('ObjectType', 
            a={'internal_name': 'UnittestDoc'}, 
            s=[X('Conditions', 
                s=[X('ConditionObject', 
                    a={'internal_name': 'UnittestDoc'}, 
                    s=[X('FieldCondition', 
                        a={'internal_name': 'OSID', 'operator': QueryOperators.EQUAL.value},
                        s=[X('Value', t='167944')]
                    )]
                )]
            )]
        )]
    )]
)

job = Job(
    jobname='dms.GetResultList', 
    Flags=0, 
    XML=xml, 
    RequestType='HOL',
    Variants=1
)

result = client.execute(job)
result_xml = X.from_string(result.values['XML'])
print(result_xml)

Dies ergibt:

<DMSContent format="HOL" output_language="0" version="9.0.780.18346" timestamp="2021-03-19T17:32:00" user="ROOT" station="ecm2" instance="TestClient">
    <Archive name="Unittest" id="7" osguid="9D4271A85AF74976B31BDE76327B3A6D">
        <ObjectType name="Unittest Doc" id="262151" maintype="4" cotype="7" osguid="42C0631FCA5F440886FA800B7DB82072" internal_name="UnittestDoc" type="DOCUMENT" modul="MULTIDOC" table="object10">
            <ObjectList>
                <Object id="167948">
                    <Fields />
                    <DocumentVariants>
                        <DocumentVariant is_active="0" doc_id="167944" doc_ver="Original" doc_parent="0">
                            <DocumentVariant is_active="0" doc_id="167946" doc_ver="1.0.0" doc_parent="167944" />
                            <DocumentVariant is_active="0" doc_id="167947" doc_ver="2.0.0" doc_parent="167946">
                                <DocumentVariant is_active="1" doc_id="167948" doc_ver="2.1.0" doc_parent="167947">
                                    <DocumentVariant is_active="0" doc_id="167952" doc_ver="2.1.1" doc_parent="167948" />
                                    <DocumentVariant is_active="0" doc_id="167954" doc_ver="2.1.2" doc_parent="167948" />
                                </DocumentVariant>
                            </DocumentVariant>
                            <DocumentVariant is_active="0" doc_id="167949" doc_ver="3.0.0" doc_parent="167948" />
                            <DocumentVariant is_active="0" doc_id="167956" doc_ver="4.0.0" doc_parent="167944" />
                        </DocumentVariant>
                    </DocumentVariants>
                </Object>
            </ObjectList>
            <Statistics startpos="0" pagesize="-1" total_hits="1" />
        </ObjectType>
    </Archive>
    <Messages />
</DMSContent>

Universalfunktion?

Nun wäre eine Helper-Funktion denkbar, welche einen vollen Variantenbaum mit minimalen API-Schritten erzeugt.

Dies lässt aber folgende Fragen zum Design offen:

  • Für jede Quellvariante muss noch ein separates dms.GetObjectDetails ausgelöst werden, da dms.GetResultList die Metadaten von Varianten nicht auslesen kann.
  • dms.GetObjectDetailsliefert aber nicht alle Metadaten aus dem Bereich der Basisparameter, welche ggf. auf technischen/historischen Felder mit migriert werden sollen.
  • Wird dms.SetActiveVariant anstelle von dms.XmlInserts mit dem Attribute parentvariant_id genutzt, müsste zum Sparen von API-Aufrufen geprüft werde, ob Metadaten zum Elternelement überhaupt abweichen und nur in diesem Fall ein dms.XmlUpdate ausgelöst werden.
  • Die Importreihenfolge muss korrekt sein, jeder Parent muss vor seinen Child-Dokumenten angelegt werden.
  • Für jede Quelldatei muss ein std.StoreInCacheById ausgelöst werden und diese Dateien für die Migration vorgehalten werden.
  • Wie ist überhaupt umzugehen mit Teilmigrationen? Ist es möglich, eine Migration vorzubereiten oder müssten dann bei veränderten, variantierten Quelldokumenten die vorab erzeugte Dokumenthierarchie nochmals gelöscht werden, um zuverlässige Ergebnisse zu erhalten?
  • Was ist, wenn der Migrationsprozess an einer beliebigen Stelle abbricht?
  • Was ist, wenn sich nicht aktive Varianten verändern beziehungsweise die Dokumente einer Variante inhaltlich verändern? Checksummenvergleich aller Variantendokumente in Quelle und Ziel? („Cache invalidation is hard.“)

Eventuell wäre es für eine Variantenmigration sinnvoll diese bewusst in zwei Funktionen aufzuteilen:

  1. Export von Variantenbäumen mit allen nötigen Daten
    Diesen Export könnte man dann z.B. als JSON abspeichern um eine spätere Verarbeitung zu vereinfachen.

  2. Import des vorher festgelegten Exports
    Verarbeitung der JSON-Datei.

Mit dieser Aufteilung hätte man eine etwas bessere Kontrolle, sollten während der Verarbeitung Probleme auftreten. Zudem können die zwei Schritte zeitlich unabhängig voneinander ausgeführt werden.
Dennoch haben wir festgestellt, dass sollten Probleme auftreten die bisherige Verarbeitung gestoppt werden muss; gelöscht; und wieder ab dem Dokument begonnen werden.
Teilmigrationen sind über diesen Weg leider auch nicht möglich. Es wäre nur möglich ein Dokument mit allen Varianten nochmals zu überschreiben.

Ich würde grundsätzlich die Migration der Varianten unabhängig von der Migration der Ordner, Register und „normaler“ Dokumente sehen (sofern die Varianten nur einen kleinen Teil der Dokumente betrifft).

Das Problem ist, dass sich im Prinzip die Daten bei einer Inkrementellen Migration noch im nach hinein ändern könnten sodass sich sowohl die Metadaten wie auch die aktive Variante etc. ändert.

Man sollte sich daher im Zielsystem auf einem versteckten Feld sowohl die ObjectID wie auch die Checksumme der Metadaten speichern.

Daher folgender Vorschlag:

  1. Ermitteln aller Varianten im Quellsystem und Zielsystem per SQL (API ado.ExecuteSQL)
  2. Selektieren alle Root Variantendokumente
SELECT v.doc_id FROM osdocver v WHERE v.doc_parent = 0
  1. Pro Root-Dokument für alle Varianten die Checksummen der Dokumente (Tabelle osdochash), Checksumme der Metadaten und natürlich die IDs inklusive des Aktive Variante Flags vergleichen
    object2 ist dann die Tabelle des Documenttyps, 354 ist die ID des jeweiligen Root Dokuments
SELECT o.id as id, o.feld1 as SourceId, o.feld2 as MetaHash, h.oshash as FileHash FROM enaio.osdocver v LEFT OUTER JOIN enaio.osdochash h ON (h.object_id = v.doc_id AND h.osguid IS NULL), enaio.object2 o WHERE v.doc_root = 354 AND v.doc_id = o.id
  1. Wenn sich nur ein Wert bei den Werten pro Root-Dokument unterscheidet dann Variante im Zielsystem vollständig löschen und erneut importieren

Noch ein kleiner Gedanke zu Migration der Metadaten (+ Hash Bildung):

Vielleicht wäre es ein Ansatz jeweils das Ergebnis der des GetResultList (als HOL mit Variants=1) per XSL Transformation von einem DMSContent (GetResultList) zu einem DSMData (XMLImport) umzuwandeln, darauf die Checksumme zu bilden. Diese Checksumme und die ID „injected“ ihr dann noch in das XML und verwendet es direkt als Import XML.

Meine Hoffnung ist, dass man dies so generisch definieren kann, dass ihr dies quasi für alle Dokumenttypen unverändert verwenden könnt.

Hinweis: Das kann auch der ecmind_blue_client:

from ecmind_blue_client.tcp_client import TcpClient as Client

client = Client('localhost', 4000, 'TestClient', 'root', 'optimal', False)

variants = client.execute_sql('SELECT v.doc_id FROM osdocver v WHERE v.doc_parent = 0')
print(variants) #[{'doc_id': '167944'}]