Mit Transaktionen im engeren technischen Sinne ist gemeint, dass mehrere Arbeitsschritte zusammengefasst werden und entweder alle ausgeführt werden, oder alle nicht ausgeführt werden (Alles-oder-nichts-Prinzip).
Wenn Geld von einem Konto auf ein anderes gebucht werden soll, dann darf es nicht vorkommen, dass die Abbuchung vom ersten Konto erfolgreich durchgeführt wird, aber die Gutschrift auf das zweite Konto fehlschlägt.
Transaktionen stellen Konsistenz sicher.
Transaktionen werden durch ein "Begin"-Kommando gestartet. Konnten die einzelnen Arbeitsschritte erfolgreich durchgeführt werden, werden sie durch ein "Commit"-Kommando endgültig bestätigt (und für andere Prozesse sichtbar). Gab es einen Fehler, werden sie durch ein "Rollback"-Kommando zurückgesetzt.
Das Begin-Startkommando und das beendende Commit- bzw. Rollback-Kommando stellt die so genannte Transaktionsklammer dar.
Die Arbeitsschritte innerhalb der Transaktion werden auch schon mal als LUW (Logical Unit of Work) bezeichnet.
Transaktionen werden an den "ACID"-Kriterien gemessen:
Wenn Transaktionen dem "ACID"-Prinzip entsprechen sollen, müssen sie serialisiert nacheinander durchgeführt werden. Aus Performance-Gründen wird hiervon oft abgewichen und ein niedrigerer "Transaction Isolation Level" eingestellt.
Das kann zu folgenden Fehlern führen:
In Datenbanken können verschiedene so genannte "Transaction Isolation Level" eingestellt werden, um den besten Kompromiss zwischen Isolation und Performance vorzugeben.
Folgende "Transaction Isolation Level" sind in ANSI-SQL2 definiert:
Der Oracle WebLogic Java EE Application Server kennt noch weitere spezielle "Transaction Isolation Level", die insbesonders im Zusammenspiel mit der Oracle Datenbank sinnvoll sein können:
In einigen Transaktionsmanagern kann die "Transaction Propagation" pro Methode unterschiedlich eingestellt werden. Es sind nicht immer alle Einstellungen möglich.
Die unterschiedlichen Einstellungen zur Transaction Propagation bewirken beim Eintritt in die jeweilige Methode Folgendes:
Während für einfache Datenbank-Transaktionen mit nur einer Datenbank die Begin-, Commit- und Rollback-Kommandos ausreichen, wird für komplexere Transaktionssteuerungen und bei verteilten Transaktionen über mehrere Ressourcen (z.B. mehrere Datenbanken oder auch DB + JMS + JCA + ...) ein Transaktionsmanager benötigt.
Zu diesem Themenbereich sind folgende Begriffe wichtig:
Downloaden Sie ein aktuelles Java SE JDK von http://www.oracle.com/technetwork/java/javase/downloads und installieren Sie es wie beschrieben unter java-install.htm.
Die folgenden Beispiele verwenden MySQL als Datenbank, können aber durch leichte Modifikationen an beliebige andere transaktionsfähige SQL-Datenbanken angepasst werden.
Installieren Sie MySQL wie beschrieben unter: mysql.htm#InstallationUnterWindows. Die folgenden Beispiele gehen davon aus, dass Sie als 'Database-Name' "MeineDb" wählen ("CREATE DATABASE MeineDb;") (Sie können auch stattdessen 'dbUrl' anpassen).
MySQL verwendet defaultmäßig die 'MyISAM'-Engine, die aber keine Transaktionen unterstützt und deshalb für die folgenden Beispiele ungeeignet ist. Wir wollen die 'InnoDB'-Engine verwenden: Öffnen Sie die zu MySQL gehörende 'my.ini'- oder 'my.cnf'-Datei, zum Beispiel in den Verzeichnissen '/etc/mysql', 'C:\', 'C:\Windows' oder 'C:\Programme\MySQL\MySQL Server 4.1'. Stellen Sie sicher dass nicht 'skip-innodb' gesetzt ist und kontrollieren Sie die anderen 'InnoDB Specific options' (z.B. 'innodb_data_file_path=ibdata:30M').
Downloaden Sie den MySQL-JDBC-Treiber (z.B. 'mysql-connector-java-5.1.16-bin.jar' aus 'mysql-connector-java-5.1.16.zip').
Legen Sie eine Projektverzeichnisstruktur an, zum Beispiel so:
[\MeinWorkspace] '- [TxnDb] |- [bin] |- [lib] | '- mysql-connector-java-5.1.16-bin.jar '- [src] '- [dbtransactions] '- ... (Java-Klassen)
Erzeugen Sie im Package-Verzeichnis 'dbtransactions' folgende Java-Datei 'TxnRollback.java':
package dbtransactions; import java.sql.*; import java.text.SimpleDateFormat; import java.util.Date; public class TxnRollback { static final String dbDrv = "com.mysql.jdbc.Driver"; static final String dbUrl = "jdbc:mysql://localhost:3306/MeineDb"; static final String dbUsr = "root"; static final String dbPwd = "mysqlpwd"; static final String dbTyp = "ENGINE=InnoDB"; static final String dbTbl = "MeineTestTabelle1"; public static void main( String[] args ) throws ClassNotFoundException, SQLException { Connection cn = null; Statement st = null; try { Class.forName( dbDrv ); cn = DriverManager.getConnection( dbUrl, dbUsr, dbPwd ); cn.setAutoCommit( false ); System.out.println( "AutoCommit=" + cn.getAutoCommit() + ", TransactionIsolation=" + cn.getTransactionIsolation() ); st = cn.createStatement(); try { st.executeUpdate( "drop table " + dbTbl ); } catch( SQLException ex ) {/*ok*/} st.executeUpdate( "create table " + dbTbl + " ( t VARCHAR(12) ) " + dbTyp ); st.executeUpdate( "insert into " + dbTbl + " values ( '" + getTime() + "' )" ); readData( "Nach Insert:", st ); cn.rollback(); // 'cn.commit();' bzw. 'cn.rollback();' readData( "Nach Commit bzw. Rollback:", st ); } finally { try { if( null != st ) st.close(); } catch( Exception ex ) {/*ok*/} try { if( null != cn ) cn.close(); } catch( Exception ex ) {/*ok*/} } } private static final void readData( String s, Statement st ) throws SQLException { System.out.println( s ); ResultSet rs = st.executeQuery( "select * from " + dbTbl ); if( rs.next() ) System.out.println( " " + rs.getString( 1 ) ); else System.out.println( " Keine Daten." ); try { if( null != rs ) rs.close(); } catch( Exception ex ) {/*ok*/} } private static final String getTime() { return (new SimpleDateFormat( "HH:mm:ss.SSS" )).format( new Date() ); } }
Passen Sie die String-Konstanten 'dbDrv', 'dbUrl', 'dbUsr' und 'dbPwd' an.
Kompilation und Ausführung können z.B. folgendermaßen aus dem Sourceverzeichnis 'TxnDb\src' heraus erfolgen (passen Sie die Pfade an):
cd \MeinWorkspace\TxnDb\src
javac -d ../bin dbtransactions/TxnRollback.java
java -cp ../bin;../lib/mysql-connector-java-5.1.16-bin.jar dbtransactions.TxnRollback
Vor dem Rollback-Kommando werden die Daten korrekt gelesen, nach dem Rollback-Kommando gibt es keine Daten mehr (falls das bei Ihnen nicht so ist, stimmt etwas mit der MySQL-InnoDB-Konfiguration nicht).
Wenn Sie 'cn.rollback();' durch 'cn.commit();' ersetzen, bleiben die Daten persistent gespeichert.
Ersetzen Sie 'cn.commit();' wieder durch 'cn.rollback();', aber entfernen Sie beim 'create table'-Kommando den Zusatz 'ENGINE=InnoDB': Auch dann bleiben die Daten persistent gespeichert, weil der Rollback nicht funktioniert.
Das folgende Beispiel stellt verschiedene Transaction Isolation Level ein und untersucht die Ergebnisse auf Dirty Reads, Non-repeatable Reads und Phantom Reads.
Erzeugen Sie im Package-Verzeichnis 'dbtransactions' folgende Java-Hilfsdatei 'DbUtil.java':
package dbtransactions; import java.sql.*; public class DbUtil { public static final void showTransactionSupport( String dbDrv, String dbUrl, String dbUsr, String dbPwd ) throws ClassNotFoundException, SQLException { Connection cn = null; try { Class.forName( dbDrv ); cn = DriverManager.getConnection( dbUrl, dbUsr, dbPwd ); DatabaseMetaData md = cn.getMetaData() ; System.out.println( "supportsTransactions(): " + md.supportsTransactions() ); System.out.println( "TRANSACTION_READ_UNCOMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED) ); System.out.println( "TRANSACTION_READ_COMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED) ); System.out.println( "TRANSACTION_REPEATABLE_READ: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ) ); System.out.println( "TRANSACTION_SERIALIZABLE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE) ); System.out.println( "Default transaction isolation: " + md.getDefaultTransactionIsolation() ); System.out.println( "Actual transaction isolation: " + cn.getTransactionIsolation() ); System.out.println( "Actual AutoCommit: " + cn.getAutoCommit() ); } finally { try { if( null != cn ) cn.close(); } catch( Exception ex ) {/*ok*/} } } public static final void showDbTbl( String s, Statement st, String dbTbl ) throws SQLException { ResultSet rs = null; try { rs = st.executeQuery( "select * from " + dbTbl ); ResultSetMetaData rsmd = rs.getMetaData(); int i, n = rsmd.getColumnCount(); System.out.println( s ); for( i=0; i<n; i++ ) System.out.print( "+---------------" ); System.out.println( "+" ); for( i=1; i<=n; i++ ) System.out.print( "| " + extendStringTo14( rsmd.getColumnName( i ) ) ); System.out.println( "|" ); for( i=0; i<n; i++ ) System.out.print( "+---------------" ); System.out.println( "+" ); while( rs.next() ) { for( i=1; i<=n; i++ ) System.out.print( "| " + extendStringTo14( rs.getString( i ) ) ); System.out.println( "|" ); } for( i=0; i<n; i++ ) System.out.print( "+---------------" ); System.out.println( "+" ); } finally { try { if( null != rs ) rs.close(); } catch( Exception ex ) {/*ok*/} } } // Extend String to length of 14 characters private static final String extendStringTo14( String s ) { if( null == s ) s = ""; final String sFillStrWithWantLen = " "; final int iWantLen = sFillStrWithWantLen.length(); final int iActLen = s.length(); if( iActLen < iWantLen ) return (s + sFillStrWithWantLen).substring( 0, iWantLen ); if( iActLen > 2 * iWantLen ) return s.substring( 0, 2 * iWantLen ); return s; } public static final int countRows( Statement st, String dbTbl ) throws SQLException { int c = 0; ResultSet rs = st.executeQuery( "select count(*) from " + dbTbl ); if( rs.next() ) c = rs.getInt( 1 ); rs.close(); return c; } }
Erzeugen Sie im Package-Verzeichnis 'dbtransactions' folgende Java-Datei 'TxnIsol.java':
package dbtransactions; import java.sql.*; import java.text.SimpleDateFormat; import java.util.Date; public class TxnIsol { static final String dbDrv = "com.mysql.jdbc.Driver"; static final String dbUrl = "jdbc:mysql://localhost:3306/MeineDb"; static final String dbUsr = "root"; static final String dbPwd = "mysqlpwd"; static final String dbTyp = "ENGINE=InnoDB"; static final String dbTbl = "MeineTestTabelle1"; public static void main( String[] args ) throws ClassNotFoundException, SQLException, InterruptedException { DbUtil.showTransactionSupport( dbDrv, dbUrl, dbUsr, dbPwd ); testTransactionIsolation( Connection.TRANSACTION_READ_UNCOMMITTED ); testTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED ); testTransactionIsolation( Connection.TRANSACTION_REPEATABLE_READ ); testTransactionIsolation( Connection.TRANSACTION_SERIALIZABLE ); } public static void testTransactionIsolation( int txnisol ) throws ClassNotFoundException, SQLException, InterruptedException { System.out.println( "\n===========================================================" ); prepareTable(); System.out.println( "\n" + getTime() + " prepareTable() ready." ); (new MyThread1( txnisol )).start(); (new MyThread2( txnisol )).start(); Thread.sleep( 1000 ); } private static final void prepareTable() throws ClassNotFoundException, SQLException { Connection cn = null; Statement st = null; try { Class.forName( dbDrv ); cn = DriverManager.getConnection( dbUrl, dbUsr, dbPwd ); st = cn.createStatement(); try { st.executeUpdate( "drop table " + dbTbl ); } catch( SQLException ex ) {/*ok*/} st.executeUpdate( "create table " + dbTbl + " ( i INT, s VARCHAR(12), t VARCHAR(12) ) " + dbTyp ); st.executeUpdate( "insert into " + dbTbl + " values ( 0, 'Null', '" + getTime() + "' );" ); } finally { try { if( null != st ) st.close(); } catch( Exception ex ) {/*ok*/} try { if( null != cn ) cn.close(); } catch( Exception ex ) {/*ok*/} } } public static final String getTime() { return (new SimpleDateFormat( "HH:mm:ss.SSS" )).format( new Date() ); } } class MyThread1 extends Thread { int txnisol; MyThread1( int txnisol ) { this.txnisol = txnisol; } @Override public void run() { Connection cn1 = null; Statement st1 = null; int c1=0, c2=0; try { Thread.sleep( 100 ); Class.forName( TxnIsol.dbDrv ); cn1 = DriverManager.getConnection( TxnIsol.dbUrl, TxnIsol.dbUsr, TxnIsol.dbPwd ); cn1.setTransactionIsolation( txnisol ); cn1.setAutoCommit( false ); System.out.println( "\nThread1: AutoCommit=" + cn1.getAutoCommit() + ", TransactionIsolation=" + cn1.getTransactionIsolation() + "\n" ); st1 = cn1.createStatement(); st1.executeUpdate( "insert into " + TxnIsol.dbTbl + " values ( 1, 'Thread1', '" + TxnIsol.getTime() + "' );" ); DbUtil.showDbTbl( TxnIsol.getTime() + ", Thread1:", st1, TxnIsol.dbTbl ); c1 = DbUtil.countRows( st1, TxnIsol.dbTbl ); Thread.sleep( 400 ); c2 = DbUtil.countRows( st1, TxnIsol.dbTbl ); System.out.println( "\nc1=" + c1 + ", c2=" + c2 ); DbUtil.showDbTbl( TxnIsol.getTime() + ", Thread1:", st1, TxnIsol.dbTbl ); System.out.println( "\n" + TxnIsol.getTime() + ", Thread1: Commit" ); cn1.commit(); } catch( Exception ex ) { ex.printStackTrace(); } finally { try { if( null != st1 ) st1.close(); } catch( Exception ex ) {/*ok*/} try { if( null != cn1 ) cn1.close(); } catch( Exception ex ) {/*ok*/} } } } class MyThread2 extends Thread { int txnisol; MyThread2( int txnisol ) { this.txnisol = txnisol; } @Override public void run() { Connection cn2 = null; Statement st2 = null; try { Thread.sleep( 300 ); Class.forName( TxnIsol.dbDrv ); cn2 = DriverManager.getConnection( TxnIsol.dbUrl, TxnIsol.dbUsr, TxnIsol.dbPwd ); cn2.setTransactionIsolation( txnisol ); cn2.setAutoCommit( false ); System.out.println( "\nThread2: AutoCommit=" + cn2.getAutoCommit() + ", TransactionIsolation=" + cn2.getTransactionIsolation() + "\n" ); st2 = cn2.createStatement(); DbUtil.showDbTbl( TxnIsol.getTime() + ", Thread2:", st2, TxnIsol.dbTbl ); st2.executeUpdate( "insert into " + TxnIsol.dbTbl + " values ( 2, 'Thread2', '" + TxnIsol.getTime() + "' );" ); System.out.println( "\n" + TxnIsol.getTime() + ", Thread2: Commit insert i=2" ); cn2.commit(); st2.executeUpdate( "update " + TxnIsol.dbTbl + " set s = 'Thread2', t = '" + TxnIsol.getTime() + "' where i = 0;" ); System.out.println( TxnIsol.getTime() + ", Thread2: Commit update i=0\n" ); cn2.commit(); DbUtil.showDbTbl( TxnIsol.getTime() + ", Thread2:", st2, TxnIsol.dbTbl ); cn2.commit(); } catch( Exception ex ) { ex.printStackTrace(); } finally { try { if( null != st2 ) st2.close(); } catch( Exception ex ) {/*ok*/} try { if( null != cn2 ) cn2.close(); } catch( Exception ex ) {/*ok*/} } } }
Passen Sie die String-Konstanten 'dbDrv', 'dbUrl', 'dbUsr' und 'dbPwd' an.
Kompilation und Ausführung können z.B. folgendermaßen aus dem Sourceverzeichnis 'TxnDb\src' heraus erfolgen (passen Sie die Pfade an):
cd \MeinWorkspace\TxnDb\src
javac -d ../bin dbtransactions/TxnIsol.java
java -cp ../bin;../lib/mysql-connector-java-5.1.16-bin.jar dbtransactions.TxnIsol
Der erste Ergebnisblock für 'TRANSACTION_READ_UNCOMMITTED' kann zum Beispiel so aussehen:
20:00:00.078 prepareTable() ready. Thread1: TransactionIsolation=1 <-- TRANSACTION_READ_UNCOMMITTED 20:00:00.234, Thread1: +---+---------+--------------+ | i | s | t | +---+---------+--------------+ | 0 | Null | 20:00:00.046 | | 1 | Thread1 | 20:00:00.234 | +---+---------+--------------+ Thread2: TransactionIsolation=1 <-- TRANSACTION_READ_UNCOMMITTED 20:00:00.484, Thread2: +---+---------+--------------+ | i | s | t | +---+---------+--------------+ | 0 | Null | 20:00:00.046 | | 1 | Thread1 | 20:00:00.234 | <-- Dirty Read +---+---------+--------------+ 20:00:00.484, Thread2: Commit insert i=2 c1=2, c2=3 <-- Phantom Read 20:00:00.656, Thread1: +---+---------+--------------+ | i | s | t | +---+---------+--------------+ | 0 | Thread2 | 20:00:00.546 | <-- Non-repeatable Read | 1 | Thread1 | 20:00:00.234 | | 2 | Thread2 | 20:00:00.484 | <-- Phantom Read +---+---------+--------------+ 20:00:00.671, Thread1: Commit 20:00:00.671, Thread2: Commit update i=0 20:00:00.750, Thread2: +---+---------+--------------+ | i | s | t | +---+---------+--------------+ | 0 | Thread2 | 20:00:00.546 | | 1 | Thread1 | 20:00:00.234 | | 2 | Thread2 | 20:00:00.484 | +---+---------+--------------+
Die Ergebnisse sind etwas mühsam zu interpretieren, aber Sie können sehen, dass bei 'TRANSACTION_READ_UNCOMMITTED' alle drei Fehlerarten (Dirty Reads, Non-repeatable Reads und Phantom Reads) auftreten und bei 'TRANSACTION_SERIALIZABLE' keine mehr.
Eine Java-EE-Webanwendungen mit EJBs (Enterprise JavaBeans) kann typischerweise etwa folgende vereinfachte Struktur haben (genaueres hierzu finden Sie unter sw-patterns.htm#JavaEE-Patterns):
Client | |HTTP ............................. | ............................. . | Web Container. . Front Controller . . | . . Business Delegate . ............................. | ............................. |RMI-IIOP ............................. | ............................. . | EJB Container. . Session Facade (Session EJB) . . ____________________|____________________ . . | | | | . . Entity EJB Entity EJB Entity EJB MDB . . | | | | . ........ | ........... | ........... | ........... | ........ | | | | SQL-Datenbank ERP, Legacy JMS
Java-EE-Transaktionen werden fast nie von Entity Beans, sondern meistens von Session Beans verwaltet (oder von externen Komponenten). Java-EE-Transaktionen können nicht nur einzelne Aktionen beinhalten, sondern auch mehrere RMI-Aufrufe, mehrere Datenbankoperationen und mehrere JMS-Nachrichten umfassen. Unter bestimmten Voraussetzungen können auch JCA-Konnektoren an Transaktionen teilnehmen.
Die Transaktionsklammer wird üblicherweise in der Session EJB in der Session Facade gesetzt.
Bei der Transaktionssteuerung wird unterschieden zwischen:
Bei CMT (Container Managed Transactions) (mit '<transaction-type>Container</transaction-type>' in der 'ejb-jar.xml') bilden in der Regel die Remote-Methoden der Session-Beans eine Transaktionseinheit.
Bei Container Managed Transactions übernimmt der EJB-Container die Verwaltung der Transaktionen. Es brauchen keine Begin-, Commit- und Rollback-Kommandos programmiert zu werden. In der 'ejb-jar.xml' wird deklarativ eingestellt, ob alle oder welche Methoden transaktionsgesteuert werden sollen und mit welchem '<trans-attribute>' (meistens 'Required').
In Session Beans kann ein Rollback auch explizit ausgelöst werden. Dabei muss beachtet werden, dass bei CMT nicht 'ctx.getUserTransaction().setRollbackOnly()', sondern 'ctx.setRollbackOnly()' aufgerufen werden muss (wie weiter unten das Beispiel 'CmtSbImpl.java' zeigt).
Das folgende Beispiel demonstriert einen Rollback in einer CMT-Session-Bean unter JBoss zusammen mit MySQL.
MySQL
Installieren Sie MySQL wie oben beschrieben (inklusive 'InnoDB'-Engine).
JBoss
Installieren Sie JBoss wie beschrieben unter jee-ejb2.htm#InstallationJBoss.
Projektverzeichnis, Lib, build.xml, jndi.properties, log4j.properties
Legen Sie ein Projektverzeichnis an, z.B. 'D:\MeinWorkspace\MeinEjbProjekt' (im Folgenden '<MeinEjbProjekt>' genannt).
Legen Sie im Projektverzeichnis '<MeinEjbProjekt>' das Unterverzeichnis 'lib' sowie das Unterverzeichnis 'src' und darin die Unterverzeichnisse 'src\meinclient' und 'src\meintransactiontest' an:
[\MeinWorkspace] '- [MeinEjbProjekt] |- [lib] | '- jbossall-client.jar |- [src] | |- [meinclient] | '- [meintransactiontest] |- build.xml |- jndi.properties '- log4j.properties
Kopieren Sie in das Unterverzeichnis '<MeinEjbProjekt>\lib' die zum Java EE Application Server gehörende .jar-Library. Für JBoss ist dies die 'jbossall-client.jar' aus dem JBoss-'client'-Verzeichnis (z.B. 'C:\JBoss\client'). Für andere Java EE Application Server siehe: jee-jndi.htm#AppClient-Lib.
Speichern Sie im Verzeichnis '<MeinEjbProjekt>' die drei Dateien 'build.xml', 'jndi.properties' und 'log4j.properties'.
Container Managed Persistence Entity Bean Home Interface
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmpEbHomeIF.java':
package meintransactiontest; import java.rmi.RemoteException; import javax.ejb.*; public interface CmpEbHomeIF extends EJBHome { public CmpEbIF create() throws RemoteException, CreateException; public CmpEbIF findByPrimaryKey( Object oid ) throws RemoteException, FinderException; }
Container Managed Persistence Entity Bean Remote Interface
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmpEbIF.java':
package meintransactiontest; import java.rmi.RemoteException; import javax.ejb.EJBObject; public interface CmpEbIF extends EJBObject { String getEntityValue() throws RemoteException; void setEntityValue( String entityValue ) throws RemoteException; }
Container Managed Persistence Entity Bean Implementation
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmpEbImpl.java':
package meintransactiontest; import javax.ejb.*; public abstract class CmpEbImpl implements EntityBean { private EntityContext ctx; public abstract String getEntityValue(); public abstract void setEntityValue( String entityValue ); public Object ejbCreate() throws CreateException { return null; } public void ejbPostCreate() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbLoad() {} public void ejbStore() {} public void ejbRemove() {} public void setEntityContext( EntityContext entityContext ) { ctx = entityContext; } public void unsetEntityContext() { ctx = null; } }
Container Managed Transaction Session Bean Home Interface
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmtSbHomeIF.java':
package meintransactiontest; import java.rmi.RemoteException; import javax.ejb.*; public interface CmtSbHomeIF extends EJBHome { public CmtSbIF create() throws RemoteException, CreateException; }
Container Managed Transaction Session Bean Remote Interface
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmtSbIF.java':
package meintransactiontest; import java.rmi.RemoteException; import javax.ejb.*; public interface CmtSbIF extends EJBObject { void insertTestEntry() throws RemoteException, CreateException; void testRollback( boolean rollback ) throws RemoteException, FinderException; String showEntityValue( String s ) throws RemoteException, FinderException; }
Container Managed Transaction Session Bean Implementation
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'CmtSbImpl.java':
package meintransactiontest; import java.rmi.RemoteException; import java.text.SimpleDateFormat; import java.util.Date; import javax.ejb.*; import javax.rmi.PortableRemoteObject; public class CmtSbImpl implements SessionBean { private static final SimpleDateFormat DF = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss.SSS" ); private SessionContext ctx; private CmpEbHomeIF cmpEbHome; private Object pk1; public void setSessionContext( SessionContext sessionContext ) { ctx = sessionContext; } public void insertTestEntry() throws RemoteException, CreateException { Object cmpEbRef = ctx.lookup( "CmpEb" ); // "java:comp/env/ejb/CmpEb" cmpEbHome = (CmpEbHomeIF) PortableRemoteObject.narrow( cmpEbRef, CmpEbHomeIF.class ); CmpEbIF cmpEb = cmpEbHome.create(); pk1 = cmpEb.getPrimaryKey(); cmpEb.setEntityValue( "Urspruenglicher Wert" ); } public void testRollback( boolean rollback ) throws RemoteException, FinderException { CmpEbIF cmpEb = cmpEbHome.findByPrimaryKey( pk1 ); cmpEb.setEntityValue( "Geaendert: " + DF.format( new Date() ) ); if( rollback ) ctx.setRollbackOnly(); } public String showEntityValue( String s ) throws RemoteException, FinderException { if( null == s ) s = ""; CmpEbIF cmpEb = cmpEbHome.findByPrimaryKey( pk1 ); s += ":\n" + cmpEb.getEntityValue(); return s; } public void ejbCreate() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbRemove() {} }
Standard EJB Deployment-Descriptor
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'ejb-jar.xml':
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd"> <ejb-jar> <enterprise-beans> <session> <ejb-name> CmtSb </ejb-name> <home> meintransactiontest.CmtSbHomeIF </home> <remote> meintransactiontest.CmtSbIF </remote> <ejb-class> meintransactiontest.CmtSbImpl </ejb-class> <session-type> Stateless </session-type> <transaction-type> Container </transaction-type> </session> <entity> <display-name> CmpEb </display-name> <ejb-name> CmpEb </ejb-name> <home> meintransactiontest.CmpEbHomeIF </home> <remote> meintransactiontest.CmpEbIF </remote> <ejb-class> meintransactiontest.CmpEbImpl </ejb-class> <persistence-type> Container </persistence-type> <prim-key-class> java.lang.Object </prim-key-class> <reentrant> false </reentrant> <cmp-version> 2.x </cmp-version> <abstract-schema-name> CmpEb </abstract-schema-name> <cmp-field><field-name> entityValue </field-name></cmp-field> <resource-ref> <res-ref-name> jdbc/MeinDatasourceJndiName </res-ref-name> <res-type> javax.sql.DataSource </res-type> <res-auth> Container </res-auth> </resource-ref> </entity> </enterprise-beans> <assembly-descriptor> <container-transaction> <method> <ejb-name> CmtSb </ejb-name> <method-name> * </method-name> </method> <trans-attribute> Required </trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
JBoss JDBC Deployment-Descriptor
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meintransactiontest' die folgende Datei 'jbosscmp-jdbc.xml':
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE jbosscmp-jdbc PUBLIC "-//JBoss//DTD JBOSSCMP-JDBC 4.0//EN" "http://www.jboss.org/j2ee/dtd/jbosscmp-jdbc_4_0.dtd"> <jbosscmp-jdbc> <defaults> <datasource> java:/MeinDatasourceJndiName </datasource> <datasource-mapping> mySQL </datasource-mapping> <create-table> true </create-table> <remove-table> true </remove-table> <unknown-pk> <unknown-pk-class> java.lang.Integer </unknown-pk-class> <field-name> oid </field-name> <column-name> oid </column-name> <jdbc-type> INTEGER </jdbc-type> <sql-type> INT(11) </sql-type> <auto-increment /> </unknown-pk> <entity-command name="mysql-get-generated-keys" /> </defaults> </jbosscmp-jdbc>
Connector Service
Registrieren Sie in JBoss (bei gestopptem JBoss) einen geeigneten Connector Service, indem Sie aus dem '<JBOSS_HOME>\docs\examples\jca'-Verzeichnis die passende '...-ds.xml'-Datei, also für die MySQL-Datenbank die 'mysql-ds.xml'-Datei, in das '<JBOSS_HOME>\server\default\deploy'-Verzeichnis kopieren und zum Beispiel folgendermaßen anpassen:
<?xml version="1.0" encoding="UTF-8"?> <datasources> <local-tx-datasource> <jndi-name>MeinDatasourceJndiName</jndi-name> <connection-url>jdbc:mysql://localhost:3306/MeineDb</connection-url> <driver-class>com.mysql.jdbc.Driver</driver-class> <user-name>root</user-name> <password>mysqlpwd</password> <exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter</exception-sorter-class-name> <metadata> <type-mapping>mySQL</type-mapping> </metadata> </local-tx-datasource> </datasources>
Passen Sie die Einträge an Ihre Datenbankverbindung an.
Test-Client
Speichern Sie im Verzeichnis '<MeinEjbProjekt>\src\meinclient' die folgende Datei 'CmtTestApp.java':
package meinclient; import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import meintransactiontest.CmtSbHomeIF; import meintransactiontest.CmtSbIF; public class CmtTestApp { public static void main( String[] args ) throws Exception { Context ctx = new InitialContext(); Object ref = ctx.lookup( "CmtSb" ); CmtSbHomeIF home = (CmtSbHomeIF) PortableRemoteObject.narrow( ref, CmtSbHomeIF.class ); CmtSbIF cmtSb = home.create(); cmtSb.insertTestEntry(); System.out.println( cmtSb.showEntityValue( "Nach insertTestEntry()" ) ); cmtSb.testRollback( true ); System.out.println( cmtSb.showEntityValue( "Nach testRollback(true)" ) ); cmtSb.testRollback( false ); System.out.println( cmtSb.showEntityValue( "Nach testRollback(false)" ) ); } }
Verzeichnisstruktur
Die Verzeichnisstruktur muss jetzt in etwa folgendermaßen aussehen:
[\MeinWorkspace] '- [MeinEjbProjekt] |- [bin] | '- ... |- [lib] | '- jbossall-client.jar |- [src] | |- [meinclient] | | '- CmtTestApp.java | '- [meintransactiontest] | |- CmpEbHomeIF.java | |- CmpEbIF.java | |- CmpEbImpl.java | |- CmtSbHomeIF.java | |- CmtSbIF.java | |- CmtSbImpl.java | |- ejb-jar.xml | '- jbosscmp-jdbc.xml |- build.xml |- jndi.properties '- log4j.properties
Wichtig: Im '<MeinEjbProjekt>'-Verzeichnis müssen sich die Dateien 'build.xml', 'jndi.properties' und 'log4j.properties' befinden, im '<MeinEjbProjekt>\lib'-Verzeichnis die 'jbossall-client.jar' und im '<JBOSS_HOME>\server\default\deploy'-Verzeichnis eine geeignete 'mysql-ds.xml'-Datei. .
Aufruf
Bei laufendem JBoss öffnen Sie ein Kommandozeilenfenster und geben die folgenden Kommandos ein:
cd \MeinWorkspace\MeinEjbProjekt
ant -Dclntapp=meinclient.CmtTestApp -Dsrvjar=CmtTest.jar -Dsrvpkg=meintransactiontest
Eclipse
Falls Sie das Projekt in
Eclipse
bearbeiten wollen, gehen Sie vor wie
hier beschrieben.
Falls Sie außerhalb von Eclipse Dateien verändert oder hinzugefügt haben,
müssen Sie in Eclipse die entsprechenden Projekte im 'Package Explorer' markieren und mit 'F5' einen 'Refresh' durchführen.
Außerdem müssen Sie für dieses Beispiel die build.xml-Properties anders einstellen:
'clntapp=meinclient.CmtTestApp', 'srvjar=CmtTest.jar' und 'srvpkg=meintransactiontest'.
Transaktionsattribut 'Required'
Ersetzen Sie testweise das Transaktionsattribut 'Required' in der 'ejb-jar.xml' durch andere Werte ('NotSupported', 'Supports', 'Required', 'RequiresNew', 'Mandatory', 'Never'), zum Beispiel so:
<trans-attribute> NotSupported </trans-attribute>
statt
<trans-attribute> Required </trans-attribute>
und sehen Sie sich die Ergebnisse bzw. Fehlermeldungen an.
JBoss Management Web Console
Um einige Statistikinformationen zu erhalten, öffnen Sie die JBoss Management Web Console über http://localhost:8080/web-console. Doppelklicken Sie auf 'J2EE Domains' | 'Manager' | 'JBoss' | 'CmtTest.jar' und 'CmtSb' bzw. 'CmpEb'.
Bei BMT (Bean Managed Transactions) (mit '<transaction-type>Bean</transaction-type>' in der 'ejb-jar.xml') werden Transaktionen von der Session-Bean über das JTA (Java Transaction API) gestartet und beendet.
Dies kann zum Beispiel folgendermaßen aussehen:
import javax.ejb.*; import javax.transaction.*; public class MyTransactionEjb implements SessionBean { private SessionContext ctx = null; public void setSessionContext( SessionContext sessionContext ) { ctx = sessionContext; } public void doSomething() throws SystemException { UserTransaction utx = ctx.getUserTransaction(); int initialTxStat = utx.getStatus(); beginnTransactionIfRequired( utx, initialTxStat ); try { // ... // ... call EJBs, JMS, JDBC, XA-compliant-JCA, ... // ... } catch( Exception ex ) { utx.setRollbackOnly(); } completeTransactionIfRequired( utx, initialTxStat ); } void beginnTransactionIfRequired( UserTransaction utx, int initialTxStat ) { switch( initialTxStat ) { case Status.STATUS_ACTIVE: break; case Status.STATUS_NO_TRANSACTION: try { utx.begin(); } catch( Exception ex ) { throw new EJBException( "Error at Transaction Beginn." ); } break; default: throw new EJBException( "Unexpected Transaction Status." ); } } void completeTransactionIfRequired( UserTransaction utx, int initialTxStat ) { if( Status.STATUS_NO_TRANSACTION == initialTxStat ) { try { if( Status.STATUS_MARKED_ROLLBACK == utx.getStatus() ) utx.rollback(); else utx.commit(); } catch( Exception ex ) { throw new EJBException( "Error at Transaction Completion." ); } } } public void ejbCreate() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbRemove() {} }
Programmgerüst für Transaktions-Client
Ein Programmgerüst für eine von einem Client außerhalb des EJB-Containers gesteuerte JTA-Transaktion könnte zum Beispiel so aussehen:
import javax.naming.*; import javax.transaction.*; public class MyTransactionClient1 { public static void main( String[] args ) throws Exception { Context ctx = new InitialContext(); // Je nach Application Server: // "java:comp/UserTransaction" bzw. // "java:comp/TransactionManager" bzw. // "java:/TransactionManager" bzw. // "javax.transaction.UserTransaction": UserTransaction utx = (UserTransaction) ctx.lookup( "/UserTransaction" ); utx.begin(); try { // ... // ... call EJBs, JMS, JDBC, XA-compliant-JCA, ... // ... if( Status.STATUS_MARKED_ROLLBACK != utx.getStatus() ) utx.commit(); else utx.rollback(); } catch( Exception ex ) { utx.rollback(); throw ex; } } }
Beispiel für einen Client, der von extern das JTA/JTS und eine DataSource eines Java EE Application Servers verwendet
Die Verwendung von DataSourcen unter JTA könnte mit Oracle WebLogic zum Beispiel so aussehen (mit der Lib: weblogic.jar) (passen Sie 'meinAppServer...' und 'MeineTabelle' an) (zu JNDI-Properties siehe auch: jee-jndi.htm#jndi.properties):
import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; import javax.naming.Context; import javax.naming.InitialContext; import javax.sql.DataSource; import javax.transaction.Status; import javax.transaction.UserTransaction; public class MyTransactionClient2 { public static void main( String[] args ) throws Exception { Properties prp = new Properties(); prp.put( Context.PROVIDER_URL, "t3://meinAppServer:7001" ); prp.put( Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory" ); prp.put( Context.SECURITY_PRINCIPAL, "meinAppSrvBenutzername" ); prp.put( Context.SECURITY_CREDENTIALS, "meinAppSrvKennwort" ); InitialContext ctx = new InitialContext( prp ); UserTransaction utx = (UserTransaction) ctx.lookup( "javax.transaction.UserTransaction" ); DataSource ds = (DataSource) ctx.lookup( "jdbc/MeinDatasourceJndiName" ); Connection cn = null; Statement st = null; ResultSet rs = null; utx.begin(); try { cn = ds.getConnection(); st = cn.createStatement(); rs = st.executeQuery( "Select * from MeineTabelle" ); if( rs.next() ) System.out.println( "Ergebnis: " + rs.getString( 1 ) ); System.out.println( cn.getMetaData().getDatabaseProductName() ); System.out.println( cn.getMetaData().getDatabaseProductVersion() ); if( Status.STATUS_MARKED_ROLLBACK != utx.getStatus() ) { utx.commit(); } else { utx.rollback(); } } catch( Exception ex ) { utx.rollback(); throw ex; } finally { try { if( null != rs ) rs.close(); } catch( Exception ex ) {/*ok*/} try { if( null != st ) st.close(); } catch( Exception ex ) {/*ok*/} try { if( null != cn ) cn.close(); } catch( Exception ex ) {/*ok*/} } } }
Trace der XA-Two-Phase-Commit-Kommunikation
Falls Sie Probleme im Zusammenhang mit XA-Two-Phase-Commit analysieren wollen: Aktivieren Sie Trace-Ausgaben sowohl des JTA/JTS als auch des Ressourcen-Treibers (z.B. XA-DataSource im JDBC-Treiber).
Zum Beispiel für Oracle WebLogic schalten Sie Tracing für JTA/JTS ein, indem Sie dem Startkommando folgende Kommandozeilenoption hinzufügen (z.B. in startWeblogic.cmd):
-Dweblogic.Debug=weblogic.JDBCConn,weblogic.JDBCSQL,weblogic.JTA2PC,weblogic.JTAXA,weblogic.JTAJDBC
Zum Beispiel für den AS/400-DB2-JDBC-Treiber jt400-5.4.0.2.jar schalten Sie Tracing ein, indem Sie "Trace=true" und einen geeigneten "server trace"-Level setzen (wie unter JDBC server trace property beschrieben ist).
Die Tracing-Ausgaben können entweder auf der Application-Server-Konsole oder in der Application-Server-Log-Datei erscheinen.
Die XA Specification der Open Group finden Sie unter http://www.opengroup.org/bookstore/catalog/c193.htm.
Für Diagnose- oder Testzwecke kann es sinnvoll sein, das XA-Two-Phase-Commit-Protokoll über direkte Kommandos zum Ressourcenmanager statt über den JTA/JTS-Transaktionsmanager anzusteuern. Hierzu könnte ein Programmgerüst folgendermaßen aussehen:
import java.sql.Connection; import javax.sql.XAConnection; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import com.ibm.as400.access.AS400JDBCXADataSource; // jt400-5.4.0.2.jar fuer DB2/400 auf AS/400 V5R4 public class JdbcXaDataSourceTest { public static void main( String[] args ) throws Exception { String url = "meinDbServer", usr = "meinDbBenutzername", pwd = "meinDbKennwort"; // Je nach Datenbank andere XADataSource einsetzen // (hier fuer DB2/400 auf AS/400 V5R4): AS400JDBCXADataSource xaDataSource = new AS400JDBCXADataSource( url ); xaDataSource.setUser( usr ); xaDataSource.setPassword( pwd ); XAConnection xaConnection = xaDataSource.getXAConnection(); XAResource xaResource = xaConnection.getXAResource(); Connection connection = xaConnection.getConnection(); Xid xid = new XidImpl( 100, new byte[]{0x0A}, new byte[]{0x03} ); xaResource.start( xid, XAResource.TMNOFLAGS ); System.out.println( connection.getMetaData().getDatabaseProductName() ); System.out.println( connection.getMetaData().getDatabaseProductVersion() ); // connection.createStatement().executeQuery( "..." ); xaResource.end( xid, XAResource.TMSUCCESS ); if( xaResource.prepare( xid ) == XAResource.XA_OK ) xaResource.commit( xid, false ); xaConnection.close(); } } // Entweder folgende XidImpl oder alternativ auch XidImpl vom Application Server, z.B.: // weblogic.transaction.internal.XidImpl // org.jboss.tm.XidImpl class XidImpl implements Xid { protected int formatId; protected byte[] gtrid; protected byte[] bqual; public XidImpl( int formatId, byte gtrid[], byte bqual[] ) { this.formatId = formatId; this.gtrid = gtrid; this.bqual = bqual; } public int getFormatId() { return formatId; } public byte[] getGlobalTransactionId() { return gtrid; } public byte[] getBranchQualifier() { return bqual; } }
Die XA Specification der Open Group finden Sie unter http://www.opengroup.org/bookstore/catalog/c193.htm.
Die Javadoc zu den Java-Interfaces finden Sie unter XADataSource, XAConnection, XAResource und Xid.
Die Javadoc zum im Beispiel verwendeten XA-Ressourcenmanager zur DB2/400 auf AS/400 V5R4 finden Sie unter http://publib.boulder.ibm.com/infocenter/iseries/v5r4/index.jsp?topic=/rzahh/javadoc/com/ibm/as400/access/AS400JDBCXAResource.html.
Siehe:
JPA mit Java SE und "EntityTransaction",
JPA-Servlet-Filter mit "EntityTransaction",
JPA-Servlet-Filter mit "JTA Transaction",
JPA mit EJB 3 und "JTA Transaction",
JPA in JSP-Webanwendung mit geteilter Transaktion,
Optimistic Locking bei geteilter Transaktion (z.B. in Webanwendung).
... siehe unter java-hibernate.htm#Transaktionen.
... siehe unter jee-spring-db-tx.htm.