Ein Trigger auf eine Tabelle ist ja nichts Besonderes - eine LOB-Spalte (CLOB, BLOB) auch nicht. Setzt man aber beides zusammen ein, so kann man ein seltsames Verhalten beobachten: Irgendwie feuert der Trigger niemals zur richtigen Zeit ...
Dazu (wie immer) ein Beispiel. Eine Tabelle mit einer LOB-Spalte und ein Trigger, welcher die Größe des LOB in eine andere Spalte schreibt, werden erzeugt. Zur Sicherheit wird im Trigger zusätzlich die Spalte TRIGGER_FIRED auf Y gesetzt.
import java.sql.*; import java.io.*; import oracle.jdbc.*; import oracle.sql.*; public class lobWriter { public static void main(String args[]) throws Exception { PreparedStatement pstmtInsert = null; PreparedStatement pstmtSelect = null; ResultSet rsLobLocator = null; CLOB lobLocator = null; int iLobId = 1; Writer lobWriter = null; DriverManager.registerDriver(new oracle.jdbc.OracleDriver()); Connection con = DriverManager.getConnection( "jdbc:oracle:thin:@192.168.2.140:1521:orcl", "scott", "tiger" ); con.setAutoCommit(false); // LOB-Locator in die Tabelle einfügen // Insert a lob locator into the table pstmtInsert = con.prepareStatement ( "insert into lobtable (id, dokument) values (?, empty_clob())" ); // Den LOB-Locator abrufen // Retrieve the lob locator pstmtSelect = con.prepareStatement ( "select dokument from lobtable where id = ? for update" ); pstmtInsert.setInt(1, iLobId); pstmtInsert.execute(); pstmtSelect.setInt(1, iLobId); rsLobLocator = pstmtSelect.executeQuery(); if (rsLobLocator.next()) { lobLocator = ((OracleResultSet)rsLobLocator).getCLOB(1); } // LOB befüllen // populate lob via streaming lobWriter = lobLocator.setCharacterStream(0L); for (int i=0;i<1000;i++) { lobWriter.write("Dies ist ein Text ... zum " + i + "ten!\n"); } // Ressourcen schließen // Close the handles lobWriter.flush(); lobWriter.close(); rsLobLocator.close(); pstmtSelect.close(); pstmtInsert.close(); con.commit(); con.close(); } }
Der Java-Code macht ein INSERT nur auf die Spalten ID und DOKUMENT. Schauen wir uns die Tabelleninhalte anschließend mal an ...
select id, dokument, laenge len_table, dbms_lob.getlength(dokument) len_lob, trigger_fired from lobtable ID DOKUMENT LEN_TABLE LEN_LOB T ---------- ---------------------------------------- ---------- ---------- - 1 Dies ist ein Text ... zum 0ten! 0 33890 Y Dies ist ein Text ... zum 1ten! Dies ist ein Tex
Und dieses SQL offenbart das ganze Dilemma. Dass der Trigger gefeuert wurde, ist an der Spalte TRIGGER_FIRED klar erkennbar. Allerdings wurde eine Länge von 0 geschrieben - wie die Funktion DBMS_LOB.GETLENGTH erkennen lässt, ist der LOB aber klar größer. Ganz offensichtlich hat der Trigger zum falschen Zeitpunkt gefeuert.
Tatsächlich feuert der Trigger unmittelbar nach dem DML-Kommando, also dem SQL Insert. Wenn man in den Java-Code schaut, wird dort jedoch zunächst ein leerer Lob (empty_clob()) eingefügt. Anschließend "holt" sich das Java-Programm den LOB-Locator mit einem SELECT ... FOR UPDATE und füllt ihn mittels Java Streaming. Für die Datenbank ist das "Füllen" des LOB mit Java Streaming allerdings kein DML mehr. Verfolgt man diesen Weg, um LOB-Inhalte in die Datenbank zu laden, so können vorhandene Trigger nicht auf die später per Streaming geladenen LOB-Inhalte zugreifen ...
Und wie löst man das Problem? Mit temporären LOBs - wir ändern den Java-Code ein wenig um:
import java.sql.*; import java.io.*; import oracle.jdbc.*; import oracle.sql.*; public class lobWriter2 { public static void main(String args[]) throws Exception { PreparedStatement pstmtInsert = null; ResultSet rsLobLocator = null; CLOB lobLocator = null; int iLobId = 1; Writer lobWriter = null; DriverManager.registerDriver(new oracle.jdbc.OracleDriver()); Connection con = DriverManager.getConnection( "jdbc:oracle:thin:@192.168.2.140:1521:orcl", "scott", "tiger" ); con.setAutoCommit(false); // temporären LOB erzeugen und befüllen // create temporary lob and populate it lobLocator = CLOB.createTemporary(con, true, CLOB.DURATION_SESSION); lobWriter = lobLocator.setCharacterStream(0L); for (int i=0;i<1000;i++) { lobWriter.write("Dies ist ein Text ... zum " + i + "ten!\n"); } lobWriter.flush(); lobWriter.close(); // LOB in die Tabelle einfügen // Insert the LOB into the table pstmtInsert = con.prepareStatement ( "insert into lobtable (id, dokument) values (?, ?)" ); pstmtInsert.setInt(1, iLobId); ((OraclePreparedStatement)pstmtInsert).setCLOB(2, lobLocator); pstmtInsert.execute(); // WICHTIG: temporären LOB freigeben // IMPORTANT: free the temporary lob lobLocator.freeTemporary(); pstmtInsert.close(); con.commit(); con.close(); } }
Nun wird zunächst ein temporärer LOB erzeugt, dieser wird befüllt und erst dann findet das SQL INSERT statt. Zum DML-Zeitpunkt ist der LOB auf Datenbankseite also vollständig zusammengestellt; wenn der Trigger feuert, "sieht" er also die Inhalte und die Spalte LAENGE in der Tabelle wird richtig vom Trigger gesetzt. Wir machen die Testabfrage nochmal:
select id, dokument, laenge len_table, dbms_lob.getlength(dokument) len_lob, trigger_fired from lobtable ID DOKUMENT LEN_TABLE LEN_LOB T ---------- ---------------------------------------- ---------- ---------- - 1 Dies ist ein Text ... zum 0ten! 33890 33890 Y Dies ist ein Text ... zum 1ten!
Mehr zum Thema temporäre LOBs (welche übrigens im TEMP-Tablespace gespeichert werden) findet Ihr in der Dokumentation. Wichtig ist bei temporären LOBs vor allem, dass Ihr sie freigebt, wenn Ihr fertig seid - dazu hatte ich in einem früheren Posting auch schon ein wenig was geschrieben.