com.literatejava.hibernate.allocator.LinearBlockAllocator Maven / Gradle / Ivy
Show all versions of linear-block-allocator Show documentation
package com.literatejava.hibernate.allocator;
/*
* Linear Block Allocator; a superior & portable ID allocator for Hibernate.
* http://literatejava.com/hibernate/linear-block-allocator-a-superior-alternative-to-hilo/
*
* Copyright (c) 2005-2015, Tom Whitmore/ LiterateJava.com and other contributors
* as indicated by @author tags or express copyright attribution statements.
* This implementation includes sources adapted from Hibernate, copyright (c)
* 2008 Red Hat Middleware LLC. All third-party contributions are distributed
* under license.
*
* 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
*/
import java.io.Serializable;
import java.sql.*;
import java.util.Properties;
import org.hibernate.*;
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.SessionImplementor;
import org.hibernate.id.*;
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;
/**
* Allocate Long or Integer keys, using a portable table-based algorithm.
*
* - This strategy is highly efficient, database-portable, and simplifies/ supersedes HiLo-type strategies.
*
- Keys are allocated in blocks from an "allocator" table, with a current block held in memory; used by the application as required.
*
- Default block-size of 20 outperforms SEQUENCE allocation by a factor of 10 or more, using only standard SQL & fully portable
* between databases.
*
- Block allocation is multiuser & cluster-safe; database contention is handled by retry.
*
- This allocator requires the ability to obtain a Session-independent connection to the database. This is
* possible under most common configurations where connection acquisition is under Hibernate control. If
* Hibernate is being driven with user-supplied connections, another generation strategy should be chosen.
*
*
*
* Allocation using the "linear block" algorithm, can be understood as allocating blocks from a linear number-line. NEXT_VAL in the database
* represents the start of the next block. Blocks are allocated by simply incrementing the counter in a concurrency-safe manner. Block-based
* allocation and portable high performance are achieved simply, without unnecessary complexities.
*
*
* Compared to HiLo, "linear block allocation" treats keyspace as the linear number-line it fundamentally is, rather than breaking
* it into a two-dimensional keyspace (the separate "hi" & "lo" words). Keeping NEXT_VAL the same type & magnitude as the actual keys
* simplifies maintenance, reduces multiplication to simple addition, and removes unnecessary complexity. There is no performance advantage or
* any other benefit to justify HiLo's more complicated number-space & design; it is merely a flawed concept rejected by Occam's razor.
*
*
* Database operation & maintenance are easy. With linear block allocation, "allocator state" and existing keys are always in direct correspondence.
* NEXT_VAL corresponds directly to MAX(existing keys); and must always be greater. Bulk inserts, validity checking or manual updates to the allocator
* are obvious & easy. Unlike Hi-Lo, tuning or changing block-size are possible without losing allocator position & database integrity.
* Since NEXT_VAL represents values directly (rather than via multiplier) changing block-size does not affect the next key to be allocated.
*
*
* LinearBlockAllocator also generates better human-friendly keys. Block-sizes default to human-readable decimal-based (20, 100 etc) sizes --
* there is no forced bias towards large binary numbers! Key wastage is decreased & restarting your server twice allocates customer=60, not customer=98304 :)
* Imagine how much easier development will be, without having to type stupid large ugly numbers for all keys.
*
*
* LinearBlockAllocator is configured with two groups of parameters -- those defining the allocator table, and those selecting/controlling allocation for the specific sequence.
*
*
* Allocator table definition:
*
* - table -- allocator table to use, default "KEY_ALLOC"
*
- sequenceColumn -- sequence-name column, default "SEQ"
*
- allocColumn -- next-block column, default "NEXT_VAL "
*
* Sequence selection:
*
* - sequenceName -- sequence-name to use; defaults to table-name of the Hibernate entity
*
- blockSize -- block-size (# of keys) to cache in memory; default 20
*
*
* Allocator table can contain multiple "allocation sequences", keyed by "name". Many applications can use a single allocator table to store
* all their sequences. Each Hibernate generator is configured independently, so shared or independent allocator tables are equally able to
* be configured.
*
*
* Block-size is the key parameter controlling performance. Larger block-sizes increase performance, at the expense of a slight increase in unused keys
* lost on server restart. Increasing the block-size to 200 achieve most of the performance benefits practically possible; entities/rows must still be
* INSERT'ed into the database.
*
*
* Compared with vendor-specific strategies such as Oracle SEQUENCE, "LinearBlockAllocator" approaches double the performance (half the number of database
* operations per INSERT) in a completely portable & SQL-standard manner.
*
*
*
* @see TableHiLoGenerator
* @author Tom Whitmore
* @author Hibernate implementation of this algorithm includes code adapted from Hibernate sources/ Gavin King, (c) 2008 Red Hat Middleware LLC.
*/
public class LinearBlockAllocator
/*extends TransactionHelper*/
implements PersistentIdentifierGenerator, Configurable
{
/* COLUMN and TABLE should be renamed but it would break the public API */
public static final String ALLOC_TABLE = "table";
public static final String SEQUENCE_COLUMN = "sequenceColumn";
public static final String ALLOC_COLUMN = "allocColumn";
//
public static final String SEQUENCE_NAME = "sequenceName";
public static final String BLOCK_SIZE = "blockSize";
/** default Table & Column names */
public static final String DEFAULT_TABLE = "KEY_ALLOC";
public static final String DEFAULT_SEQUENCE_COLUMN = "SEQ";
public static final String DEFAULT_ALLOC_COLUMN = "NEXT_VAL";
public static final int DEFAULT_BLOCK_SIZE = 100;
/** internal defaults */
private static final int DEFAULT_SEQUENCE_COLUMNLENGTH = 128;
// - DB configuration
protected String tableName;
protected String sequenceColumn;
protected String allocColumn;
protected String sequenceName;
protected int blockSize;
// - identifier/ allocator Type.
protected Type returnType;
// - compiled
protected String query;
protected String update;
protected Class returnClass;
// - current allocation state;
// ENG NOTE -- uses 'long' internally for efficiency, allocation of super-big keys would require IntegralDataTypeHolder for these fields instead.
protected long allocNext;
protected long allocHi;
protected IntegralDataTypeHolder resultFactory;
// - statistics; mainly for unit testing.
protected long statisticsTableAccessCount;
// -
private static final Logger log = Logger.getLogger( LinearBlockAllocator.class);
public void configure (Type type, Properties params, Dialect dialect) {
ObjectNameNormalizer normalizer = (ObjectNameNormalizer) params.get( IDENTIFIER_NORMALIZER);
this.tableName = ConfigurationHelper.getString( ALLOC_TABLE, params, DEFAULT_TABLE);
this.sequenceColumn = ConfigurationHelper.getString( SEQUENCE_COLUMN, params, DEFAULT_SEQUENCE_COLUMN);
this.allocColumn = ConfigurationHelper.getString( ALLOC_COLUMN, params, DEFAULT_ALLOC_COLUMN);
// get SequenceName; default to Entities' TableName.
// -
this.sequenceName = ConfigurationHelper.getString( SEQUENCE_NAME, params, params.getProperty(PersistentIdentifierGenerator.TABLE));
if (sequenceName == null) {
throw new IdentifierGenerationException( "LinearBlockAllocator: '"+SEQUENCE_NAME+"' must be specified");
}
this.blockSize = ConfigurationHelper.getInt( BLOCK_SIZE, params, DEFAULT_BLOCK_SIZE);
if (blockSize < 1) {
blockSize = 1;
}
// qualify Table Name, if necessary;
// --
if (tableName.indexOf( '.') < 0) {
String schemaName = normalizer.normalizeIdentifierQuoting( params.getProperty( SCHEMA));
String catalogName = normalizer.normalizeIdentifierQuoting( params.getProperty( CATALOG));
this.tableName = Table.qualify( catalogName, schemaName, tableName);
}
// build SQL strings;
// -- use appendLockHint(LockMode) for now, as that is how Hibernate's generators do it.
//
this.query = "select " + allocColumn +
" from " + dialect.appendLockHint( LockMode.PESSIMISTIC_WRITE, tableName) +
" where " + sequenceColumn + " = ?" +
dialect.getForUpdateString();
this.update = "update " + tableName +
" set " + allocColumn + " = ? where " + sequenceColumn + " = ? and " + allocColumn + " = ?";
// setup Return Type & Result Holder.
// --
this.returnType = type;
this.returnClass = type.getReturnedClass();
this.resultFactory = IdentifierGeneratorHelper.getIntegralDataTypeHolder( returnClass);
// done.
}
// ----------------------------------------------------------------------------------
/** allocate a Key;
* -
*/
public synchronized Serializable generate (SessionImplementor session, Object object) throws HibernateException
{
if (allocNext >= allocHi) {
if (log.isDebugEnabled())
log.debug( "allocating id block: " + tableName + ", " + sequenceName + ": blockSize=" + blockSize);
long allocated = allocateBlock( session);
//
this.allocNext = allocated;
this.allocHi = allocated + blockSize;
if (log.isDebugEnabled())
log.debug( " allocated block: " + allocNext + "-" + allocHi);
}
long result = allocNext++;
if (log.isDebugEnabled()) {
log.debug( "allocated id: " + result);
}
// answer Result, as correct type;
// -- use one single mutable instance 'Result Holder' instance as our Factory.
//
resultFactory.initialize( result);
Number resultVal = resultFactory.makeValue();
//
return resultVal;
}
protected long allocateBlock (final SessionImplementor session) {
AbstractReturningWork work = new AbstractReturningWork() {
@Override
public Long execute (Connection conn) throws SQLException {
final SqlStatementLogger statementLogger = session
.getFactory()
.getServiceRegistry()
.getService( JdbcServices.class )
.getSqlStatementLogger();
long result;
int rows;
do {
// The loop ensures atomicity of the
// select + update even for no transaction
// or read committed isolation level
statementLogger.logStatement( query, FormatStyle.BASIC.getFormatter());
PreparedStatement qps = conn.prepareStatement( query);
try {
qps.setString( 1, sequenceName);
ResultSet rs = qps.executeQuery();
if (!rs.next()) {
String err = "could not read a hi value - you need to populate the table: " + tableName + ", " + sequenceName;
log.error( err);
throw new IdentifierGenerationException( err);
}
result = rs.getLong( 1);
rs.close();
} catch (SQLException sqle) {
log.error( "could not read a hi value", sqle);
throw sqle;
} finally {
qps.close();
}
statementLogger.logStatement( update, FormatStyle.BASIC.getFormatter());
PreparedStatement ups = conn.prepareStatement( update);
try {
ups.setLong( 1, result + blockSize);
ups.setString( 2, sequenceName);
ups.setLong( 3, result);
rows = ups.executeUpdate();
} catch (SQLException sqle) {
log.error( "could not update hi value in: " + tableName, sqle);
throw sqle;
} finally {
ups.close();
}
} while (rows == 0);
// success;
// -- allocated a Block.
//
statisticsTableAccessCount++;
return new Long( result);
}
};
// perform in an isolated Transaction.
long allocated = session.getTransactionCoordinator().getTransaction().createIsolationDelegate().delegateWork(
work, true);
return allocated;
}
// ----------------------------------------------------------------------------------
public String[] sqlCreateStrings (Dialect dialect) throws HibernateException
{
// create Sequence Table
// SPEC - always output, skips with error if already exist in DB
String tableStr = "create table " + dialect.quote(tableName) + " (" +
dialect.quote(sequenceColumn) + " " + dialect.getTypeName( Types.VARCHAR, DEFAULT_SEQUENCE_COLUMNLENGTH, 0, 0) + ", " +
dialect.quote(allocColumn) + " " + dialect.getTypeName( Types.BIGINT) + ", " +
"primary key ("+dialect.quote(sequenceColumn)+")" +
")";
// create Row for this Sequence.
String valueStr = "insert into " + tableName + "("+dialect.quote(sequenceColumn)+","+dialect.quote(allocColumn)+") values ( '" + sequenceName + "', "+blockSize+" )";
// done.
return new String[]{ tableStr, valueStr};
}
public String[] sqlDropStrings (Dialect dialect) {
// delete Row for this Sequence.
//
String dropSequence = "delete from " + tableName + " where " + sequenceColumn + " = '" + sequenceName + "'";
return new String[]{ dropSequence};
}
public Object generatorKey() {
return "LinearBlockAllocator table="+tableName+", seq="+sequenceName;
}
// ----------------------------------------------------------------------------------
/** Statistics; Table Access Count.
* @return
*/
public long getStats_TableAccessCount() {
return statisticsTableAccessCount;
}
}