Java tutorial
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. * <ul> * <li>This strategy is highly efficient, database-portable, and simplifies/ supersedes HiLo-type strategies. * <li>Keys are allocated in blocks from an "allocator" table, with a current block held in memory; used by the application as required. * <li>Default block-size of 20 outperforms SEQUENCE allocation by a factor of 10 or more, using only standard SQL & fully portable * between databases. * <li>Block allocation is multiuser & cluster-safe; database contention is handled by retry. * <li>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. * * </ul> * * 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. * <p> * * 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. * <p> * * 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. * <p> * * 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. * <p> * * LinearBlockAllocator is configured with two groups of parameters -- those defining the allocator table, and those selecting/controlling allocation for the specific sequence. * <p> * * Allocator table definition: * <ul> * <li>table -- allocator table to use, default "KEY_ALLOC" * <li>sequenceColumn -- sequence-name column, default "SEQ" * <li>allocColumn -- next-block column, default "NEXT_VAL " * </ul> * Sequence selection: * <ul> * <li>sequenceName -- sequence-name to use; defaults to table-name of the Hibernate entity * <li>blockSize -- block-size (# of keys) to cache in memory; default 20 * </ul> * * 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. * <p> * * 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. * <p> * * 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. * <p> * * * @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<Long> work = new AbstractReturningWork<Long>() { @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; } }