From ef093b2c6cb7b334d91512af3214f67eb9828978 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Sat, 17 Jan 2015 13:28:34 +1300 Subject: [PATCH] Fix for #232 - New ebean leaves connections with active transactions --- .../config/dbplatform/DatabasePlatform.java | 815 +++---- .../config/dbplatform/Oracle10Platform.java | 135 +- .../config/dbplatform/PostgresPlatform.java | 158 +- .../server/transaction/JdbcTransaction.java | 1912 ++++++++--------- .../transaction/TransactionManager.java | 887 ++++---- 5 files changed, 1967 insertions(+), 1940 deletions(-) diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java index 7885bfda0..d1516895a 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -1,388 +1,427 @@ -package com.avaje.ebean.config.dbplatform; - -import java.sql.Types; - -import javax.sql.DataSource; - -import com.avaje.ebean.BackgroundExecutor; -import com.avaje.ebean.Query; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Database platform specific settings. - */ -public class DatabasePlatform { - - private static final Logger logger = LoggerFactory.getLogger(DatabasePlatform.class); - - /** - * The open quote used by quoted identifiers. - */ - protected String openQuote = "\""; - - /** - * The close quote used by quoted identifiers. - */ - protected String closeQuote = "\""; - - /** - * For limit/offset, row_number etc limiting of SQL queries. - */ - protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter(); - - /** - * Mapping of JDBC to Database types. - */ - protected DbTypeMap dbTypeMap = new DbTypeMap(); - - /** - * DB specific DDL syntax. - */ - protected DbDdlSyntax dbDdlSyntax = new DbDdlSyntax(); - - /** - * Defines DB identity/sequence features. - */ - protected DbIdentity dbIdentity = new DbIdentity(); - - /** - * The JDBC type to map booleans to (by default). - */ - protected int booleanDbType = Types.BOOLEAN; - - /** - * The JDBC type to map Blob to. - */ - protected int blobDbType = Types.BLOB; - - /** - * The JDBC type to map Clob to. - */ - protected int clobDbType = Types.CLOB; - - /** - * For Oracle treat empty strings as null. - */ - protected boolean treatEmptyStringsAsNull; - - /** - * The database platform name. - */ - protected String name = "generic"; - - protected String columnAliasPrefix = "c"; - - protected String tableAliasPlaceHolder = "${ta}"; - - /** - * Use a BackTick ` at the beginning and end of table or column names that you - * want to use quoted identifiers for. The backticks get converted to the - * appropriate characters in convertQuotedIdentifiers - */ - private static final char BACK_TICK = '`'; - - /** - * The like clause. Can be overridden to disable default escape character. - */ - protected String likeClause = "like ?"; - - protected DbEncrypt dbEncrypt; - - protected boolean idInExpandedForm; - - protected boolean selectCountWithAlias; - - /** - * If set then use the FORWARD ONLY hint when creating ResultSets for - * findIterate() and findVisit(). - */ - protected boolean forwardOnlyHintOnFindIterate; - - /** - * Flag set for SQL Server due to lack of support of getGeneratedKeys in - * batch mode (meaning for batch inserts you should explicitly turn off - * getGeneratedKeys - joy). - */ - protected boolean disallowBatchOnCascade; - - /** - * Instantiates a new database platform. - */ - public DatabasePlatform() { - } - - /** - * Return the name of the DatabasePlatform. - *

- * "generic" is returned when no specific database platform has been set or - * found. - *

- * - * @return the name - */ - public String getName() { - return name; - } - - /** - * Return a DB Sequence based IdGenerator. - * - * @param be - * the BackgroundExecutor that can be used to load the sequence if - * desired - * @param ds - * the DataSource - * @param seqName - * the name of the sequence - * @param batchSize - * the number of sequences that should be loaded - */ - public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, - String seqName, int batchSize) { - - return null; - } - - /** - * Return the DbEncrypt handler for this DB platform. - */ - public DbEncrypt getDbEncrypt() { - return dbEncrypt; - } - - /** - * Set the DbEncrypt handler for this DB platform. - */ - public void setDbEncrypt(DbEncrypt dbEncrypt) { - this.dbEncrypt = dbEncrypt; - } - - /** - * Return the mapping of JDBC to DB types. - * - * @return the db type map - */ - public DbTypeMap getDbTypeMap() { - return dbTypeMap; - } - - /** - * Return the DDL syntax for this platform. - * - * @return the db ddl syntax - */ - public DbDdlSyntax getDbDdlSyntax() { - return dbDdlSyntax; - } - - /** - * Return the column alias prefix. - */ - public String getColumnAliasPrefix() { - return columnAliasPrefix; - } - - /** - * Set the column alias prefix. - */ - public void setColumnAliasPrefix(String columnAliasPrefix) { - this.columnAliasPrefix = columnAliasPrefix; - } - - /** - * Return the table alias placeholder. - */ - public String getTableAliasPlaceHolder() { - return tableAliasPlaceHolder; - } - - /** - * Set the table alias placeholder. - */ - public void setTableAliasPlaceHolder(String tableAliasPlaceHolder) { - this.tableAliasPlaceHolder = tableAliasPlaceHolder; - } - - /** - * Return the close quote for quoted identifiers. - * - * @return the close quote - */ - public String getCloseQuote() { - return closeQuote; - } - - /** - * Return the open quote for quoted identifiers. - * - * @return the open quote - */ - public String getOpenQuote() { - return openQuote; - } - - /** - * Return the JDBC type used to store booleans. - * - * @return the boolean db type - */ - public int getBooleanDbType() { - return booleanDbType; - } - - /** - * Return the data type that should be used for Blob. - *

- * This is typically Types.BLOB but for Postgres is Types.LONGVARBINARY for - * example. - *

- */ - public int getBlobDbType() { - return blobDbType; - } - - /** - * Return the data type that should be used for Clob. - *

- * This is typically Types.CLOB but for Postgres is Types.VARCHAR. - *

- */ - public int getClobDbType() { - return clobDbType; - } - - /** - * Return true if empty strings should be treated as null. - * - * @return true, if checks if is treat empty strings as null - */ - public boolean isTreatEmptyStringsAsNull() { - return treatEmptyStringsAsNull; - } - - /** - * Return true if a compound ID in (...) type expression needs to be in - * expanded form of (a=? and b=?) or (a=? and b=?) or ... rather than (a,b) in - * ((?,?),(?,?),...); - */ - public boolean isIdInExpandedForm() { - return idInExpandedForm; - } - - /** - * Return true if the ResultSet TYPE_FORWARD_ONLY Hint should be used on - * findIterate() and findVisit() PreparedStatements. - *

- * This specifically is required for MySql when processing large results. - *

- */ - public boolean isForwardOnlyHintOnFindIterate() { - return forwardOnlyHintOnFindIterate; - } - - /** - * Set to true if the ResultSet TYPE_FORWARD_ONLY Hint should be used by default on findIterate PreparedStatements. - */ - public void setForwardOnlyHintOnFindIterate(boolean forwardOnlyHintOnFindIterate) { - this.forwardOnlyHintOnFindIterate = forwardOnlyHintOnFindIterate; - } - - /** - * Return the DB identity/sequence features for this platform. - * - * @return the db identity - */ - public DbIdentity getDbIdentity() { - return dbIdentity; - } - - /** - * Return the SqlLimiter used to apply additional sql around a query to limit - * its results. - *

- * Basically add the clauses for limit/offset, rownum, row_number(). - *

- * - * @return the sql limiter - */ - public SqlLimiter getSqlLimiter() { - return sqlLimiter; - } - - /** - * Convert backticks to the platform specific open quote and close quote - * - *

- * Specific plugins may implement this method to cater for platform specific - * naming rules. - *

- * - * @param dbName - * the db name - * - * @return the string - */ - public String convertQuotedIdentifiers(String dbName) { - // Ignore null values e.g. schema name or catalog - if (dbName != null && dbName.length() > 0) { - if (dbName.charAt(0) == BACK_TICK) { - if (dbName.charAt(dbName.length() - 1) == BACK_TICK) { - - String quotedName = getOpenQuote(); - quotedName += dbName.substring(1, dbName.length() - 1); - quotedName += getCloseQuote(); - - return quotedName; - - } else { - logger.error("Missing backquote on [" + dbName + "]"); - } - } - } - return dbName; - } - - /** - * Set to true if select count against anonymous view requires an alias. - */ - public boolean isSelectCountWithAlias() { - return selectCountWithAlias; - } - - public String completeSql(String sql, Query query) { - if (Boolean.TRUE.equals(query.isForUpdate())) { - sql = withForUpdate(sql); - } - - return sql; - } - - protected String withForUpdate(String sql) { - // silently assume the database does not support the "for update" clause. - logger.info("it seems your database does not support the 'for update' clause"); - - return sql; - } - - /** - * Returns the like clause used by this database platform. - *

- * This may include an escape clause to disable a default escape character. - */ - public String getLikeClause() { - return likeClause; - } - - /** - * Return true if the persistBatchOnCascade setting should be ignored. - *

- * This is primarily for SQL Server which does not support getGeneratedKeys with jdbc batch mode - * so can't really be transparently used. - *

- */ - public boolean isDisallowBatchOnCascade() { - return disallowBatchOnCascade; - } -} +package com.avaje.ebean.config.dbplatform; + +import java.sql.Types; + +import javax.sql.DataSource; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.Query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Database platform specific settings. + */ +public class DatabasePlatform { + + private static final Logger logger = LoggerFactory.getLogger(DatabasePlatform.class); + + /** + * Behavior used when ending a query only transaction (at read committed isolation level). + */ + public enum OnQueryOnly { + + /** + * Rollback the transaction. + */ + ROLLBACK, + + /** + * Just close the transaction. Valid at READ_COMMITTED isolation and preferred on some Databases + * as a performance optimisation. + */ + CLOSE, + + /** + * Commit the transaction + */ + COMMIT + } + + /** + * The behaviour used when ending a read only transaction at read committed isolation level. + */ + protected OnQueryOnly onQueryOnly = OnQueryOnly.ROLLBACK; + + /** + * The open quote used by quoted identifiers. + */ + protected String openQuote = "\""; + + /** + * The close quote used by quoted identifiers. + */ + protected String closeQuote = "\""; + + /** + * For limit/offset, row_number etc limiting of SQL queries. + */ + protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter(); + + /** + * Mapping of JDBC to Database types. + */ + protected DbTypeMap dbTypeMap = new DbTypeMap(); + + /** + * DB specific DDL syntax. + */ + protected DbDdlSyntax dbDdlSyntax = new DbDdlSyntax(); + + /** + * Defines DB identity/sequence features. + */ + protected DbIdentity dbIdentity = new DbIdentity(); + + /** + * The JDBC type to map booleans to (by default). + */ + protected int booleanDbType = Types.BOOLEAN; + + /** + * The JDBC type to map Blob to. + */ + protected int blobDbType = Types.BLOB; + + /** + * The JDBC type to map Clob to. + */ + protected int clobDbType = Types.CLOB; + + /** + * For Oracle treat empty strings as null. + */ + protected boolean treatEmptyStringsAsNull; + + /** + * The database platform name. + */ + protected String name = "generic"; + + protected String columnAliasPrefix = "c"; + + protected String tableAliasPlaceHolder = "${ta}"; + + /** + * Use a BackTick ` at the beginning and end of table or column names that you + * want to use quoted identifiers for. The backticks get converted to the + * appropriate characters in convertQuotedIdentifiers + */ + private static final char BACK_TICK = '`'; + + /** + * The like clause. Can be overridden to disable default escape character. + */ + protected String likeClause = "like ?"; + + protected DbEncrypt dbEncrypt; + + protected boolean idInExpandedForm; + + protected boolean selectCountWithAlias; + + /** + * If set then use the FORWARD ONLY hint when creating ResultSets for + * findIterate() and findVisit(). + */ + protected boolean forwardOnlyHintOnFindIterate; + + /** + * Flag set for SQL Server due to lack of support of getGeneratedKeys in + * batch mode (meaning for batch inserts you should explicitly turn off + * getGeneratedKeys - joy). + */ + protected boolean disallowBatchOnCascade; + + /** + * Instantiates a new database platform. + */ + public DatabasePlatform() { + } + + /** + * Return the name of the DatabasePlatform. + *

+ * "generic" is returned when no specific database platform has been set or + * found. + *

+ * + * @return the name + */ + public String getName() { + return name; + } + + /** + * Return a DB Sequence based IdGenerator. + * + * @param be + * the BackgroundExecutor that can be used to load the sequence if + * desired + * @param ds + * the DataSource + * @param seqName + * the name of the sequence + * @param batchSize + * the number of sequences that should be loaded + */ + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, String seqName, int batchSize) { + return null; + } + + /** + * Return the behaviour to use when ending a read only transaction. + */ + public OnQueryOnly getOnQueryOnly() { + return onQueryOnly; + } + + /** + * Set the behaviour to use when ending a read only transaction. + */ + public void setOnQueryOnly(OnQueryOnly onQueryOnly) { + this.onQueryOnly = onQueryOnly; + } + + /** + * Return the DbEncrypt handler for this DB platform. + */ + public DbEncrypt getDbEncrypt() { + return dbEncrypt; + } + + /** + * Set the DbEncrypt handler for this DB platform. + */ + public void setDbEncrypt(DbEncrypt dbEncrypt) { + this.dbEncrypt = dbEncrypt; + } + + /** + * Return the mapping of JDBC to DB types. + * + * @return the db type map + */ + public DbTypeMap getDbTypeMap() { + return dbTypeMap; + } + + /** + * Return the DDL syntax for this platform. + * + * @return the db ddl syntax + */ + public DbDdlSyntax getDbDdlSyntax() { + return dbDdlSyntax; + } + + /** + * Return the column alias prefix. + */ + public String getColumnAliasPrefix() { + return columnAliasPrefix; + } + + /** + * Set the column alias prefix. + */ + public void setColumnAliasPrefix(String columnAliasPrefix) { + this.columnAliasPrefix = columnAliasPrefix; + } + + /** + * Return the table alias placeholder. + */ + public String getTableAliasPlaceHolder() { + return tableAliasPlaceHolder; + } + + /** + * Set the table alias placeholder. + */ + public void setTableAliasPlaceHolder(String tableAliasPlaceHolder) { + this.tableAliasPlaceHolder = tableAliasPlaceHolder; + } + + /** + * Return the close quote for quoted identifiers. + * + * @return the close quote + */ + public String getCloseQuote() { + return closeQuote; + } + + /** + * Return the open quote for quoted identifiers. + * + * @return the open quote + */ + public String getOpenQuote() { + return openQuote; + } + + /** + * Return the JDBC type used to store booleans. + * + * @return the boolean db type + */ + public int getBooleanDbType() { + return booleanDbType; + } + + /** + * Return the data type that should be used for Blob. + *

+ * This is typically Types.BLOB but for Postgres is Types.LONGVARBINARY for + * example. + *

+ */ + public int getBlobDbType() { + return blobDbType; + } + + /** + * Return the data type that should be used for Clob. + *

+ * This is typically Types.CLOB but for Postgres is Types.VARCHAR. + *

+ */ + public int getClobDbType() { + return clobDbType; + } + + /** + * Return true if empty strings should be treated as null. + * + * @return true, if checks if is treat empty strings as null + */ + public boolean isTreatEmptyStringsAsNull() { + return treatEmptyStringsAsNull; + } + + /** + * Return true if a compound ID in (...) type expression needs to be in + * expanded form of (a=? and b=?) or (a=? and b=?) or ... rather than (a,b) in + * ((?,?),(?,?),...); + */ + public boolean isIdInExpandedForm() { + return idInExpandedForm; + } + + /** + * Return true if the ResultSet TYPE_FORWARD_ONLY Hint should be used on + * findIterate() and findVisit() PreparedStatements. + *

+ * This specifically is required for MySql when processing large results. + *

+ */ + public boolean isForwardOnlyHintOnFindIterate() { + return forwardOnlyHintOnFindIterate; + } + + /** + * Set to true if the ResultSet TYPE_FORWARD_ONLY Hint should be used by default on findIterate PreparedStatements. + */ + public void setForwardOnlyHintOnFindIterate(boolean forwardOnlyHintOnFindIterate) { + this.forwardOnlyHintOnFindIterate = forwardOnlyHintOnFindIterate; + } + + /** + * Return the DB identity/sequence features for this platform. + * + * @return the db identity + */ + public DbIdentity getDbIdentity() { + return dbIdentity; + } + + /** + * Return the SqlLimiter used to apply additional sql around a query to limit + * its results. + *

+ * Basically add the clauses for limit/offset, rownum, row_number(). + *

+ * + * @return the sql limiter + */ + public SqlLimiter getSqlLimiter() { + return sqlLimiter; + } + + /** + * Convert backticks to the platform specific open quote and close quote + * + *

+ * Specific plugins may implement this method to cater for platform specific + * naming rules. + *

+ * + * @param dbName + * the db name + * + * @return the string + */ + public String convertQuotedIdentifiers(String dbName) { + // Ignore null values e.g. schema name or catalog + if (dbName != null && dbName.length() > 0) { + if (dbName.charAt(0) == BACK_TICK) { + if (dbName.charAt(dbName.length() - 1) == BACK_TICK) { + + String quotedName = getOpenQuote(); + quotedName += dbName.substring(1, dbName.length() - 1); + quotedName += getCloseQuote(); + + return quotedName; + + } else { + logger.error("Missing backquote on [" + dbName + "]"); + } + } + } + return dbName; + } + + /** + * Set to true if select count against anonymous view requires an alias. + */ + public boolean isSelectCountWithAlias() { + return selectCountWithAlias; + } + + public String completeSql(String sql, Query query) { + if (Boolean.TRUE.equals(query.isForUpdate())) { + sql = withForUpdate(sql); + } + + return sql; + } + + protected String withForUpdate(String sql) { + // silently assume the database does not support the "for update" clause. + logger.info("it seems your database does not support the 'for update' clause"); + + return sql; + } + + /** + * Returns the like clause used by this database platform. + *

+ * This may include an escape clause to disable a default escape character. + */ + public String getLikeClause() { + return likeClause; + } + + /** + * Return true if the persistBatchOnCascade setting should be ignored. + *

+ * This is primarily for SQL Server which does not support getGeneratedKeys with jdbc batch mode + * so can't really be transparently used. + *

+ */ + public boolean isDisallowBatchOnCascade() { + return disallowBatchOnCascade; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java index 796e69327..fa1731de9 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/Oracle10Platform.java @@ -1,67 +1,68 @@ -package com.avaje.ebean.config.dbplatform; - -import com.avaje.ebean.BackgroundExecutor; - -import javax.sql.DataSource; -import java.sql.Types; - -/** - * Oracle10 and greater specific platform. - */ -public class Oracle10Platform extends DatabasePlatform { - - public Oracle10Platform() { - super(); - this.name = "oracle"; - this.dbEncrypt = new Oracle10DbEncrypt(); - - this.sqlLimiter = new RownumSqlLimiter(); - - // Not using getGeneratedKeys as instead we will - // batch load sequences which enables JDBC batch execution - dbIdentity.setSupportsGetGeneratedKeys(false); - dbIdentity.setIdType(IdType.SEQUENCE); - dbIdentity.setSupportsSequence(true); - - this.treatEmptyStringsAsNull = true; - - this.openQuote = "\""; - this.closeQuote = "\""; - - booleanDbType = Types.INTEGER; - dbTypeMap.put(Types.BOOLEAN, new DbType("number(1) default 0")); - - dbTypeMap.put(Types.INTEGER, new DbType("number", 10)); - dbTypeMap.put(Types.BIGINT, new DbType("number", 19)); - dbTypeMap.put(Types.REAL, new DbType("number", 19, 4)); - dbTypeMap.put(Types.DOUBLE, new DbType("number", 19, 4)); - dbTypeMap.put(Types.SMALLINT, new DbType("number", 5)); - dbTypeMap.put(Types.TINYINT, new DbType("number", 3)); - dbTypeMap.put(Types.DECIMAL, new DbType("number", 38)); - - dbTypeMap.put(Types.VARCHAR, new DbType("varchar2", 255)); - - dbTypeMap.put(Types.LONGVARBINARY, new DbType("blob")); - dbTypeMap.put(Types.LONGVARCHAR, new DbType("clob")); - dbTypeMap.put(Types.VARBINARY, new DbType("raw", 255)); - dbTypeMap.put(Types.BINARY, new DbType("raw", 255)); - - dbTypeMap.put(Types.TIME, new DbType("timestamp")); - - dbDdlSyntax.setDropTableCascade("cascade constraints purge"); - dbDdlSyntax.setIdentity(null); - dbDdlSyntax.setMaxConstraintNameLength(30); - } - - @Override - public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, - String seqName, int batchSize) { - - return new OracleSequenceIdGenerator(be, ds, seqName, batchSize); - } - - @Override - protected String withForUpdate(String sql) { - return sql + " for update"; - } -} +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; + +import javax.sql.DataSource; +import java.sql.Types; + +/** + * Oracle10 and greater specific platform. + */ +public class Oracle10Platform extends DatabasePlatform { + + public Oracle10Platform() { + super(); + this.name = "oracle"; + // OnQueryOnly.CLOSE as a performance optimisation on Oracle + this.onQueryOnly = OnQueryOnly.CLOSE; + this.dbEncrypt = new Oracle10DbEncrypt(); + this.sqlLimiter = new RownumSqlLimiter(); + + // Not using getGeneratedKeys as instead we will + // batch load sequences which enables JDBC batch execution + dbIdentity.setSupportsGetGeneratedKeys(false); + dbIdentity.setIdType(IdType.SEQUENCE); + dbIdentity.setSupportsSequence(true); + + this.treatEmptyStringsAsNull = true; + + this.openQuote = "\""; + this.closeQuote = "\""; + + booleanDbType = Types.INTEGER; + dbTypeMap.put(Types.BOOLEAN, new DbType("number(1) default 0")); + + dbTypeMap.put(Types.INTEGER, new DbType("number", 10)); + dbTypeMap.put(Types.BIGINT, new DbType("number", 19)); + dbTypeMap.put(Types.REAL, new DbType("number", 19, 4)); + dbTypeMap.put(Types.DOUBLE, new DbType("number", 19, 4)); + dbTypeMap.put(Types.SMALLINT, new DbType("number", 5)); + dbTypeMap.put(Types.TINYINT, new DbType("number", 3)); + dbTypeMap.put(Types.DECIMAL, new DbType("number", 38)); + + dbTypeMap.put(Types.VARCHAR, new DbType("varchar2", 255)); + + dbTypeMap.put(Types.LONGVARBINARY, new DbType("blob")); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("clob")); + dbTypeMap.put(Types.VARBINARY, new DbType("raw", 255)); + dbTypeMap.put(Types.BINARY, new DbType("raw", 255)); + + dbTypeMap.put(Types.TIME, new DbType("timestamp")); + + dbDdlSyntax.setDropTableCascade("cascade constraints purge"); + dbDdlSyntax.setIdentity(null); + dbDdlSyntax.setMaxConstraintNameLength(30); + } + + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return new OracleSequenceIdGenerator(be, ds, seqName, batchSize); + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java index 08fa91546..021e30bb4 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -1,78 +1,80 @@ -package com.avaje.ebean.config.dbplatform; - -import com.avaje.ebean.BackgroundExecutor; - -import javax.sql.DataSource; -import java.sql.Types; - -/** - * Postgres v9 specific platform. - *

- * Uses serial types and getGeneratedKeys. - *

- */ -public class PostgresPlatform extends DatabasePlatform { - - /** - * Unique jdbc type id defined for hstore type. - */ - public static final int TYPE_HSTORE = 4001; - - public PostgresPlatform() { - super(); - this.name = "postgres"; - this.likeClause = "like ? escape''"; - - this.dbDdlSyntax = new PostgresDdlSyntax(); - - this.selectCountWithAlias = true; - this.blobDbType = Types.LONGVARBINARY; - this.clobDbType = Types.VARCHAR; - - this.dbEncrypt = new PostgresDbEncrypt(); - - // Use Identity and getGeneratedKeys - this.dbIdentity.setIdType(IdType.IDENTITY); - this.dbIdentity.setSupportsGetGeneratedKeys(true); - this.dbIdentity.setSupportsSequence(true); - - this.columnAliasPrefix = "as c"; - - this.openQuote = "\""; - this.closeQuote = "\""; - - dbTypeMap.put(TYPE_HSTORE, new DbType("hstore")); - - dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); - dbTypeMap.put(Types.DOUBLE, new DbType("float")); - dbTypeMap.put(Types.TINYINT, new DbType("smallint")); - dbTypeMap.put(Types.DECIMAL, new DbType("decimal", 38)); - - dbTypeMap.put(Types.BINARY, new DbType("bytea", false)); - dbTypeMap.put(Types.VARBINARY, new DbType("bytea", false)); - - dbTypeMap.put(Types.BLOB, new DbType("bytea", false)); - dbTypeMap.put(Types.CLOB, new DbType("text")); - dbTypeMap.put(Types.LONGVARBINARY, new DbType("bytea", false)); - dbTypeMap.put(Types.LONGVARCHAR, new DbType("text")); - - dbDdlSyntax.setDropTableCascade("cascade"); - dbDdlSyntax.setDropIfExists("if exists"); - - } - - /** - * Create a Postgres specific sequence IdGenerator. - */ - @Override - public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, - String seqName, int batchSize) { - - return new PostgresSequenceIdGenerator(be, ds, seqName, batchSize); - } - - @Override - protected String withForUpdate(String sql) { - return sql + " for update"; - } -} +package com.avaje.ebean.config.dbplatform; + +import com.avaje.ebean.BackgroundExecutor; + +import javax.sql.DataSource; +import java.sql.Types; + +/** + * Postgres v9 specific platform. + *

+ * Uses serial types and getGeneratedKeys. + *

+ */ +public class PostgresPlatform extends DatabasePlatform { + + /** + * Unique jdbc type id defined for hstore type. + */ + public static final int TYPE_HSTORE = 4001; + + public PostgresPlatform() { + super(); + this.name = "postgres"; + // OnQueryOnly.CLOSE as a performance optimisation on Postgres + this.onQueryOnly = OnQueryOnly.CLOSE; + this.likeClause = "like ? escape''"; + + this.dbDdlSyntax = new PostgresDdlSyntax(); + + this.selectCountWithAlias = true; + this.blobDbType = Types.LONGVARBINARY; + this.clobDbType = Types.VARCHAR; + + this.dbEncrypt = new PostgresDbEncrypt(); + + // Use Identity and getGeneratedKeys + this.dbIdentity.setIdType(IdType.IDENTITY); + this.dbIdentity.setSupportsGetGeneratedKeys(true); + this.dbIdentity.setSupportsSequence(true); + + this.columnAliasPrefix = "as c"; + + this.openQuote = "\""; + this.closeQuote = "\""; + + dbTypeMap.put(TYPE_HSTORE, new DbType("hstore")); + + dbTypeMap.put(Types.INTEGER, new DbType("integer", false)); + dbTypeMap.put(Types.DOUBLE, new DbType("float")); + dbTypeMap.put(Types.TINYINT, new DbType("smallint")); + dbTypeMap.put(Types.DECIMAL, new DbType("decimal", 38)); + + dbTypeMap.put(Types.BINARY, new DbType("bytea", false)); + dbTypeMap.put(Types.VARBINARY, new DbType("bytea", false)); + + dbTypeMap.put(Types.BLOB, new DbType("bytea", false)); + dbTypeMap.put(Types.CLOB, new DbType("text")); + dbTypeMap.put(Types.LONGVARBINARY, new DbType("bytea", false)); + dbTypeMap.put(Types.LONGVARCHAR, new DbType("text")); + + dbDdlSyntax.setDropTableCascade("cascade"); + dbDdlSyntax.setDropIfExists("if exists"); + + } + + /** + * Create a Postgres specific sequence IdGenerator. + */ + @Override + public IdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, + String seqName, int batchSize) { + + return new PostgresSequenceIdGenerator(be, ds, seqName, batchSize); + } + + @Override + protected String withForUpdate(String sql) { + return sql + " for update"; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java index 39812210a..64ea8167e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java @@ -1,956 +1,956 @@ -package com.avaje.ebeaninternal.server.transaction; - -import com.avaje.ebean.TransactionCallback; -import com.avaje.ebean.bean.PersistenceContext; -import com.avaje.ebean.config.PersistBatch; -import com.avaje.ebeaninternal.api.DerivedRelationshipData; -import com.avaje.ebeaninternal.api.SpiTransaction; -import com.avaje.ebeaninternal.api.TransactionEvent; -import com.avaje.ebeaninternal.server.core.PersistRequest; -import com.avaje.ebeaninternal.server.core.PersistRequestBean; -import com.avaje.ebeaninternal.server.lib.util.Str; -import com.avaje.ebeaninternal.server.persist.BatchControl; -import com.avaje.ebeaninternal.server.transaction.TransactionManager.OnQueryOnly; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.persistence.PersistenceException; -import javax.persistence.RollbackException; -import java.io.IOException; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.*; - -/** - * JDBC Connection based transaction. - */ -public class JdbcTransaction implements SpiTransaction { - - private static final Logger logger = LoggerFactory.getLogger(JdbcTransaction.class); - - private static final Object PLACEHOLDER = new Object(); - - private static final String illegalStateMessage = "Transaction is Inactive"; - - /** - * The associated TransactionManager. - */ - protected final TransactionManager manager; - - /** - * The transaction id. - */ - protected final String id; - - /** - * Flag to indicate if this was an explicitly created Transaction. - */ - protected final boolean explicit; - - /** - * Behaviour for ending query only transactions. - */ - protected final OnQueryOnly onQueryOnly; - - /** - * The status of the transaction. - */ - protected boolean active; - - /** - * The underlying Connection. - */ - protected Connection connection; - - /** - * Used to queue up persist requests for batch execution. - */ - protected BatchControl batchControl; - - /** - * The event which holds persisted beans. - */ - protected TransactionEvent event; - - /** - * Holder of the objects fetched to ensure unique objects are used. - */ - protected PersistenceContext persistenceContext; - - /** - * Used to give developers more control over the insert update and delete - * functionality. - */ - protected boolean persistCascade = true; - - /** - * Flag used for performance to skip commit or rollback of query only - * transactions in read committed transaction isolation. - */ - protected boolean queryOnly = true; - - protected boolean localReadOnly; - - protected PersistBatch oldBatchMode; - - protected PersistBatch batchMode; - - protected PersistBatch batchOnCascadeMode; - - protected int batchSize = -1; - - protected boolean batchFlushOnQuery = true; - - protected Boolean batchGetGeneratedKeys; - - protected Boolean batchFlushOnMixed; - - protected String logPrefix; - - /** - * The depth used by batch processing to help the ordering of statements. - */ - protected int depth; - - /** - * Set to true if the connection has autoCommit=true initially. - */ - protected final boolean autoCommit; - - protected IdentityHashMap persistingBeans; - - protected HashSet deletingBeansHash; - - protected HashMap m2mIntersectionSave; - - protected HashMap> derivedRelMap; - - protected Map userObjects; - - protected List callbackList; - - protected boolean batchOnCascadeSet; - - /** - * Create a new JdbcTransaction. - */ - public JdbcTransaction(String id, boolean explicit, Connection connection, TransactionManager manager) { - try { - this.active = true; - this.id = id; - this.logPrefix = deriveLogPrefix(id); - this.explicit = explicit; - this.manager = manager; - this.connection = connection; - this.batchMode = manager == null ? PersistBatch.NONE : manager.getPersistBatch(); - this.batchOnCascadeMode = manager == null ? PersistBatch.NONE : manager.getPersistBatchOnCascade(); - this.onQueryOnly = manager == null ? OnQueryOnly.ROLLBACK : manager.getOnQueryOnly(); - this.persistenceContext = new DefaultPersistenceContext(); - this.autoCommit = connection.getAutoCommit(); - if (this.autoCommit) { - connection.setAutoCommit(false); - } - - } catch (Exception e) { - throw new PersistenceException(e); - } - } - - private static String deriveLogPrefix(String id) { - - StringBuilder sb = new StringBuilder(); - sb.append("txn["); - if (id != null) { - sb.append(id); - } - sb.append("] "); - return sb.toString(); - } - - @Override - public String getLogPrefix() { - return logPrefix; - } - - public String toString() { - return logPrefix; - } - - @Override - public void register(TransactionCallback callback) { - if (callbackList == null) { - callbackList = new ArrayList(4); - } - callbackList.add(callback); - } - - protected void firePreRollback() { - if (callbackList != null) { - for (TransactionCallback callback : callbackList) { - try { - callback.preRollback(); - } catch (Exception e) { - logger.error("Error executing preRollback callback", e); - } - } - } - } - - protected void firePostRollback() { - if (callbackList != null) { - for (TransactionCallback callback : callbackList) { - try { - callback.postRollback(); - } catch (Exception e) { - logger.error("Error executing postRollback callback", e); - } - } - } - } - - protected void firePreCommit() { - if (callbackList != null) { - for (TransactionCallback callback : callbackList) { - try { - callback.preCommit(); - } catch (Exception e) { - logger.error("Error executing preCommit callback", e); - } - } - } - } - - protected void firePostCommit() { - if (callbackList != null) { - for (TransactionCallback callback : callbackList) { - try { - callback.postCommit(); - } catch (Exception e) { - logger.error("Error executing postCommit callback", e); - } - } - } - } - - @Override - public List getDerivedRelationship(Object bean) { - if (derivedRelMap == null) { - return null; - } - return derivedRelMap.get(System.identityHashCode(bean)); - } - - @Override - public void registerDerivedRelationship(DerivedRelationshipData derivedRelationship) { - if (derivedRelMap == null) { - derivedRelMap = new HashMap>(); - } - Integer key = new Integer(System.identityHashCode(derivedRelationship.getAssocBean())); - - List list = derivedRelMap.get(key); - if (list == null) { - list = new ArrayList(); - derivedRelMap.put(key, list); - } - list.add(derivedRelationship); - } - - /** - * Add a bean to the registed list. - *

- * This is to handle bi-directional relationships where both sides Cascade. - *

- */ - @Override - public void registerDeleteBean(Integer persistingBean) { - if (deletingBeansHash == null) { - deletingBeansHash = new HashSet(); - } - deletingBeansHash.add(persistingBean); - } - - /** - * Unregister the persisted bean. - */ - @Override - public void unregisterDeleteBean(Integer persistedBean) { - if (deletingBeansHash != null) { - deletingBeansHash.remove(persistedBean); - } - } - - /** - * Return true if this is a bean that has already been saved/deleted. - */ - @Override - public boolean isRegisteredDeleteBean(Integer persistingBean) { - return deletingBeansHash != null && deletingBeansHash.contains(persistingBean); - } - - /** - * Unregister the persisted bean. - */ - @Override - public void unregisterBean(Object bean) { - persistingBeans.remove(bean); - } - - /** - * Return true if this is a bean that has already been saved. This will - * register the bean if it is not already. - */ - @Override - public boolean isRegisteredBean(Object bean) { - if (persistingBeans == null) { - persistingBeans = new IdentityHashMap(); - } - return (persistingBeans.put(bean, PLACEHOLDER) != null); - } - - /** - * Return true if the m2m intersection save is allowed from a given bean direction. - * This is to stop m2m intersection management via both directions of a m2m. - */ - @Override - public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) { - if (m2mIntersectionSave == null) { - // first attempt so yes allow this m2m intersection direction - m2mIntersectionSave = new HashMap(); - m2mIntersectionSave.put(intersectionTable, beanName); - return true; - } - String existingBean = m2mIntersectionSave.get(intersectionTable); - if (existingBean == null) { - // first time into this intersection table so allow - m2mIntersectionSave.put(intersectionTable, beanName); - return true; - } - - // only allow if save coming from the same bean type - // to stop saves coming from both directions of m2m - return existingBean.equals(beanName); - } - - /** - * Return the depth of the current persist request plus the diff. This has the - * effect of changing the current depth and returning the new value. Pass - * diff=0 to return the current depth. - *

- * The depth of 0 is for the initial persist request. It is modified as the - * cascading of the save or delete traverses to the the associated Ones (-1) - * and associated Manys (+1). - *

- *

- * The depth is used to help the ordering of batched statements. - *

- * - * @param diff the amount to add or subtract from the depth. - * @return the current depth plus the diff - */ - @Override - public int depth(int diff) { - depth += diff; - return depth; - } - - /** - * Return the current depth. - */ - @Override - public int depth() { - return depth; - } - - @Override - public boolean isReadOnly() { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - try { - return connection.isReadOnly(); - } catch (SQLException e) { - throw new PersistenceException(e); - } - } - - @Override - public void setReadOnly(boolean readOnly) { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - try { - localReadOnly = readOnly; - connection.setReadOnly(readOnly); - } catch (SQLException e) { - throw new PersistenceException(e); - } - } - - @Override - public void setBatchMode(boolean batchMode) { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - this.batchMode = (batchMode) ? PersistBatch.ALL : PersistBatch.NONE; - } - - @Override - public void setBatch(PersistBatch batchMode) { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - this.batchMode = batchMode; - } - - @Override - public PersistBatch getBatch() { - return batchMode; - } - - @Override - public void setBatchOnCascade(PersistBatch batchOnCascadeMode) { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - this.batchOnCascadeMode = batchOnCascadeMode; - } - - @Override - public PersistBatch getBatchOnCascade() { - return batchOnCascadeMode; - } - - @Override - public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) { - this.batchGetGeneratedKeys = getGeneratedKeys; - if (batchControl != null) { - batchControl.setGetGeneratedKeys(getGeneratedKeys); - } - } - - @Override - public void setBatchFlushOnMixed(boolean batchFlushOnMixed) { - this.batchFlushOnMixed = batchFlushOnMixed; - if (batchControl != null) { - batchControl.setBatchFlushOnMixed(batchFlushOnMixed); - } - } - - /** - * Return the batchSize specifically set for this transaction or 0. - *

- * Returning 0 implies to use the system wide default batch size. - *

- */ - @Override - public int getBatchSize() { - return batchSize; - } - - @Override - public void setBatchSize(int batchSize) { - this.batchSize = batchSize; - if (batchControl != null) { - batchControl.setBatchSize(batchSize); - } - } - - @Override - public boolean isBatchFlushOnQuery() { - return batchFlushOnQuery; - } - - @Override - public void setBatchFlushOnQuery(boolean batchFlushOnQuery) { - this.batchFlushOnQuery = batchFlushOnQuery; - } - - /** - * Return true if this request should be batched. Returning false means that - * this request should be executed immediately. - */ - @Override - public boolean isBatchThisRequest(PersistRequest.Type type) { - if (!batchOnCascadeSet && !explicit && depth <= 0) { - // implicit transaction, no gain by batching where depth <= 0 - return false; - } - switch (batchMode) { - case ALL: - return true; - case INSERT: - return type == PersistRequest.Type.INSERT; - default: - return false; - } - } - - /** - * Return true if JDBC batch should be used on cascade persist. - */ - private boolean isBatchOnCascade(PersistRequest.Type type) { - - switch (batchOnCascadeMode) { - case ALL: - return true; - case INSERT: - return type == PersistRequest.Type.INSERT; - default: - return false; - } - } - - public void checkBatchEscalationOnCollection() { - if (batchMode == PersistBatch.NONE && batchOnCascadeMode != PersistBatch.NONE) { - batchMode = batchOnCascadeMode; - batchOnCascadeSet = true; - } - } - - public void flushBatchOnCollection() { - if (batchOnCascadeSet) { - if (batchControl != null) { - if (logger.isTraceEnabled()) { - logger.trace("... flushBatchOnCollection"); - } - batchControl.flushReset(); - } - // restore the previous batch mode of NONE - batchMode = PersistBatch.NONE; - } - } - - /** - * Flush after completing persist cascade. - */ - @Override - public void flushBatchOnCascade() { - if (batchControl != null) { - if (logger.isTraceEnabled()) { - logger.trace("... flushBatchOnCascade"); - } - batchControl.flushReset(); - } - // restore the previous batch mode - batchMode = oldBatchMode; - } - - private boolean isAlreadyBatching(PersistRequest.Type type) { - switch (batchMode) { - case ALL: - return true; - case INSERT: - return type == PersistRequest.Type.INSERT; - default: - return false; - } - } - - public boolean checkBatchEscalationOnCascade(PersistRequestBean request) { - - if (isAlreadyBatching(request.getType())) { - // already batching (at top level) - return false; - } - - if (isBatchOnCascade(request.getType())) { - // escalate up to batch mode for this request (and cascade) - oldBatchMode = batchMode; - batchMode = PersistBatch.ALL; - if (batchControl != null) { - // flush with reset so that this request goes into it's own batch buffer - batchControl.flushReset(); - } - // skip using jdbc batch for the top level bean (no gain there) - request.setSkipBatchForTopLevel(); - return true; - } - - if (batchControl != null && !batchControl.isEmpty()) { - if (logger.isTraceEnabled()) { - logger.trace("... flush from batchOnCascade "); - } - batchControl.flushReset(); - } - return false; - } - - @Override - public BatchControl getBatchControl() { - return batchControl; - } - - /** - * Set the BatchControl to the transaction. This is done once per transaction - * on the first persist request. - */ - @Override - public void setBatchControl(BatchControl batchControl) { - queryOnly = false; - this.batchControl = batchControl; - // in case these parameters have already been set - if (batchGetGeneratedKeys != null) { - batchControl.setGetGeneratedKeys(batchGetGeneratedKeys); - } - if (batchSize != -1) { - batchControl.setBatchSize(batchSize); - } - if (batchFlushOnMixed != null) { - batchControl.setBatchFlushOnMixed(batchFlushOnMixed); - } - } - - /** - * Flush any queued persist requests. - *

- * This is general will result in a number of batched PreparedStatements - * executing. - *

- */ - @Override - public void flushBatch() { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - if (batchControl != null) { - batchControl.flush(); - } - } - - /** - * Return the persistence context associated with this transaction. - */ - @Override - public PersistenceContext getPersistenceContext() { - return persistenceContext; - } - - /** - * Set the persistence context to this transaction. - *

- * This could be considered similar to EJB3 Extended PersistanceContext. In - * that you get the PersistanceContext from a transaction, hold onto it, and - * then set it back later to a second transaction. - *

- */ - @Override - public void setPersistenceContext(PersistenceContext context) { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - this.persistenceContext = context; - } - - /** - * Return the underlying TransactionEvent. - */ - @Override - public TransactionEvent getEvent() { - queryOnly = false; - if (event == null) { - event = new TransactionEvent(); - } - return event; - } - - /** - * Return true if this was an explicitly created transaction. - */ - @Override - public boolean isExplicit() { - return explicit; - } - - @Override - public boolean isLogSql() { - return TransactionManager.SQL_LOGGER.isDebugEnabled(); - } - - @Override - public boolean isLogSummary() { - return TransactionManager.SUM_LOGGER.isDebugEnabled(); - } - - @Override - public void logSql(String msg) { - TransactionManager.SQL_LOGGER.trace(Str.add(logPrefix, msg)); - } - - @Override - public void logSummary(String msg) { - TransactionManager.SUM_LOGGER.debug(Str.add(logPrefix, msg)); - } - - /** - * Return the transaction id. - */ - @Override - public String getId() { - return id; - } - - /** - * Return the underlying connection for internal use. - */ - @Override - public Connection getInternalConnection() { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - return connection; - } - - /** - * Return the underlying connection for public use. - */ - @Override - public Connection getConnection() { - queryOnly = false; - return getInternalConnection(); - } - - protected void deactivate() { - try { - if (localReadOnly) { - // reset readOnly status prior to returning to pool - connection.setReadOnly(false); - } - } catch (SQLException e) { - logger.error("Error setting to readOnly?", e); - } - try { - if (autoCommit) { - // reset the autoCommit status prior to returning to pool - connection.setAutoCommit(true); - } - } catch (SQLException e) { - logger.error("Error setting to readOnly?", e); - } - try { - connection.close(); - } catch (Exception ex) { - // the connection pool will automatically remove the - // connection if it does not pass the test - logger.error("Error closing connection", ex); - } - connection = null; - active = false; - } - - /** - * Notify the transaction manager. - */ - protected void notifyCommit() { - if (manager != null) { - if (queryOnly) { - manager.notifyOfQueryOnly(true, this, null); - } else { - manager.notifyOfCommit(this); - } - } - } - - protected void notifyQueryOnly() { - if (manager != null) { - manager.notifyOfQueryOnly(true, this, null); - } - } - - /** - * Rollback, Commit or Close for query only transaction. - *

- * For a transaction that was used for queries only we can choose to either - * rollback or just close the connection for performance. - *

- */ - protected void connectionEndForQueryOnly() { - try { - switch (onQueryOnly) { - case ROLLBACK: - performRollback(); - break; - case COMMIT: - performCommit(); - break; - case CLOSE_ON_READCOMMITTED: - // valid at READ COMMITTED Isolation - break; - default: - performRollback(); - } - } catch (SQLException e) { - logger.error("Error when ending a query only transaction via " + onQueryOnly, e); - } - } - - /** - * Perform the actual rollback on the connection. - */ - protected void performRollback() throws SQLException { - connection.rollback(); - } - - /** - * Perform the actual commit on the connection. - */ - protected void performCommit() throws SQLException { - connection.commit(); - } - - /** - * End the transaction on a query only request. - */ - @Override - public void endQueryOnly() { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - try { - connectionEndForQueryOnly(); - } finally { - // these will not throw an exception - deactivate(); - notifyQueryOnly(); - } - } - - /** - * Commit the transaction. - */ - @Override - public void commit() throws RollbackException { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - - firePreCommit(); - - try { - if (queryOnly) { - // can rollback or just close for performance - connectionEndForQueryOnly(); - } else { - // commit - if (batchControl != null && !batchControl.isEmpty()) { - batchControl.flush(); - } - performCommit(); - } - - } catch (Exception e) { - throw new RollbackException(e); - - } finally { - // these will not throw an exception - firePostCommit(); - deactivate(); - notifyCommit(); - } - } - - /** - * Notify the transaction manager. - */ - protected void notifyRollback(Throwable cause) { - if (manager != null) { - if (queryOnly) { - manager.notifyOfQueryOnly(false, this, cause); - } else { - manager.notifyOfRollback(this, cause); - } - } - } - - /** - * Rollback the transaction. - */ - @Override - public void rollback() throws PersistenceException { - rollback(null); - } - - /** - * Rollback the transaction. If there is a throwable it is logged as the cause - * in the transaction log. - */ - @Override - public void rollback(Throwable cause) throws PersistenceException { - if (!isActive()) { - throw new IllegalStateException(illegalStateMessage); - } - firePreRollback(); - try { - performRollback(); - - } catch (Exception ex) { - throw new PersistenceException(ex); - - } finally { - // these will not throw an exception - firePostRollback(); - deactivate(); - notifyRollback(cause); - } - } - - /** - * If the transaction is active then perform rollback. - */ - @Override - public void end() throws PersistenceException { - if (isActive()) { - rollback(); - } - } - - /** - * Return true if the transaction is active. - */ - @Override - public boolean isActive() { - return active; - } - - @Override - public boolean isPersistCascade() { - return persistCascade; - } - - @Override - public void setPersistCascade(boolean persistCascade) { - this.persistCascade = persistCascade; - } - - @Override - public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) { - getEvent().add(tableName, inserts, updates, deletes); - } - - @Override - public void putUserObject(String name, Object value) { - if (userObjects == null) { - userObjects = new HashMap(); - } - userObjects.put(name, value); - } - - @Override - public Object getUserObject(String name) { - if (userObjects == null) { - return null; - } - return userObjects.get(name); - } - - /** - * Alias for end(), which enables this class to be used in try-with-resources. - */ - @Override - public void close() throws IOException { - try { - end(); - } catch (PersistenceException ex) { - throw new IOException(ex); - } - } -} +package com.avaje.ebeaninternal.server.transaction; + +import com.avaje.ebean.TransactionCallback; +import com.avaje.ebean.bean.PersistenceContext; +import com.avaje.ebean.config.PersistBatch; +import com.avaje.ebeaninternal.api.DerivedRelationshipData; +import com.avaje.ebeaninternal.api.SpiTransaction; +import com.avaje.ebeaninternal.api.TransactionEvent; +import com.avaje.ebeaninternal.server.core.PersistRequest; +import com.avaje.ebeaninternal.server.core.PersistRequestBean; +import com.avaje.ebeaninternal.server.lib.util.Str; +import com.avaje.ebeaninternal.server.persist.BatchControl; +import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.PersistenceException; +import javax.persistence.RollbackException; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.*; + +/** + * JDBC Connection based transaction. + */ +public class JdbcTransaction implements SpiTransaction { + + private static final Logger logger = LoggerFactory.getLogger(JdbcTransaction.class); + + private static final Object PLACEHOLDER = new Object(); + + private static final String illegalStateMessage = "Transaction is Inactive"; + + /** + * The associated TransactionManager. + */ + protected final TransactionManager manager; + + /** + * The transaction id. + */ + protected final String id; + + /** + * Flag to indicate if this was an explicitly created Transaction. + */ + protected final boolean explicit; + + /** + * Behaviour for ending query only transactions. + */ + protected final OnQueryOnly onQueryOnly; + + /** + * The status of the transaction. + */ + protected boolean active; + + /** + * The underlying Connection. + */ + protected Connection connection; + + /** + * Used to queue up persist requests for batch execution. + */ + protected BatchControl batchControl; + + /** + * The event which holds persisted beans. + */ + protected TransactionEvent event; + + /** + * Holder of the objects fetched to ensure unique objects are used. + */ + protected PersistenceContext persistenceContext; + + /** + * Used to give developers more control over the insert update and delete + * functionality. + */ + protected boolean persistCascade = true; + + /** + * Flag used for performance to skip commit or rollback of query only + * transactions in read committed transaction isolation. + */ + protected boolean queryOnly = true; + + protected boolean localReadOnly; + + protected PersistBatch oldBatchMode; + + protected PersistBatch batchMode; + + protected PersistBatch batchOnCascadeMode; + + protected int batchSize = -1; + + protected boolean batchFlushOnQuery = true; + + protected Boolean batchGetGeneratedKeys; + + protected Boolean batchFlushOnMixed; + + protected String logPrefix; + + /** + * The depth used by batch processing to help the ordering of statements. + */ + protected int depth; + + /** + * Set to true if the connection has autoCommit=true initially. + */ + protected final boolean autoCommit; + + protected IdentityHashMap persistingBeans; + + protected HashSet deletingBeansHash; + + protected HashMap m2mIntersectionSave; + + protected HashMap> derivedRelMap; + + protected Map userObjects; + + protected List callbackList; + + protected boolean batchOnCascadeSet; + + /** + * Create a new JdbcTransaction. + */ + public JdbcTransaction(String id, boolean explicit, Connection connection, TransactionManager manager) { + try { + this.active = true; + this.id = id; + this.logPrefix = deriveLogPrefix(id); + this.explicit = explicit; + this.manager = manager; + this.connection = connection; + this.batchMode = manager == null ? PersistBatch.NONE : manager.getPersistBatch(); + this.batchOnCascadeMode = manager == null ? PersistBatch.NONE : manager.getPersistBatchOnCascade(); + this.onQueryOnly = manager == null ? OnQueryOnly.ROLLBACK : manager.getOnQueryOnly(); + this.persistenceContext = new DefaultPersistenceContext(); + this.autoCommit = connection.getAutoCommit(); + if (this.autoCommit) { + connection.setAutoCommit(false); + } + + } catch (Exception e) { + throw new PersistenceException(e); + } + } + + private static String deriveLogPrefix(String id) { + + StringBuilder sb = new StringBuilder(); + sb.append("txn["); + if (id != null) { + sb.append(id); + } + sb.append("] "); + return sb.toString(); + } + + @Override + public String getLogPrefix() { + return logPrefix; + } + + public String toString() { + return logPrefix; + } + + @Override + public void register(TransactionCallback callback) { + if (callbackList == null) { + callbackList = new ArrayList(4); + } + callbackList.add(callback); + } + + protected void firePreRollback() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.preRollback(); + } catch (Exception e) { + logger.error("Error executing preRollback callback", e); + } + } + } + } + + protected void firePostRollback() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.postRollback(); + } catch (Exception e) { + logger.error("Error executing postRollback callback", e); + } + } + } + } + + protected void firePreCommit() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.preCommit(); + } catch (Exception e) { + logger.error("Error executing preCommit callback", e); + } + } + } + } + + protected void firePostCommit() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.postCommit(); + } catch (Exception e) { + logger.error("Error executing postCommit callback", e); + } + } + } + } + + @Override + public List getDerivedRelationship(Object bean) { + if (derivedRelMap == null) { + return null; + } + return derivedRelMap.get(System.identityHashCode(bean)); + } + + @Override + public void registerDerivedRelationship(DerivedRelationshipData derivedRelationship) { + if (derivedRelMap == null) { + derivedRelMap = new HashMap>(); + } + Integer key = new Integer(System.identityHashCode(derivedRelationship.getAssocBean())); + + List list = derivedRelMap.get(key); + if (list == null) { + list = new ArrayList(); + derivedRelMap.put(key, list); + } + list.add(derivedRelationship); + } + + /** + * Add a bean to the registed list. + *

+ * This is to handle bi-directional relationships where both sides Cascade. + *

+ */ + @Override + public void registerDeleteBean(Integer persistingBean) { + if (deletingBeansHash == null) { + deletingBeansHash = new HashSet(); + } + deletingBeansHash.add(persistingBean); + } + + /** + * Unregister the persisted bean. + */ + @Override + public void unregisterDeleteBean(Integer persistedBean) { + if (deletingBeansHash != null) { + deletingBeansHash.remove(persistedBean); + } + } + + /** + * Return true if this is a bean that has already been saved/deleted. + */ + @Override + public boolean isRegisteredDeleteBean(Integer persistingBean) { + return deletingBeansHash != null && deletingBeansHash.contains(persistingBean); + } + + /** + * Unregister the persisted bean. + */ + @Override + public void unregisterBean(Object bean) { + persistingBeans.remove(bean); + } + + /** + * Return true if this is a bean that has already been saved. This will + * register the bean if it is not already. + */ + @Override + public boolean isRegisteredBean(Object bean) { + if (persistingBeans == null) { + persistingBeans = new IdentityHashMap(); + } + return (persistingBeans.put(bean, PLACEHOLDER) != null); + } + + /** + * Return true if the m2m intersection save is allowed from a given bean direction. + * This is to stop m2m intersection management via both directions of a m2m. + */ + @Override + public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) { + if (m2mIntersectionSave == null) { + // first attempt so yes allow this m2m intersection direction + m2mIntersectionSave = new HashMap(); + m2mIntersectionSave.put(intersectionTable, beanName); + return true; + } + String existingBean = m2mIntersectionSave.get(intersectionTable); + if (existingBean == null) { + // first time into this intersection table so allow + m2mIntersectionSave.put(intersectionTable, beanName); + return true; + } + + // only allow if save coming from the same bean type + // to stop saves coming from both directions of m2m + return existingBean.equals(beanName); + } + + /** + * Return the depth of the current persist request plus the diff. This has the + * effect of changing the current depth and returning the new value. Pass + * diff=0 to return the current depth. + *

+ * The depth of 0 is for the initial persist request. It is modified as the + * cascading of the save or delete traverses to the the associated Ones (-1) + * and associated Manys (+1). + *

+ *

+ * The depth is used to help the ordering of batched statements. + *

+ * + * @param diff the amount to add or subtract from the depth. + * @return the current depth plus the diff + */ + @Override + public int depth(int diff) { + depth += diff; + return depth; + } + + /** + * Return the current depth. + */ + @Override + public int depth() { + return depth; + } + + @Override + public boolean isReadOnly() { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + try { + return connection.isReadOnly(); + } catch (SQLException e) { + throw new PersistenceException(e); + } + } + + @Override + public void setReadOnly(boolean readOnly) { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + try { + localReadOnly = readOnly; + connection.setReadOnly(readOnly); + } catch (SQLException e) { + throw new PersistenceException(e); + } + } + + @Override + public void setBatchMode(boolean batchMode) { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + this.batchMode = (batchMode) ? PersistBatch.ALL : PersistBatch.NONE; + } + + @Override + public void setBatch(PersistBatch batchMode) { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + this.batchMode = batchMode; + } + + @Override + public PersistBatch getBatch() { + return batchMode; + } + + @Override + public void setBatchOnCascade(PersistBatch batchOnCascadeMode) { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + this.batchOnCascadeMode = batchOnCascadeMode; + } + + @Override + public PersistBatch getBatchOnCascade() { + return batchOnCascadeMode; + } + + @Override + public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) { + this.batchGetGeneratedKeys = getGeneratedKeys; + if (batchControl != null) { + batchControl.setGetGeneratedKeys(getGeneratedKeys); + } + } + + @Override + public void setBatchFlushOnMixed(boolean batchFlushOnMixed) { + this.batchFlushOnMixed = batchFlushOnMixed; + if (batchControl != null) { + batchControl.setBatchFlushOnMixed(batchFlushOnMixed); + } + } + + /** + * Return the batchSize specifically set for this transaction or 0. + *

+ * Returning 0 implies to use the system wide default batch size. + *

+ */ + @Override + public int getBatchSize() { + return batchSize; + } + + @Override + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + if (batchControl != null) { + batchControl.setBatchSize(batchSize); + } + } + + @Override + public boolean isBatchFlushOnQuery() { + return batchFlushOnQuery; + } + + @Override + public void setBatchFlushOnQuery(boolean batchFlushOnQuery) { + this.batchFlushOnQuery = batchFlushOnQuery; + } + + /** + * Return true if this request should be batched. Returning false means that + * this request should be executed immediately. + */ + @Override + public boolean isBatchThisRequest(PersistRequest.Type type) { + if (!batchOnCascadeSet && !explicit && depth <= 0) { + // implicit transaction, no gain by batching where depth <= 0 + return false; + } + switch (batchMode) { + case ALL: + return true; + case INSERT: + return type == PersistRequest.Type.INSERT; + default: + return false; + } + } + + /** + * Return true if JDBC batch should be used on cascade persist. + */ + private boolean isBatchOnCascade(PersistRequest.Type type) { + + switch (batchOnCascadeMode) { + case ALL: + return true; + case INSERT: + return type == PersistRequest.Type.INSERT; + default: + return false; + } + } + + public void checkBatchEscalationOnCollection() { + if (batchMode == PersistBatch.NONE && batchOnCascadeMode != PersistBatch.NONE) { + batchMode = batchOnCascadeMode; + batchOnCascadeSet = true; + } + } + + public void flushBatchOnCollection() { + if (batchOnCascadeSet) { + if (batchControl != null) { + if (logger.isTraceEnabled()) { + logger.trace("... flushBatchOnCollection"); + } + batchControl.flushReset(); + } + // restore the previous batch mode of NONE + batchMode = PersistBatch.NONE; + } + } + + /** + * Flush after completing persist cascade. + */ + @Override + public void flushBatchOnCascade() { + if (batchControl != null) { + if (logger.isTraceEnabled()) { + logger.trace("... flushBatchOnCascade"); + } + batchControl.flushReset(); + } + // restore the previous batch mode + batchMode = oldBatchMode; + } + + private boolean isAlreadyBatching(PersistRequest.Type type) { + switch (batchMode) { + case ALL: + return true; + case INSERT: + return type == PersistRequest.Type.INSERT; + default: + return false; + } + } + + public boolean checkBatchEscalationOnCascade(PersistRequestBean request) { + + if (isAlreadyBatching(request.getType())) { + // already batching (at top level) + return false; + } + + if (isBatchOnCascade(request.getType())) { + // escalate up to batch mode for this request (and cascade) + oldBatchMode = batchMode; + batchMode = PersistBatch.ALL; + if (batchControl != null) { + // flush with reset so that this request goes into it's own batch buffer + batchControl.flushReset(); + } + // skip using jdbc batch for the top level bean (no gain there) + request.setSkipBatchForTopLevel(); + return true; + } + + if (batchControl != null && !batchControl.isEmpty()) { + if (logger.isTraceEnabled()) { + logger.trace("... flush from batchOnCascade "); + } + batchControl.flushReset(); + } + return false; + } + + @Override + public BatchControl getBatchControl() { + return batchControl; + } + + /** + * Set the BatchControl to the transaction. This is done once per transaction + * on the first persist request. + */ + @Override + public void setBatchControl(BatchControl batchControl) { + queryOnly = false; + this.batchControl = batchControl; + // in case these parameters have already been set + if (batchGetGeneratedKeys != null) { + batchControl.setGetGeneratedKeys(batchGetGeneratedKeys); + } + if (batchSize != -1) { + batchControl.setBatchSize(batchSize); + } + if (batchFlushOnMixed != null) { + batchControl.setBatchFlushOnMixed(batchFlushOnMixed); + } + } + + /** + * Flush any queued persist requests. + *

+ * This is general will result in a number of batched PreparedStatements + * executing. + *

+ */ + @Override + public void flushBatch() { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + if (batchControl != null) { + batchControl.flush(); + } + } + + /** + * Return the persistence context associated with this transaction. + */ + @Override + public PersistenceContext getPersistenceContext() { + return persistenceContext; + } + + /** + * Set the persistence context to this transaction. + *

+ * This could be considered similar to EJB3 Extended PersistanceContext. In + * that you get the PersistanceContext from a transaction, hold onto it, and + * then set it back later to a second transaction. + *

+ */ + @Override + public void setPersistenceContext(PersistenceContext context) { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + this.persistenceContext = context; + } + + /** + * Return the underlying TransactionEvent. + */ + @Override + public TransactionEvent getEvent() { + queryOnly = false; + if (event == null) { + event = new TransactionEvent(); + } + return event; + } + + /** + * Return true if this was an explicitly created transaction. + */ + @Override + public boolean isExplicit() { + return explicit; + } + + @Override + public boolean isLogSql() { + return TransactionManager.SQL_LOGGER.isDebugEnabled(); + } + + @Override + public boolean isLogSummary() { + return TransactionManager.SUM_LOGGER.isDebugEnabled(); + } + + @Override + public void logSql(String msg) { + TransactionManager.SQL_LOGGER.trace(Str.add(logPrefix, msg)); + } + + @Override + public void logSummary(String msg) { + TransactionManager.SUM_LOGGER.debug(Str.add(logPrefix, msg)); + } + + /** + * Return the transaction id. + */ + @Override + public String getId() { + return id; + } + + /** + * Return the underlying connection for internal use. + */ + @Override + public Connection getInternalConnection() { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + return connection; + } + + /** + * Return the underlying connection for public use. + */ + @Override + public Connection getConnection() { + queryOnly = false; + return getInternalConnection(); + } + + protected void deactivate() { + try { + if (localReadOnly) { + // reset readOnly status prior to returning to pool + connection.setReadOnly(false); + } + } catch (SQLException e) { + logger.error("Error setting to readOnly?", e); + } + try { + if (autoCommit) { + // reset the autoCommit status prior to returning to pool + connection.setAutoCommit(true); + } + } catch (SQLException e) { + logger.error("Error setting to readOnly?", e); + } + try { + connection.close(); + } catch (Exception ex) { + // the connection pool will automatically remove the + // connection if it does not pass the test + logger.error("Error closing connection", ex); + } + connection = null; + active = false; + } + + /** + * Notify the transaction manager. + */ + protected void notifyCommit() { + if (manager != null) { + if (queryOnly) { + manager.notifyOfQueryOnly(true, this, null); + } else { + manager.notifyOfCommit(this); + } + } + } + + protected void notifyQueryOnly() { + if (manager != null) { + manager.notifyOfQueryOnly(true, this, null); + } + } + + /** + * Rollback, Commit or Close for query only transaction. + *

+ * For a transaction that was used for queries only we can choose to either + * rollback or just close the connection for performance. + *

+ */ + protected void connectionEndForQueryOnly() { + try { + switch (onQueryOnly) { + case ROLLBACK: + performRollback(); + break; + case COMMIT: + performCommit(); + break; + case CLOSE: + // valid at READ COMMITTED Isolation + break; + default: + performRollback(); + } + } catch (SQLException e) { + logger.error("Error when ending a query only transaction via " + onQueryOnly, e); + } + } + + /** + * Perform the actual rollback on the connection. + */ + protected void performRollback() throws SQLException { + connection.rollback(); + } + + /** + * Perform the actual commit on the connection. + */ + protected void performCommit() throws SQLException { + connection.commit(); + } + + /** + * End the transaction on a query only request. + */ + @Override + public void endQueryOnly() { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + try { + connectionEndForQueryOnly(); + } finally { + // these will not throw an exception + deactivate(); + notifyQueryOnly(); + } + } + + /** + * Commit the transaction. + */ + @Override + public void commit() throws RollbackException { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + + firePreCommit(); + + try { + if (queryOnly) { + // can rollback or just close for performance + connectionEndForQueryOnly(); + } else { + // commit + if (batchControl != null && !batchControl.isEmpty()) { + batchControl.flush(); + } + performCommit(); + } + + } catch (Exception e) { + throw new RollbackException(e); + + } finally { + // these will not throw an exception + firePostCommit(); + deactivate(); + notifyCommit(); + } + } + + /** + * Notify the transaction manager. + */ + protected void notifyRollback(Throwable cause) { + if (manager != null) { + if (queryOnly) { + manager.notifyOfQueryOnly(false, this, cause); + } else { + manager.notifyOfRollback(this, cause); + } + } + } + + /** + * Rollback the transaction. + */ + @Override + public void rollback() throws PersistenceException { + rollback(null); + } + + /** + * Rollback the transaction. If there is a throwable it is logged as the cause + * in the transaction log. + */ + @Override + public void rollback(Throwable cause) throws PersistenceException { + if (!isActive()) { + throw new IllegalStateException(illegalStateMessage); + } + firePreRollback(); + try { + performRollback(); + + } catch (Exception ex) { + throw new PersistenceException(ex); + + } finally { + // these will not throw an exception + firePostRollback(); + deactivate(); + notifyRollback(cause); + } + } + + /** + * If the transaction is active then perform rollback. + */ + @Override + public void end() throws PersistenceException { + if (isActive()) { + rollback(); + } + } + + /** + * Return true if the transaction is active. + */ + @Override + public boolean isActive() { + return active; + } + + @Override + public boolean isPersistCascade() { + return persistCascade; + } + + @Override + public void setPersistCascade(boolean persistCascade) { + this.persistCascade = persistCascade; + } + + @Override + public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) { + getEvent().add(tableName, inserts, updates, deletes); + } + + @Override + public void putUserObject(String name, Object value) { + if (userObjects == null) { + userObjects = new HashMap(); + } + userObjects.put(name, value); + } + + @Override + public Object getUserObject(String name) { + if (userObjects == null) { + return null; + } + return userObjects.get(name); + } + + /** + * Alias for end(), which enables this class to be used in try-with-resources. + */ + @Override + public void close() throws IOException { + try { + end(); + } catch (PersistenceException ex) { + throw new IOException(ex); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java index 7680704b2..89575ca33 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java @@ -1,451 +1,436 @@ -package com.avaje.ebeaninternal.server.transaction; - -import com.avaje.ebean.BackgroundExecutor; -import com.avaje.ebean.config.PersistBatch; -import com.avaje.ebean.config.ServerConfig; -import com.avaje.ebean.event.TransactionEventListener; -import com.avaje.ebeaninternal.api.SpiTransaction; -import com.avaje.ebeaninternal.api.TransactionEvent; -import com.avaje.ebeaninternal.api.TransactionEventTable; -import com.avaje.ebeaninternal.api.TransactionEventTable.TableIUD; -import com.avaje.ebeaninternal.server.cluster.ClusterManager; -import com.avaje.ebeaninternal.server.core.BootupClasses; -import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; -import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.persistence.PersistenceException; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Manages transactions. - *

- * Keeps the Cache and Cluster in synch when transactions are committed. - *

- */ -public class TransactionManager { - - private static final Logger logger = LoggerFactory.getLogger(TransactionManager.class); - - public static final Logger SQL_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.SQL"); - - public static final Logger SUM_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.SUM"); - - public static final Logger TXN_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.TXN"); - - /** - * The behavior desired when ending a query only transaction. - */ - public enum OnQueryOnly { - - /** - * Rollback the transaction. - */ - ROLLBACK, - - /** - * Just close the transaction. - */ - CLOSE_ON_READCOMMITTED, - - /** - * Commit the transaction - */ - COMMIT - } - - protected final BeanDescriptorManager beanDescriptorManager; - - /** - * Prefix for transaction id's (logging). - */ - protected final String prefix; - - protected final String externalTransPrefix; - - /** - * The dataSource of connections. - */ - protected final DataSource dataSource; - - /** - * Flag to indicate the default Isolation is READ COMMITTED. This enables us - * to close queryOnly transactions rather than commit or rollback them. - */ - protected final OnQueryOnly onQueryOnly; - - protected final BackgroundExecutor backgroundExecutor; - - protected final ClusterManager clusterManager; - - protected final String serverName; - - protected final PersistBatch persistBatch; - - protected final PersistBatch persistBatchOnCascade; - - /** - * Id's for transaction logging. - */ - protected final AtomicLong transactionCounter = new AtomicLong(1000); - - protected final BulkEventListenerMap bulkEventListenerMap; - - protected final TransactionEventListener[] transactionEventListeners; - - /** - * Create the TransactionManager - */ - public TransactionManager(ClusterManager clusterManager, BackgroundExecutor backgroundExecutor, ServerConfig config, - BeanDescriptorManager descMgr, BootupClasses bootupClasses) { - - this.persistBatch = config.getPersistBatch(); - this.persistBatchOnCascade = config.appliedPersistBatchOnCascade(); - this.beanDescriptorManager = descMgr; - this.clusterManager = clusterManager; - this.serverName = config.getName(); - this.backgroundExecutor = backgroundExecutor; - this.dataSource = config.getDataSource(); - this.bulkEventListenerMap = new BulkEventListenerMap(config.getBulkTableEventListeners()); - - List transactionEventListeners = bootupClasses.getTransactionEventListeners(); - this.transactionEventListeners = transactionEventListeners.toArray(new TransactionEventListener[transactionEventListeners.size()]); - - this.prefix = ""; - this.externalTransPrefix = "e"; - - String value = System.getProperty("ebean.transaction.onqueryonly", "CLOSE").toUpperCase().trim(); - this.onQueryOnly = getOnQueryOnly(value, dataSource); - - initialiseHeartbeat(); - } - - private void initialiseHeartbeat() { - if (dataSource instanceof DataSourcePool) { - DataSourcePool ds = (DataSourcePool)dataSource; - backgroundExecutor.executePeriodically(ds.getHeartbeatRunnable(), ds.getHeartbeatFreqSecs(), TimeUnit.SECONDS); - } - } - - public void shutdown(boolean shutdownDataSource, boolean deregisterDriver) { - if (shutdownDataSource && (dataSource instanceof DataSourcePool)) { - ((DataSourcePool)dataSource).shutdown(deregisterDriver); - } - } - - public BeanDescriptorManager getBeanDescriptorManager() { - return beanDescriptorManager; - } - - public BulkEventListenerMap getBulkEventListenerMap() { - return bulkEventListenerMap; - } - - public PersistBatch getPersistBatch() { - return persistBatch; - } - - public PersistBatch getPersistBatchOnCascade() { - return persistBatchOnCascade; - } - - /** - * Return the behaviour to use when a query only transaction is committed. - *

- * There is a potential optimisation available when read committed is the default - * isolation level. If it is, then Connections used only for queries do not require - * commit or rollback but instead can just be put back into the pool via close(). - *

- *

- * If the Isolation level is higher (say SERIALIZABLE) then Connections used - * just for queries do need to be committed or rollback after the query. - *

- */ - private OnQueryOnly getOnQueryOnly(String onQueryOnly, DataSource ds) { - - if (onQueryOnly.equals("COMMIT")){ - return OnQueryOnly.COMMIT; - } - if (onQueryOnly.startsWith("CLOSE")){ - if (!isReadCommitedIsolation(ds)){ - String m = "transaction.queryonlyclose is true but the transaction Isolation Level is not READ_COMMITTED"; - throw new PersistenceException(m); - } else { - return OnQueryOnly.CLOSE_ON_READCOMMITTED; - } - } - // default to rollback - return OnQueryOnly.ROLLBACK; - } - - /** - * Return true if the isolation level is read committed. - */ - private boolean isReadCommitedIsolation(DataSource ds) { - - Connection c = null; - try { - c = ds.getConnection(); - - int isolationLevel = c.getTransactionIsolation(); - return (isolationLevel == Connection.TRANSACTION_READ_COMMITTED); - - } catch (SQLException ex) { - String m = "Errored trying to determine the default Isolation Level"; - throw new PersistenceException(m, ex); - - } finally { - try { - if (c != null) { - c.close(); - } - } catch (SQLException ex) { - logger.error("closing connection", ex); - } - } - } - - public String getServerName() { - return serverName; - } - - public DataSource getDataSource() { - return dataSource; - } - - /** - * Defines the type of behavior to use when closing a transaction that was used to query data only. - */ - public OnQueryOnly getOnQueryOnly() { - return onQueryOnly; - } - - /** - * Wrap the externally supplied Connection. - */ - public SpiTransaction wrapExternalConnection(Connection c) { - - return wrapExternalConnection(externalTransPrefix + c.hashCode(), c); - } - - /** - * Wrap an externally supplied Connection with a known transaction id. - */ - public SpiTransaction wrapExternalConnection(String id, Connection c) { - - ExternalJdbcTransaction t = new ExternalJdbcTransaction(id, true, c, this); - - // set the default batch mode - t.setBatch(persistBatch); - t.setBatchOnCascade(persistBatchOnCascade); - return t; - } - - /** - * Create a new Transaction. - */ - public SpiTransaction createTransaction(boolean explicit, int isolationLevel) { - Connection c = null; - try { - c = dataSource.getConnection(); - long id = transactionCounter.incrementAndGet(); - - SpiTransaction t = createTransaction(explicit, c, id); - if (isolationLevel > -1) { - c.setTransactionIsolation(isolationLevel); - } - - if (explicit && TXN_LOGGER.isTraceEnabled()) { - TXN_LOGGER.trace(t.getLogPrefix()+"Begin"); - } - - return t; - - } catch (SQLException ex) { - // close connection on failed creation - try { - if (c != null){ - c.close(); - } - } catch (SQLException e) { - logger.error("Error closing failed connection", e); - } - throw new PersistenceException(ex); - } - } - - public SpiTransaction createQueryTransaction() { - Connection c = null; - try { - c = dataSource.getConnection(); - long id = transactionCounter.incrementAndGet(); - - return createTransaction(false, c, id); - - } catch (PersistenceException ex) { - // close the connection and re-throw the exception - try { - if (c != null) { - c.close(); - } - } catch (SQLException e) { - logger.error("Error closing failed connection", e); - } - throw ex; - - } catch (SQLException ex) { - // don't need to close connection in this case - throw new PersistenceException(ex); - } - } - - /** - * Create a new transaction. - */ - protected SpiTransaction createTransaction(boolean explicit, Connection c, long id) { - return new JdbcTransaction(prefix + id, explicit, c, this); - } - - /** - * Process a local rolled back transaction. - */ - public void notifyOfRollback(SpiTransaction transaction, Throwable cause) { - - try { - if (TXN_LOGGER.isInfoEnabled()) { - String msg = transaction.getLogPrefix()+"Rollback"; - if (cause != null){ - msg += " error: "+formatThrowable(cause); - } - TXN_LOGGER.info(msg); - } - - for (TransactionEventListener listener : transactionEventListeners) { - listener.postTransactionRollback(transaction, cause); - } - - } catch (Exception ex) { - logger.error("Error while notifying TransactionEventListener of rollback event", ex); - } - } - - /** - * Query only transaction in read committed isolation. - */ - public void notifyOfQueryOnly(boolean onCommit, SpiTransaction transaction, Throwable cause) { - - // Nothing that interesting here - if (TXN_LOGGER.isTraceEnabled()) { - TXN_LOGGER.trace(transaction.getLogPrefix()+"Commit - query only"); - } - } - - private String formatThrowable(Throwable e){ - if (e == null){ - return ""; - } - StringBuilder sb = new StringBuilder(); - formatThrowable(e, sb); - return sb.toString(); - } - - private void formatThrowable(Throwable e, StringBuilder sb){ - - sb.append(e.toString()); - StackTraceElement[] stackTrace = e.getStackTrace(); - if (stackTrace.length > 0){ - sb.append(" stack0: "); - sb.append(stackTrace[0]); - } - Throwable cause = e.getCause(); - if (cause != null){ - sb.append(" cause: "); - formatThrowable(cause, sb); - } - } - - /** - * Process a local committed transaction. - */ - public void notifyOfCommit(SpiTransaction transaction) { - - try { - - if (transaction.isExplicit()) { - if (TXN_LOGGER.isInfoEnabled()) { - TXN_LOGGER.info(transaction.getLogPrefix()+"Commit"); - } - } else if (TXN_LOGGER.isDebugEnabled()) { - TXN_LOGGER.debug(transaction.getLogPrefix()+"Commit"); - } - - PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, transaction.getEvent()); - - postCommit.notifyLocalCacheIndex(); - postCommit.notifyCluster(); - - // cluster and text indexing - backgroundExecutor.execute(postCommit.notifyPersistListeners()); - - for (TransactionEventListener listener : transactionEventListeners) { - listener.postTransactionCommit(transaction); - } - - } catch (Exception ex) { - logger.error("NotifyOfCommit failed. L2 Cache potentially not notified.", ex); - } - } - - /** - * Process a Transaction that comes from another framework or local code. - *

- * For cases where raw SQL/JDBC or other frameworks are used this can - * invalidate the appropriate parts of the cache. - *

- */ - public void externalModification(TransactionEventTable tableEvents) { - - TransactionEvent event = new TransactionEvent(); - event.add(tableEvents); - - PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event); - - // invalidate parts of local cache and index - postCommit.notifyLocalCacheIndex(); - - backgroundExecutor.execute(postCommit.notifyPersistListeners()); - } - - /** - * Notify local BeanPersistListeners etc of events from another server in the cluster. - */ - public void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { - - if (logger.isDebugEnabled()) { - logger.debug("Cluster Received: " + remoteEvent.toString()); - } - - List tableIUDList = remoteEvent.getTableIUDList(); - if (tableIUDList != null) { - for (int i = 0; i < tableIUDList.size(); i++) { - TableIUD tableIUD = tableIUDList.get(i); - beanDescriptorManager.cacheNotify(tableIUD); - } - } - - List beanPersistList = remoteEvent.getBeanPersistList(); - if (beanPersistList != null) { - for (int i = 0; i < beanPersistList.size(); i++) { - BeanPersistIds beanPersist = beanPersistList.get(i); - beanPersist.notifyCacheAndListener(); - } - } - } - -} +package com.avaje.ebeaninternal.server.transaction; + +import com.avaje.ebean.BackgroundExecutor; +import com.avaje.ebean.config.PersistBatch; +import com.avaje.ebean.config.ServerConfig; +import com.avaje.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly; +import com.avaje.ebean.event.TransactionEventListener; +import com.avaje.ebeaninternal.api.SpiTransaction; +import com.avaje.ebeaninternal.api.TransactionEvent; +import com.avaje.ebeaninternal.api.TransactionEventTable; +import com.avaje.ebeaninternal.api.TransactionEventTable.TableIUD; +import com.avaje.ebeaninternal.server.cluster.ClusterManager; +import com.avaje.ebeaninternal.server.core.BootupClasses; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; +import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.PersistenceException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Manages transactions. + *

+ * Keeps the Cache and Cluster in synch when transactions are committed. + *

+ */ +public class TransactionManager { + + private static final Logger logger = LoggerFactory.getLogger(TransactionManager.class); + + public static final Logger SQL_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.SQL"); + + public static final Logger SUM_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.SUM"); + + public static final Logger TXN_LOGGER = LoggerFactory.getLogger("org.avaje.ebean.TXN"); + + protected final BeanDescriptorManager beanDescriptorManager; + + /** + * Prefix for transaction id's (logging). + */ + protected final String prefix; + + protected final String externalTransPrefix; + + /** + * The dataSource of connections. + */ + protected final DataSource dataSource; + + /** + * Flag to indicate the default Isolation is READ COMMITTED. This enables us + * to close queryOnly transactions rather than commit or rollback them. + */ + protected final OnQueryOnly onQueryOnly; + + protected final BackgroundExecutor backgroundExecutor; + + protected final ClusterManager clusterManager; + + protected final String serverName; + + protected final PersistBatch persistBatch; + + protected final PersistBatch persistBatchOnCascade; + + /** + * Id's for transaction logging. + */ + protected final AtomicLong transactionCounter = new AtomicLong(1000); + + protected final BulkEventListenerMap bulkEventListenerMap; + + protected final TransactionEventListener[] transactionEventListeners; + + /** + * Create the TransactionManager + */ + public TransactionManager(ClusterManager clusterManager, BackgroundExecutor backgroundExecutor, ServerConfig config, + BeanDescriptorManager descMgr, BootupClasses bootupClasses) { + + this.persistBatch = config.getPersistBatch(); + this.persistBatchOnCascade = config.appliedPersistBatchOnCascade(); + this.beanDescriptorManager = descMgr; + this.clusterManager = clusterManager; + this.serverName = config.getName(); + this.backgroundExecutor = backgroundExecutor; + this.dataSource = config.getDataSource(); + this.bulkEventListenerMap = new BulkEventListenerMap(config.getBulkTableEventListeners()); + + List transactionEventListeners = bootupClasses.getTransactionEventListeners(); + this.transactionEventListeners = transactionEventListeners.toArray(new TransactionEventListener[transactionEventListeners.size()]); + + this.prefix = ""; + this.externalTransPrefix = "e"; + + this.onQueryOnly = initOnQueryOnly(config.getDatabasePlatform().getOnQueryOnly(), dataSource); + + initialiseHeartbeat(); + } + + private void initialiseHeartbeat() { + if (dataSource instanceof DataSourcePool) { + DataSourcePool ds = (DataSourcePool)dataSource; + backgroundExecutor.executePeriodically(ds.getHeartbeatRunnable(), ds.getHeartbeatFreqSecs(), TimeUnit.SECONDS); + } + } + + public void shutdown(boolean shutdownDataSource, boolean deregisterDriver) { + if (shutdownDataSource && (dataSource instanceof DataSourcePool)) { + ((DataSourcePool)dataSource).shutdown(deregisterDriver); + } + } + + public BeanDescriptorManager getBeanDescriptorManager() { + return beanDescriptorManager; + } + + public BulkEventListenerMap getBulkEventListenerMap() { + return bulkEventListenerMap; + } + + public PersistBatch getPersistBatch() { + return persistBatch; + } + + public PersistBatch getPersistBatchOnCascade() { + return persistBatchOnCascade; + } + + /** + * Return the behaviour to use when a query only transaction is committed. + *

+ * There is a potential optimisation available when read committed is the default + * isolation level. If it is, then Connections used only for queries do not require + * commit or rollback but instead can just be put back into the pool via close(). + *

+ *

+ * If the Isolation level is higher (say SERIALIZABLE) then Connections used + * just for queries do need to be committed or rollback after the query. + *

+ */ + private OnQueryOnly initOnQueryOnly(OnQueryOnly dbPlatformOnQueryOnly, DataSource ds) { + + // first check for a system property 'override' + String systemPropertyValue = System.getProperty("ebean.transaction.onqueryonly"); + if (systemPropertyValue != null) { + return OnQueryOnly.valueOf(systemPropertyValue.trim().toUpperCase()); + } + + if (OnQueryOnly.CLOSE.equals(dbPlatformOnQueryOnly)) { + // check for read committed isolation level + if (!isReadCommitedIsolation(ds)){ + logger.warn("Ignoring DatabasePlatform.OnQueryOnly.CLOSE as the transaction Isolation Level is not READ_COMMITTED"); + // we will just use ROLLBACK and ignore the desired optimisation + return OnQueryOnly.ROLLBACK; + } else { + // will use the OnQueryOnly.CLOSE optimisation + return OnQueryOnly.CLOSE; + } + } + // default to rollback if not defined on the platform + return dbPlatformOnQueryOnly == null ? OnQueryOnly.ROLLBACK : dbPlatformOnQueryOnly; + } + + /** + * Return true if the isolation level is read committed. + */ + private boolean isReadCommitedIsolation(DataSource ds) { + + Connection c = null; + try { + c = ds.getConnection(); + + int isolationLevel = c.getTransactionIsolation(); + return (isolationLevel == Connection.TRANSACTION_READ_COMMITTED); + + } catch (SQLException ex) { + String m = "Errored trying to determine the default Isolation Level"; + throw new PersistenceException(m, ex); + + } finally { + try { + if (c != null) { + c.close(); + } + } catch (SQLException ex) { + logger.error("closing connection", ex); + } + } + } + + public String getServerName() { + return serverName; + } + + public DataSource getDataSource() { + return dataSource; + } + + /** + * Defines the type of behavior to use when closing a transaction that was used to query data only. + */ + public OnQueryOnly getOnQueryOnly() { + return onQueryOnly; + } + + /** + * Wrap the externally supplied Connection. + */ + public SpiTransaction wrapExternalConnection(Connection c) { + + return wrapExternalConnection(externalTransPrefix + c.hashCode(), c); + } + + /** + * Wrap an externally supplied Connection with a known transaction id. + */ + public SpiTransaction wrapExternalConnection(String id, Connection c) { + + ExternalJdbcTransaction t = new ExternalJdbcTransaction(id, true, c, this); + + // set the default batch mode + t.setBatch(persistBatch); + t.setBatchOnCascade(persistBatchOnCascade); + return t; + } + + /** + * Create a new Transaction. + */ + public SpiTransaction createTransaction(boolean explicit, int isolationLevel) { + Connection c = null; + try { + c = dataSource.getConnection(); + long id = transactionCounter.incrementAndGet(); + + SpiTransaction t = createTransaction(explicit, c, id); + if (isolationLevel > -1) { + c.setTransactionIsolation(isolationLevel); + } + + if (explicit && TXN_LOGGER.isTraceEnabled()) { + TXN_LOGGER.trace(t.getLogPrefix()+"Begin"); + } + + return t; + + } catch (SQLException ex) { + // close connection on failed creation + try { + if (c != null){ + c.close(); + } + } catch (SQLException e) { + logger.error("Error closing failed connection", e); + } + throw new PersistenceException(ex); + } + } + + public SpiTransaction createQueryTransaction() { + Connection c = null; + try { + c = dataSource.getConnection(); + long id = transactionCounter.incrementAndGet(); + + return createTransaction(false, c, id); + + } catch (PersistenceException ex) { + // close the connection and re-throw the exception + try { + if (c != null) { + c.close(); + } + } catch (SQLException e) { + logger.error("Error closing failed connection", e); + } + throw ex; + + } catch (SQLException ex) { + // don't need to close connection in this case + throw new PersistenceException(ex); + } + } + + /** + * Create a new transaction. + */ + protected SpiTransaction createTransaction(boolean explicit, Connection c, long id) { + return new JdbcTransaction(prefix + id, explicit, c, this); + } + + /** + * Process a local rolled back transaction. + */ + public void notifyOfRollback(SpiTransaction transaction, Throwable cause) { + + try { + if (TXN_LOGGER.isInfoEnabled()) { + String msg = transaction.getLogPrefix()+"Rollback"; + if (cause != null){ + msg += " error: "+formatThrowable(cause); + } + TXN_LOGGER.info(msg); + } + + for (TransactionEventListener listener : transactionEventListeners) { + listener.postTransactionRollback(transaction, cause); + } + + } catch (Exception ex) { + logger.error("Error while notifying TransactionEventListener of rollback event", ex); + } + } + + /** + * Query only transaction in read committed isolation. + */ + public void notifyOfQueryOnly(boolean onCommit, SpiTransaction transaction, Throwable cause) { + + // Nothing that interesting here + if (TXN_LOGGER.isTraceEnabled()) { + TXN_LOGGER.trace(transaction.getLogPrefix()+"Commit - query only"); + } + } + + private String formatThrowable(Throwable e){ + if (e == null){ + return ""; + } + StringBuilder sb = new StringBuilder(); + formatThrowable(e, sb); + return sb.toString(); + } + + private void formatThrowable(Throwable e, StringBuilder sb){ + + sb.append(e.toString()); + StackTraceElement[] stackTrace = e.getStackTrace(); + if (stackTrace.length > 0){ + sb.append(" stack0: "); + sb.append(stackTrace[0]); + } + Throwable cause = e.getCause(); + if (cause != null){ + sb.append(" cause: "); + formatThrowable(cause, sb); + } + } + + /** + * Process a local committed transaction. + */ + public void notifyOfCommit(SpiTransaction transaction) { + + try { + + if (transaction.isExplicit()) { + if (TXN_LOGGER.isInfoEnabled()) { + TXN_LOGGER.info(transaction.getLogPrefix()+"Commit"); + } + } else if (TXN_LOGGER.isDebugEnabled()) { + TXN_LOGGER.debug(transaction.getLogPrefix()+"Commit"); + } + + PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, transaction.getEvent()); + + postCommit.notifyLocalCacheIndex(); + postCommit.notifyCluster(); + + // cluster and text indexing + backgroundExecutor.execute(postCommit.notifyPersistListeners()); + + for (TransactionEventListener listener : transactionEventListeners) { + listener.postTransactionCommit(transaction); + } + + } catch (Exception ex) { + logger.error("NotifyOfCommit failed. L2 Cache potentially not notified.", ex); + } + } + + /** + * Process a Transaction that comes from another framework or local code. + *

+ * For cases where raw SQL/JDBC or other frameworks are used this can + * invalidate the appropriate parts of the cache. + *

+ */ + public void externalModification(TransactionEventTable tableEvents) { + + TransactionEvent event = new TransactionEvent(); + event.add(tableEvents); + + PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event); + + // invalidate parts of local cache and index + postCommit.notifyLocalCacheIndex(); + + backgroundExecutor.execute(postCommit.notifyPersistListeners()); + } + + /** + * Notify local BeanPersistListeners etc of events from another server in the cluster. + */ + public void remoteTransactionEvent(RemoteTransactionEvent remoteEvent) { + + if (logger.isDebugEnabled()) { + logger.debug("Cluster Received: " + remoteEvent.toString()); + } + + List tableIUDList = remoteEvent.getTableIUDList(); + if (tableIUDList != null) { + for (int i = 0; i < tableIUDList.size(); i++) { + TableIUD tableIUD = tableIUDList.get(i); + beanDescriptorManager.cacheNotify(tableIUD); + } + } + + List beanPersistList = remoteEvent.getBeanPersistList(); + if (beanPersistList != null) { + for (int i = 0; i < beanPersistList.size(); i++) { + BeanPersistIds beanPersist = beanPersistList.get(i); + beanPersist.notifyCacheAndListener(); + } + } + } + +}