Direktes Schreiben in die Datenbank

Aus iDempiere de
Zur Navigation springen Zur Suche springen

Eine Grundregel in iDempiere ist es, das man die Datenbank nicht direkt beschreibt. Es ist sehr leicht, hier etwas kaputtzumachen und sehr schwer nachzuvollziehende Fehler auszulösen. iDempiere besitze eine ausgeklügelte Persistenzschicht in Java, die allerlei Dienste, Prüfungen und Prozesse ausführt, auf die man besser nicht verzichten will.

Wer dennoch z.B. grössere Mengen Daten in die Datenbank einbringen will, hat dazu drei Möglichkeiten:

Am besten ist es, den Easy Import zu benutzen (d.h. den Import-Button im zk-Client in der Toolbar). Dieser lädt die Daten durch das angezeigte Fenster in die Tabelle und führt dabei alle Prozesse aus, die auch bei einer direkten Eingabe in das Fenster ausgeführt werden würden. Nach meiner Erfahrung ist das eine ganz gute Lösung für ab 1 und bis zu 1000-10000 Datensätze. Insbesondere der Zugriff auf Unterfenster kostet Zeit und damit Performance. Ein Programmlauf kann hier in der angegebenen Datensatzmenge mehrere Minuten bis zu einigen Stunden dauern.

Wer mehr (oder viel mehr) Datensätze importieren will, braucht eine schnellere Lösung. Dazu habe ich die Vorgänge beim Speichern eines Datensatzes einmal mit dem Debugger analysiert, um hier Tips geben zu können, unter welchen Umständen man das machen möchte.

direktes Speichern in die Datenbank

Ich habe die folgenden Randbedingungen festgestellt, unter denen es - Vorsicht und Mut vorausgesetzt - möglich ist, Daten direkt in eine iDempiere-Tabelle zu schreiben:

  • Keine aussergewöhnlichen Setups, die da wären:
    • keine Replikation
    • SYSTEM_NATIVE_SEQUENCE auf etwas anderes als false setzen
    • keine Konfiguration für zentrale IDs
    • SYSTEM_INSERT_CHANGELOG steht auf 'N' bzw. ist für diese Tabelle nicht eingeschaltet
    • übersetzte Spalten in der Tabelle
    • kein Workflow für die Tabelle definiert
  • ordentliche Default-Werte in die Felder, als da wären:
    • richtiger Client, auf den ich auch zugreifen darf
    • richtige Organisation, auf die ich auch zugreifen darf
    • ordentliche Werte für created, createdBy, updated, updatedBy
    • ordentliche Value und ggf. DocumentNo (aber Dokumente eigentlich gar nicht direkt benutzen)
  • keine zusätzlichen Routinen, die zum Model gehören
    • nur Datensätze mit Model ohne beforeSave() und afterSave()
    • nur Datensätze, auf denen kein ModelValidator arbeitet
    • Das bedeutet, das man alle wichtigen, komplexen Klassen, insbesondere die Dokumenttypen, tunlichst in Ruhe lässt! Meine Empfehlung ist, nur eigene, selbst definierte Tabellen zu beschreiben.

wie ermittelt man die ID?

Es ist einfach möglich, die größte ID der bisherigen Tabellendaten zu ermitteln:

 SELECT MAX(MyTable_ID) FROM MyTable;

Auf den gewonnenen Wert adiert man nun 1 und fängt da mit seinen eigenen IDs an. Am Ende des Datenimports muss man dann die iDempiere-Sequenz updaten. Dazu holt man sich die entsprechende Sequenz (vierte Spalte dieses Ausdrucks) anhand des Tabellennamens

 SELECT CurrentNext, CurrentNextSys, IncrementNo, AD_Sequence_ID FROM AD_Sequence WHERE Name=? AND IsActive='Y' AND IsTableID='Y' AND IsAutoSequence='Y';

und ändert dann das CurrentNext-Feld:

 UPDATE AD_Sequence SET CurrentNext = GREATEST(CurrentNext, letzteNeueID + 1) WHERE AD_Sequence_ID = ?;

Wie ermittelt man die uuid?

Die verwendete UUID ist eine "V4" UUID. Eine solche kann man mit der PostgreSQL-Funktion uuid_generate_v4() erzeugen.

Beispiel

Mit folgenden SQL-Befehlen habe ich Daten aus einer temporären Tabelle in eine von mir vorher angelegte iDempiere-Tabelle kopiert:

 INSERT INTO BAY_Umsatzstatistik
 (
   /* iDempiere-Systemdaten */
   ad_client_id, ad_org_id, bay_umsatzstatistik_id, bay_umsatzstatistik_uu, updated, updatedby, created, createdby,
   /* meine Nutzdaten */
   bay_statistikperiode_id, c_bpartner_id, isactive, lieferungen, m_product_id, stück, stückgratis, umsatz, umsatzek
 )
 (
 SELECT
   /* iDempiere-Systemdaten */
    1000000, 1000000, row_number() over() + 1000000, uuid_generate_v4(), now(), 1001976, now(), 1001976,
   /* meine Nutzdaten */
    BAY_Statistikperiode.BAY_Statistikperiode_ID, C_BPartner.C_BPartner_ID, 'Y', e, M_Product.M_Product_ID, f,g,h,i
 FROM public.tmp AS tmp
 LEFT JOIN BAY_Statistikperiode ON BAY_Statistikperiode.Value = tmp.b
 LEFT JOIN C_BPartner ON C_BPartner.Value = tmp.c
 LEFT JOIN M_Product ON M_Product.Value = tmp.d
 );
 
 UPDATE AD_Sequence
 SET CurrentNext = greatest(CurrentNext, (SELECT MAX(BAY_Umsatzstatistik_ID) FROM BAY_Umsatzstatistik)+IncrementNo)
 WHERE Name='BAY_Umsatzstatistik' AND IsActive='Y' AND IsTableID='Y' AND IsAutoSequence='Y'

Analyse: Was geschieht beim Speichern eines neuen Datensatzes

PO.save()

Diese Methode wird beim Speichern aller Datensätze aufgerufen und stellt somit die Eintrittskarte in das Persistenzsystem dar. Ich habe hier ausschließlich verfolgt, was bei der Erzeugung eines neuen Datensatzes geschieht.

  • Ist die Organisation gültig und habe ich Zugriff darauf?
  • Before Save-Methoden werden aufgerufen
    • beforeSave() des Models wird aufgerufen, falls es eine solche Methode gibt
    • ModelValidators werden aufgerufen mit TYPE_NEW
    • Aufruf von PO.saveNew()

PO.saveNew()

  • Wenn es eine Schlüsselspalte gibt, die auf *_ID endet
    • Aufruf von DB.getNextID ()
    • UUID-Feld wird gesetzt. Dazu wird eine Formel in UUID.randomUUID() benutzt. Diese berechnet eine Version 4 IETF-UUID. (Was auch immer das heisst)
    • Gibt es eine Spalte DocumentNo, die noch keinen Wert hat, wird diese ermittelt und gesetzt
    • Gibt es eine Spalte Value, die noch keinen Wert hat, wird dort eine fortlaufende Nummer ermittelt und eingesetzt.
    • Aufruf von PO.doInsert()
    • Aufruf von PO.saveFinish()

PO.doInsert()

  • Ein INSERT-Befehl wird erzeugt, dabei wird noch tzusätzlich folgendes gemacht:
  • Ist der SysConfig-Wert SYSTEM_INSERT_CHANGELOG gesetzt, werden hier Einträge ins Changelog geschrieben.

PO.saveFinish()

  • Übersetzungszeilen für neue Datensätze werden eingefügt, wenn es übersetzte Spalten gibt.
  • afterSave() des Model wird aufgerufen, wenn eine solche Methode definiert ist.
  • ModelValidator TYPE_AFTER_NEW wird aufgerufen
  • OSGi-Event IeventTopics.PO_POST_CREATE wird aufgerufen
  • Aufruf von DocWorkflowManager.process(...)
  • interner Cache wird aktualisiert

DB.getNextID()

Hier wird es spannen, weil hier die ID erzeugt wird, die wir ja auch schreiben müssen. Zuerst einmal geschieht folgendes:

  • Sysconfig-Wert „SYSTEM_NATIVE_SEQUENCE“ ist gewöhnlich auf false gesetzt, davon gehe ich hier aus
  • Es wird beigehalten, ob ich SysAdmin im System-Mandant bin oder nicht, wenn ja wird ggf. eine System-ID erzeugt, die aus einem anderen Nummernkreis stammt
  • Aufruf von Msequence.getNextID(client, tablename, trx) (ansonsten deprecated, aber hier mit Absicht benutzt)

MSequence.getNextID()

Mit folgendem Befehl werden Informationen über die zugrundeliegende Sequenz von IDs geholt:

 selectSQL = "SELECT CurrentNext, CurrentNextSys, IncrementNo, AD_Sequence_ID "
   + "FROM AD_Sequence "
   + "WHERE Name=?"
   + " AND IsActive='Y' AND IsTableID='Y' AND IsAutoSequence='Y' "
   + " FOR UPDATE OF AD_Sequence ";
  • ggf. wird die ID vom ID-Server geholt (wenn zentrales ID-Management konfiguriert ist)

Eigentlich lasse ich den Fall, das die ID vom Server kommt, für meine Betrachtung aus. Dennoch ist er hier spannend, weil wir ja vielleicht auch IDs selber generieren wollen und nicht die automatische Sequenz benutzen wollen. Nach der Erzeugung der ID vom Server geschieht dann folgendes:

 updateSQL = conn.prepareStatement("UPDATE AD_Sequence SET CurrentNext = GREATEST(CurrentNext, ? + 1) WHERE AD_Sequence_ID = ?");

Ist die ID nicht vom Server, sondern wird von iDempiere erzeugt (der normale Fall), geschieht das folgende:

 updateSQL = conn.prepareStatement("UPDATE AD_Sequence SET CurrentNext = CurrentNext + ? WHERE AD_Sequence_ID = ?");