All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.id.enhanced.TableGenerator Maven / Gradle / Ivy

There is a newer version: 6.5.0.CR2
Show newest version
/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.id.enhanced;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.ObjectNameNormalizer;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.internal.FormatStyle;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.jdbc.spi.SqlStatementLogger;
import org.hibernate.engine.spi.SessionEventListenerManager;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.id.Configurable;
import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.id.IntegralDataTypeHolder;
import org.hibernate.id.PersistentIdentifierGenerator;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.jdbc.AbstractReturningWork;
import org.hibernate.mapping.Table;
import org.hibernate.type.Type;

import org.jboss.logging.Logger;

/**
 * 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 similar 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 generators (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: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
NAMEDEFAULTDESCRIPTION
{@link #TABLE_PARAM}{@link #DEF_TABLE}The name of the table to use to store/retrieve values
{@link #VALUE_COLUMN_PARAM}{@link #DEF_VALUE_COLUMN}The name of column which holds the sequence value for the given segment
{@link #SEGMENT_COLUMN_PARAM}{@link #DEF_SEGMENT_COLUMN}The name of the column which holds the segment key
{@link #SEGMENT_VALUE_PARAM}{@link #DEF_SEGMENT_VALUE}The value indicating which segment is used by this generator; refers to values in the {@link #SEGMENT_COLUMN_PARAM} column
{@link #SEGMENT_LENGTH_PARAM}{@link #DEF_SEGMENT_LENGTH}The data length of the {@link #SEGMENT_COLUMN_PARAM} column; used for schema creation
{@link #INITIAL_PARAM}{@link #DEFAULT_INITIAL_VALUE}The initial value to be stored for the given segment
{@link #INCREMENT_PARAM}{@link #DEFAULT_INCREMENT_SIZE}The increment size for the underlying segment; see the discussion on {@link Optimizer} for more details.
{@link #OPT_PARAM}depends on defined increment sizeAllows explicit definition of which optimization strategy to use
* * @author Steve Ebersole */ public class TableGenerator implements PersistentIdentifierGenerator, Configurable { private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, TableGenerator.class.getName() ); /** * By default (in the absence of a {@link #SEGMENT_VALUE_PARAM} setting) we use a single row for all * generators. This setting can be used to change that to instead default to using a row for each entity name. */ public static final String CONFIG_PREFER_SEGMENT_PER_ENTITY = "prefer_entity_table_as_segment_value"; /** * Configures the name of the table to use. The default value is {@link #DEF_TABLE} */ public static final String TABLE_PARAM = "table_name"; /** * The default {@link #TABLE_PARAM} value */ public static final String DEF_TABLE = "hibernate_sequences"; /** * The name of column which holds the sequence value. The default value is {@link #DEF_VALUE_COLUMN} */ public static final String VALUE_COLUMN_PARAM = "value_column_name"; /** * The default {@link #VALUE_COLUMN_PARAM} value */ public static final String DEF_VALUE_COLUMN = "next_val"; /** * The name of the column which holds the segment key. The segment defines the different buckets (segments) * of values currently tracked in the table. The default value is {@link #DEF_SEGMENT_COLUMN} */ public static final String SEGMENT_COLUMN_PARAM = "segment_column_name"; /** * The default {@link #SEGMENT_COLUMN_PARAM} value */ public static final String DEF_SEGMENT_COLUMN = "sequence_name"; /** * The value indicating which segment is used by this generator, as indicated by the actual value stored in the * column indicated by {@link #SEGMENT_COLUMN_PARAM}. The default value for setting is {@link #DEF_SEGMENT_VALUE}, * although {@link #CONFIG_PREFER_SEGMENT_PER_ENTITY} effects the default as well. */ public static final String SEGMENT_VALUE_PARAM = "segment_value"; /** * The default {@link #SEGMENT_VALUE_PARAM} value, unless {@link #CONFIG_PREFER_SEGMENT_PER_ENTITY} is specified */ public static final String DEF_SEGMENT_VALUE = "default"; /** * Indicates the length of the column defined by {@link #SEGMENT_COLUMN_PARAM}. Used in schema export. The * default value is {@link #DEF_SEGMENT_LENGTH} */ public static final String SEGMENT_LENGTH_PARAM = "segment_value_length"; /** * The default {@link #SEGMENT_LENGTH_PARAM} value */ public static final int DEF_SEGMENT_LENGTH = 255; /** * Indicates the initial value to use. The default value is {@link #DEFAULT_INITIAL_VALUE} */ public static final String INITIAL_PARAM = "initial_value"; /** * The default {@link #INITIAL_PARAM} value */ public static final int DEFAULT_INITIAL_VALUE = 1; /** * Indicates the increment size to use. The default value is {@link #DEFAULT_INCREMENT_SIZE} */ public static final String INCREMENT_PARAM = "increment_size"; /** * The default {@link #INCREMENT_PARAM} value */ public static final int DEFAULT_INCREMENT_SIZE = 1; /** * Indicates the optimizer to use, either naming a {@link Optimizer} implementation class or by naming * a {@link StandardOptimizerDescriptor} by name */ public static final String OPT_PARAM = "optimizer"; private Type identifierType; private String tableName; private String segmentColumnName; private String segmentValue; private int segmentValueLength; private String valueColumnName; private int initialValue; private int incrementSize; private String selectQuery; private String insertQuery; private String updateQuery; private Optimizer optimizer; private long accessCount; @Override public Object generatorKey() { return tableName; } /** * Type mapping for the identifier. * * @return The identifier type mapping. */ public final Type getIdentifierType() { return identifierType; } /** * 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; } /** * 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; } /** * The optimizer being used by this generator. * * @return Out optimizer. */ public final Optimizer getOptimizer() { return optimizer; } /** * Getter for property 'tableAccessCount'. Only really useful for unit test * assertions. * * @return Value for property 'tableAccessCount'. */ public final long getTableAccessCount() { return accessCount; } @Override public void configure(Type type, Properties params, Dialect dialect) throws MappingException { identifierType = type; tableName = determineGeneratorTableName( params, dialect ); segmentColumnName = determineSegmentColumnName( params, dialect ); valueColumnName = determineValueColumnName( params, dialect ); segmentValue = determineSegmentValue( params ); segmentValueLength = determineSegmentColumnSize( params ); initialValue = determineInitialValue( params ); incrementSize = determineIncrementSize( params ); this.selectQuery = buildSelectQuery( dialect ); this.updateQuery = buildUpdateQuery(); this.insertQuery = buildInsertQuery(); // if the increment size is greater than one, we prefer pooled optimization; but we // need to see if the user prefers POOL or POOL_LO... final String defaultPooledOptimizerStrategy = ConfigurationHelper.getBoolean( Environment.PREFER_POOLED_VALUES_LO, params, false ) ? StandardOptimizerDescriptor.POOLED_LO.getExternalName() : StandardOptimizerDescriptor.POOLED.getExternalName(); final String defaultOptimizerStrategy = incrementSize <= 1 ? StandardOptimizerDescriptor.NONE.getExternalName() : defaultPooledOptimizerStrategy; final String optimizationStrategy = ConfigurationHelper.getString( OPT_PARAM, params, defaultOptimizerStrategy ); optimizer = OptimizerFactory.buildOptimizer( optimizationStrategy, identifierType.getReturnedClass(), incrementSize, ConfigurationHelper.getInt( INITIAL_PARAM, params, -1 ) ); } /** * 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). * @param dialect The dialect in effect * @return The table name to use. */ protected String determineGeneratorTableName(Properties params, Dialect dialect) { String name = ConfigurationHelper.getString( TABLE_PARAM, params, DEF_TABLE ); final boolean isGivenNameUnqualified = name.indexOf( '.' ) < 0; if ( isGivenNameUnqualified ) { final ObjectNameNormalizer normalizer = (ObjectNameNormalizer) params.get( IDENTIFIER_NORMALIZER ); name = normalizer.normalizeIdentifierQuoting( name ); // if the given name is un-qualified we may neen to qualify it final String schemaName = normalizer.normalizeIdentifierQuoting( params.getProperty( SCHEMA ) ); final String catalogName = normalizer.normalizeIdentifierQuoting( params.getProperty( CATALOG ) ); name = Table.qualify( dialect.quote( catalogName ), dialect.quote( schemaName ), dialect.quote( name) ); } // if already qualified there is not much we can do in a portable manner so we pass it // through and assume the user has set up the name correctly. 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). * @param dialect The dialect in effect * @return The name of the segment column */ protected String determineSegmentColumnName(Properties params, Dialect dialect) { final ObjectNameNormalizer normalizer = (ObjectNameNormalizer) params.get( IDENTIFIER_NORMALIZER ); final String name = ConfigurationHelper.getString( SEGMENT_COLUMN_PARAM, params, DEF_SEGMENT_COLUMN ); return dialect.quote( normalizer.normalizeIdentifierQuoting( name ) ); } /** * 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). * @param dialect The dialect in effect * @return The name of the value column */ protected String determineValueColumnName(Properties params, Dialect dialect) { final ObjectNameNormalizer normalizer = (ObjectNameNormalizer) params.get( IDENTIFIER_NORMALIZER ); final String name = ConfigurationHelper.getString( VALUE_COLUMN_PARAM, params, DEF_VALUE_COLUMN ); return dialect.quote( normalizer.normalizeIdentifierQuoting( name ) ); } /** * 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) { final boolean preferSegmentPerEntity = ConfigurationHelper.getBoolean( CONFIG_PREFER_SEGMENT_PER_ENTITY, params, false ); final String defaultToUse = preferSegmentPerEntity ? params.getProperty( TABLE ) : DEF_SEGMENT_VALUE; LOG.usingDefaultIdGeneratorSegmentValue( tableName, segmentColumnName, 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 ConfigurationHelper.getInt( SEGMENT_LENGTH_PARAM, params, DEF_SEGMENT_LENGTH ); } protected int determineInitialValue(Properties params) { return ConfigurationHelper.getInt( INITIAL_PARAM, params, DEFAULT_INITIAL_VALUE ); } protected int determineIncrementSize(Properties params) { return ConfigurationHelper.getInt( INCREMENT_PARAM, params, DEFAULT_INCREMENT_SIZE ); } protected String buildSelectQuery(Dialect dialect) { final String alias = "tbl"; final String query = "select " + StringHelper.qualify( alias, valueColumnName ) + " from " + tableName + ' ' + alias + " where " + StringHelper.qualify( alias, segmentColumnName ) + "=?"; final LockOptions lockOptions = new LockOptions( LockMode.PESSIMISTIC_WRITE ); lockOptions.setAliasSpecificLockMode( alias, LockMode.PESSIMISTIC_WRITE ); final Map updateTargetColumnsMap = Collections.singletonMap( alias, new String[] { valueColumnName } ); return dialect.applyLocksToSql( query, lockOptions, updateTargetColumnsMap ); } protected String buildUpdateQuery() { return "update " + tableName + " set " + valueColumnName + "=? " + " where " + valueColumnName + "=? and " + segmentColumnName + "=?"; } protected String buildInsertQuery() { return "insert into " + tableName + " (" + segmentColumnName + ", " + valueColumnName + ") " + " values (?,?)"; } private IntegralDataTypeHolder makeValue() { return IdentifierGeneratorHelper.getIntegralDataTypeHolder( identifierType.getReturnedClass() ); } @Override public Serializable generate(final SessionImplementor session, final Object obj) { final SqlStatementLogger statementLogger = session.getFactory().getServiceRegistry() .getService( JdbcServices.class ) .getSqlStatementLogger(); final SessionEventListenerManager statsCollector = session.getEventListenerManager(); return optimizer.generate( new AccessCallback() { @Override public IntegralDataTypeHolder getNextValue() { return session.getTransactionCoordinator().getTransaction().createIsolationDelegate().delegateWork( new AbstractReturningWork() { @Override public IntegralDataTypeHolder execute(Connection connection) throws SQLException { final IntegralDataTypeHolder value = makeValue(); int rows; do { final PreparedStatement selectPS = prepareStatement( connection, selectQuery, statementLogger, statsCollector ); try { selectPS.setString( 1, segmentValue ); final ResultSet selectRS = executeQuery( selectPS, statsCollector ); if ( !selectRS.next() ) { value.initialize( initialValue ); final PreparedStatement insertPS = prepareStatement( connection, insertQuery, statementLogger, statsCollector ); try { insertPS.setString( 1, segmentValue ); value.bind( insertPS, 2 ); executeUpdate( insertPS, statsCollector ); } finally { insertPS.close(); } } else { value.initialize( selectRS, 1 ); } selectRS.close(); } catch (SQLException e) { LOG.unableToReadOrInitHiValue( e ); throw e; } finally { selectPS.close(); } final PreparedStatement updatePS = prepareStatement( connection, updateQuery, statementLogger, statsCollector ); try { final IntegralDataTypeHolder updateValue = value.copy(); if ( optimizer.applyIncrementSizeToSourceValues() ) { updateValue.add( incrementSize ); } else { updateValue.increment(); } updateValue.bind( updatePS, 1 ); value.bind( updatePS, 2 ); updatePS.setString( 3, segmentValue ); rows = executeUpdate( updatePS, statsCollector ); } catch (SQLException e) { LOG.unableToUpdateQueryHiValue( tableName, e ); throw e; } finally { updatePS.close(); } } while ( rows == 0 ); accessCount++; return value; } }, true ); } @Override public String getTenantIdentifier() { return session.getTenantIdentifier(); } } ); } private PreparedStatement prepareStatement( Connection connection, String sql, SqlStatementLogger statementLogger, SessionEventListenerManager statsCollector) throws SQLException { statementLogger.logStatement( sql, FormatStyle.BASIC.getFormatter() ); try { statsCollector.jdbcPrepareStatementStart(); return connection.prepareStatement( sql ); } finally { statsCollector.jdbcPrepareStatementEnd(); } } private int executeUpdate(PreparedStatement ps, SessionEventListenerManager statsCollector) throws SQLException { try { statsCollector.jdbcExecuteStatementStart(); return ps.executeUpdate(); } finally { statsCollector.jdbcExecuteStatementEnd(); } } private ResultSet executeQuery(PreparedStatement ps, SessionEventListenerManager statsCollector) throws SQLException { try { statsCollector.jdbcExecuteStatementStart(); return ps.executeQuery(); } finally { statsCollector.jdbcExecuteStatementEnd(); } } @Override public String[] sqlCreateStrings(Dialect dialect) throws HibernateException { return new String[] { dialect.getCreateTableString() + ' ' + tableName + " ( " + segmentColumnName + ' ' + dialect.getTypeName( Types.VARCHAR, segmentValueLength, 0, 0 ) + " not null " + ", " + valueColumnName + ' ' + dialect.getTypeName( Types.BIGINT ) + ", primary key ( " + segmentColumnName + " ) )" + dialect.getTableTypeString() }; } @Override public String[] sqlDropStrings(Dialect dialect) throws HibernateException { return new String[] { dialect.getDropTableString( tableName ) }; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy