diff --git a/src/org/hibernate/id/enhanced/TableGenerator.java b/src/org/hibernate/id/enhanced/TableGenerator.java index cce5d4dd30..78ff69a051 100644 --- a/src/org/hibernate/id/enhanced/TableGenerator.java +++ b/src/org/hibernate/id/enhanced/TableGenerator.java @@ -7,6 +7,8 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Properties; import java.util.HashMap; +import java.util.Map; +import java.util.Collections; import java.io.Serializable; import org.apache.commons.logging.Log; @@ -24,12 +26,28 @@ import org.hibernate.LockMode; import org.hibernate.mapping.Table; import org.hibernate.util.PropertiesHelper; import org.hibernate.util.StringHelper; -import org.hibernate.util.CollectionHelper; /** - * A "segmented" version of the enhanced table generator. The term "segmented" - * refers to the fact that this table can hold multiple value generators, - * segmented by a key. + * An enhanced version of table-based id generation. + *

+ * Unlike the simplistic legacy one (which, btw, was only ever intended for subclassing + * support) we "segment" the table into multiple values. Thus a single table can + * actually serve as the persistent storage for multiple independent generators. One + * approach would be to segment the values by the name of the entity for which we are + * performing generation, which would mean that we would have a row in the generator + * table for each entity name. Or any configuration really; the setup is very flexible. + *

+ * In this respect it is very simliar to the legacy + * {@link org.hibernate.id.MultipleHiLoPerTableGenerator} in terms of the + * underlying storage structure (namely a single table capable of holding + * multiple generator values). The differentiator is, as with + * {@link SequenceStyleGenerator} as well, the externalized notion + * of an optimizer. + *

+ * NOTE that by default we use a single row for all genertators (based + * on {@link #DEF_SEGMENT_VALUE}). The configuration parameter + * {@link #CONFIG_PREFER_SEGMENT_PER_ENTITY} can be used to change that to + * instead default to using a row for each entity name. *

* Configuration parameters: * @@ -85,6 +103,8 @@ import org.hibernate.util.CollectionHelper; public class TableGenerator extends TransactionHelper implements PersistentIdentifierGenerator, Configurable { private static final Log log = LogFactory.getLog( TableGenerator.class ); + public static final String CONFIG_PREFER_SEGMENT_PER_ENTITY = "hibernate.id.enhanced.table.prefer_segment_per_entity"; + public static final String TABLE_PARAM = "table_name"; public static final String DEF_TABLE = "hibernate_sequences"; @@ -109,101 +129,281 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent public static final String OPT_PARAM = "optimizer"; + private Type identifierType; + private String tableName; - private String valueColumnName; + private String segmentColumnName; private String segmentValue; private int segmentValueLength; + + private String valueColumnName; private int initialValue; private int incrementSize; - private Type identifierType; - - private String query; - private String insert; - private String update; + private String selectQuery; + private String insertQuery; + private String updateQuery; private Optimizer optimizer; private long accessCount = 0; - public String getTableName() { + /** + * {@inheritDoc} + */ + public Object generatorKey() { return tableName; } - public String getSegmentColumnName() { - return segmentColumnName; - } - - public String getSegmentValue() { - return segmentValue; - } - - public int getSegmentValueLength() { - return segmentValueLength; - } - - public String getValueColumnName() { - return valueColumnName; - } - - public Type getIdentifierType() { + /** + * Type mapping for the identifier. + * + * @return The identifier type mapping. + */ + public final Type getIdentifierType() { return identifierType; } - public int getInitialValue() { + /** + * The name of the table in which we store this generator's persistent state. + * + * @return The table name. + */ + public final String getTableName() { + return tableName; + } + + /** + * The name of the column in which we store the segment to which each row + * belongs. The value here acts as PK. + * + * @return The segment column name + */ + public final String getSegmentColumnName() { + return segmentColumnName; + } + + /** + * The value in {@link #getSegmentColumnName segment column} which + * corresponding to this generator instance. In other words this value + * indicates the row in which this generator instance will store values. + * + * @return The segment value for this generator instance. + */ + public final String getSegmentValue() { + return segmentValue; + } + + /** + * The size of the {@link #getSegmentColumnName segment column} in the + * underlying table. + *

+ * NOTE : should really have been called 'segmentColumnLength' or + * even better 'segmentColumnSize' + * + * @return the column size. + */ + public final int getSegmentValueLength() { + return segmentValueLength; + } + + /** + * The name of the column in which we store our persistent generator value. + * + * @return The name of the value column. + */ + public final String getValueColumnName() { + return valueColumnName; + } + + /** + * The initial value to use when we find no previous state in the + * generator table corresponding to our sequence. + * + * @return The initial value to use. + */ + public final int getInitialValue() { return initialValue; } - public int getIncrementSize() { + /** + * The amount of increment to use. The exact implications of this + * depends on the {@link #getOptimizer() optimizer} being used. + * + * @return The increment amount. + */ + public final int getIncrementSize() { return incrementSize; } - public Optimizer getOptimizer() { + /** + * The optimizer being used by this generator. + * + * @return Out optimizer. + */ + public final Optimizer getOptimizer() { return optimizer; } - public long getTableAccessCount() { + /** + * Getter for property 'tableAccessCount'. Only really useful for unit test + * assertions. + * + * @return Value for property 'tableAccessCount'. + */ + public final long getTableAccessCount() { return accessCount; } + /** + * {@inheritDoc} + */ public void configure(Type type, Properties params, Dialect dialect) throws MappingException { - tableName = PropertiesHelper.getString( TABLE_PARAM, params, DEF_TABLE ); - if ( tableName.indexOf( '.' ) < 0 ) { - String schemaName = params.getProperty( SCHEMA ); - String catalogName = params.getProperty( CATALOG ); - tableName = Table.qualify( catalogName, schemaName, tableName ); - } - - segmentColumnName = PropertiesHelper.getString( SEGMENT_COLUMN_PARAM, params, DEF_SEGMENT_COLUMN ); - segmentValue = params.getProperty( SEGMENT_VALUE_PARAM ); - if ( StringHelper.isEmpty( segmentValue ) ) { - log.debug( "explicit segment value for id generator [" + tableName + '.' + segmentColumnName + "] suggested; using default [" + DEF_SEGMENT_VALUE + "]" ); - segmentValue = DEF_SEGMENT_VALUE; - } - segmentValueLength = PropertiesHelper.getInt( SEGMENT_LENGTH_PARAM, params, DEF_SEGMENT_LENGTH ); - valueColumnName = PropertiesHelper.getString( VALUE_COLUMN_PARAM, params, DEF_VALUE_COLUMN ); - initialValue = PropertiesHelper.getInt( INITIAL_PARAM, params, DEFAULT_INITIAL_VALUE ); - incrementSize = PropertiesHelper.getInt( INCREMENT_PARAM, params, DEFAULT_INCREMENT_SIZE ); identifierType = type; - String query = "select " + valueColumnName + - " from " + tableName + " tbl" + - " where tbl." + segmentColumnName + "=?"; - HashMap lockMap = new HashMap(); - lockMap.put( "tbl", LockMode.UPGRADE ); - this.query = dialect.applyLocksToSql( query, lockMap, CollectionHelper.EMPTY_MAP ); + tableName = determneGeneratorTableName( params ); + segmentColumnName = determineSegmentColumnName( params ); + valueColumnName = determineValueColumnName( params ); - update = "update " + tableName + - " set " + valueColumnName + "=? " + - " where " + valueColumnName + "=? and " + segmentColumnName + "=?"; + segmentValue = determineSegmentValue( params ); - insert = "insert into " + tableName + " (" + segmentColumnName + ", " + valueColumnName + ") " + " values (?,?)"; + segmentValueLength = determineSegmentColumnSize( params ); + initialValue = determineInitialValue( params ); + incrementSize = determineIncrementSize( params ); + + this.selectQuery = buildSelectQuery( dialect ); + this.updateQuery = buildUpdateQuery(); + this.insertQuery = buildInsertQuery(); String defOptStrategy = incrementSize <= 1 ? OptimizerFactory.NONE : OptimizerFactory.POOL; String optimizationStrategy = PropertiesHelper.getString( OPT_PARAM, params, defOptStrategy ); optimizer = OptimizerFactory.buildOptimizer( optimizationStrategy, identifierType.getReturnedClass(), incrementSize ); } + /** + * Determine the table name to use for the generator values. + *

+ * Called during {@link #configure configuration}. + * + * @see #getTableName() + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The table name to use. + */ + protected String determneGeneratorTableName(Properties params) { + String name = PropertiesHelper.getString( TABLE_PARAM, params, DEF_TABLE ); + boolean isGivenNameUnqualified = name.indexOf( '.' ) < 0; + if ( isGivenNameUnqualified ) { + // if the given name is un-qualified we may neen to qualify it + String schemaName = params.getProperty( SCHEMA ); + String catalogName = params.getProperty( CATALOG ); + name = Table.qualify( catalogName, schemaName, name ); + } + return name; + } + + /** + * Determine the name of the column used to indicate the segment for each + * row. This column acts as the primary key. + *

+ * Called during {@link #configure configuration}. + * + * @see #getSegmentColumnName() + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The name of the segment column + */ + protected String determineSegmentColumnName(Properties params) { + return PropertiesHelper.getString( SEGMENT_COLUMN_PARAM, params, DEF_SEGMENT_COLUMN ); + } + + /** + * Determine the name of the column in which we will store the generator persistent value. + *

+ * Called during {@link #configure configuration}. + * + * @see #getValueColumnName() + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The name of the value column + */ + protected String determineValueColumnName(Properties params) { + return PropertiesHelper.getString( VALUE_COLUMN_PARAM, params, DEF_VALUE_COLUMN ); + } + + /** + * Determine the segment value corresponding to this generator instance. + *

+ * Called during {@link #configure configuration}. + * + * @see #getSegmentValue() + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The name of the value column + */ + protected String determineSegmentValue(Properties params) { + String segmentValue = params.getProperty( SEGMENT_VALUE_PARAM ); + if ( StringHelper.isEmpty( segmentValue ) ) { + segmentValue = determineDefaultSegmentValue( params ); + } + return segmentValue; + } + + /** + * Used in the cases where {@link #determineSegmentValue} is unable to + * determine the value to use. + * + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The default segment value to use. + */ + protected String determineDefaultSegmentValue(Properties params) { + boolean preferSegmentPerEntity = PropertiesHelper.getBoolean( CONFIG_PREFER_SEGMENT_PER_ENTITY, params, false ); + String defaultToUse = preferSegmentPerEntity ? params.getProperty( TABLE ) : DEF_SEGMENT_VALUE; + log.info( "explicit segment value for id generator [" + tableName + '.' + segmentColumnName + "] suggested; using default [" + defaultToUse + "]" ); + return defaultToUse; + } + + /** + * Determine the size of the {@link #getSegmentColumnName segment column} + *

+ * Called during {@link #configure configuration}. + * + * @see #getSegmentValueLength() + * @param params The params supplied in the generator config (plus some standard useful extras). + * @return The size of the segment column + */ + protected int determineSegmentColumnSize(Properties params) { + return PropertiesHelper.getInt( SEGMENT_LENGTH_PARAM, params, DEF_SEGMENT_LENGTH ); + } + + protected int determineInitialValue(Properties params) { + return PropertiesHelper.getInt( INITIAL_PARAM, params, DEFAULT_INITIAL_VALUE ); + } + + protected int determineIncrementSize(Properties params) { + return PropertiesHelper.getInt( INCREMENT_PARAM, params, DEFAULT_INCREMENT_SIZE ); + } + + protected String buildSelectQuery(Dialect dialect) { + final String alias = "tbl"; + String query = "select " + StringHelper.qualify( alias, valueColumnName ) + + " from " + tableName + ' ' + alias + + " where " + StringHelper.qualify( alias, segmentColumnName ) + "=?"; + HashMap lockMap = new HashMap(); + lockMap.put( alias, LockMode.UPGRADE ); + Map updateTargetColumnsMap = Collections.singletonMap( alias, new String[] { valueColumnName } ); + return dialect.applyLocksToSql( query, lockMap, updateTargetColumnsMap ); + } + + protected String buildUpdateQuery() { + return "update " + tableName + + " set " + valueColumnName + "=? " + + " where " + valueColumnName + "=? and " + segmentColumnName + "=?"; + } + + protected String buildInsertQuery() { + return "insert into " + tableName + " (" + segmentColumnName + ", " + valueColumnName + ") " + " values (?,?)"; + } + + /** + * {@inheritDoc} + */ public synchronized Serializable generate(final SessionImplementor session, Object obj) { return optimizer.generate( new AccessCallback() { @@ -214,23 +414,24 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent ); } + /** + * {@inheritDoc} + */ public Serializable doWorkInCurrentTransaction(Connection conn, String sql) throws SQLException { int result; int rows; do { - sql = query; - SQL.debug( sql ); - PreparedStatement queryPS = conn.prepareStatement( query ); + SQL.debug( selectQuery ); + PreparedStatement selectPS = conn.prepareStatement( selectQuery ); try { - queryPS.setString( 1, segmentValue ); - ResultSet queryRS = queryPS.executeQuery(); - if ( !queryRS.next() ) { + selectPS.setString( 1, segmentValue ); + ResultSet selectRS = selectPS.executeQuery(); + if ( !selectRS.next() ) { PreparedStatement insertPS = null; try { result = initialValue; - sql = insert; - SQL.debug( sql ); - insertPS = conn.prepareStatement( insert ); + SQL.debug( insertQuery ); + insertPS = conn.prepareStatement( insertQuery ); insertPS.setString( 1, segmentValue ); insertPS.setLong( 2, result ); insertPS.execute(); @@ -242,21 +443,20 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent } } else { - result = queryRS.getInt( 1 ); + result = selectRS.getInt( 1 ); } - queryRS.close(); + selectRS.close(); } catch ( SQLException sqle ) { log.error( "could not read or init a hi value", sqle ); throw sqle; } finally { - queryPS.close(); + selectPS.close(); } - sql = update; - SQL.debug( sql ); - PreparedStatement updatePS = conn.prepareStatement( update ); + SQL.debug( updateQuery ); + PreparedStatement updatePS = conn.prepareStatement( updateQuery ); try { long newValue = optimizer.applyIncrementSizeToSourceValues() ? result + incrementSize : result + 1; @@ -280,6 +480,9 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent return new Integer( result ); } + /** + * {@inheritDoc} + */ public String[] sqlCreateStrings(Dialect dialect) throws HibernateException { return new String[] { new StringBuffer() @@ -294,11 +497,16 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent .append( valueColumnName ) .append( ' ' ) .append( dialect.getTypeName( Types.BIGINT ) ) - .append( " ) " ) + .append( ", primary key ( " ) + .append( segmentColumnName ) + .append( " ) ) " ) .toString() }; } + /** + * {@inheritDoc} + */ public String[] sqlDropStrings(Dialect dialect) throws HibernateException { StringBuffer sqlDropString = new StringBuffer().append( "drop table " ); if ( dialect.supportsIfExistsBeforeTableName() ) { @@ -310,8 +518,4 @@ public class TableGenerator extends TransactionHelper implements PersistentIdent } return new String[] { sqlDropString.toString() }; } - - public Object generatorKey() { - return tableName; - } }