1 /*
2 * Hibernate, Relational Persistence for Idiomatic Java
3 *
4 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5 * indicated by the @author tags or express copyright attribution
6 * statements applied by the authors. All third-party contributions are
7 * distributed under license by Red Hat Middleware LLC.
8 *
9 * This copyrighted material is made available to anyone wishing to use, modify,
10 * copy, or redistribute it subject to the terms and conditions of the GNU
11 * Lesser General Public License, as published by the Free Software Foundation.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16 * for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this distribution; if not, write to:
20 * Free Software Foundation, Inc.
21 * 51 Franklin Street, Fifth Floor
22 * Boston, MA 02110-1301 USA
23 *
24 */
25 package org.hibernate.id;
26
27 import java.io.Serializable;
28 import java.sql.Connection;
29 import java.sql.PreparedStatement;
30 import java.sql.ResultSet;
31 import java.sql.SQLException;
32 import java.sql.Types;
33 import java.util.Properties;
34
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.hibernate.HibernateException;
38 import org.hibernate.LockMode;
39 import org.hibernate.MappingException;
40 import org.hibernate.jdbc.util.FormatStyle;
41 import org.hibernate.dialect.Dialect;
42 import org.hibernate.engine.SessionImplementor;
43 import org.hibernate.engine.TransactionHelper;
44 import org.hibernate.mapping.Table;
45 import org.hibernate.type.Type;
46 import org.hibernate.util.PropertiesHelper;
47
48 /**
49 *
50 * A hilo <tt>IdentifierGenerator</tt> that returns a <tt>Long</tt>, constructed using
51 * a hi/lo algorithm. The hi value MUST be fetched in a seperate transaction
52 * to the <tt>Session</tt> transaction so the generator must be able to obtain
53 * a new connection and commit it. Hence this implementation may not
54 * be used when the user is supplying connections. In this
55 * case a <tt>SequenceHiLoGenerator</tt> would be a better choice (where
56 * supported).<br>
57 * <br>
58 *
59 * A hilo <tt>IdentifierGenerator</tt> that uses a database
60 * table to store the last generated values. A table can contains
61 * several hi values. They are distinct from each other through a key
62 * <p/>
63 * <p>This implementation is not compliant with a user connection</p>
64 * <p/>
65 *
66 * <p>Allowed parameters (all of them are optional):</p>
67 * <ul>
68 * <li>table: table name (default <tt>hibernate_sequences</tt>)</li>
69 * <li>primary_key_column: key column name (default <tt>sequence_name</tt>)</li>
70 * <li>value_column: hi value column name(default <tt>sequence_next_hi_value</tt>)</li>
71 * <li>primary_key_value: key value for the current entity (default to the entity's primary table name)</li>
72 * <li>primary_key_length: length of the key column in DB represented as a varchar (default to 255)</li>
73 * <li>max_lo: max low value before increasing hi (default to Short.MAX_VALUE)</li>
74 * </ul>
75 *
76 * @author Emmanuel Bernard
77 * @author <a href="mailto:kr@hbt.de">Klaus Richarz</a>.
78 */
79 public class MultipleHiLoPerTableGenerator
80 extends TransactionHelper
81 implements PersistentIdentifierGenerator, Configurable {
82
83 private static final Logger log = LoggerFactory.getLogger(MultipleHiLoPerTableGenerator.class);
84
85 public static final String ID_TABLE = "table";
86 public static final String PK_COLUMN_NAME = "primary_key_column";
87 public static final String PK_VALUE_NAME = "primary_key_value";
88 public static final String VALUE_COLUMN_NAME = "value_column";
89 public static final String PK_LENGTH_NAME = "primary_key_length";
90
91 private static final int DEFAULT_PK_LENGTH = 255;
92 public static final String DEFAULT_TABLE = "hibernate_sequences";
93 private static final String DEFAULT_PK_COLUMN = "sequence_name";
94 private static final String DEFAULT_VALUE_COLUMN = "sequence_next_hi_value";
95
96 private String tableName;
97 private String pkColumnName;
98 private String valueColumnName;
99 private String query;
100 private String insert;
101 private String update;
102
103 //hilo params
104 public static final String MAX_LO = "max_lo";
105
106 private long hi;
107 private int lo;
108 private int maxLo;
109 private Class returnClass;
110 private int keySize;
111
112
113 public String[] sqlCreateStrings(Dialect dialect) throws HibernateException {
114 return new String[] {
115 new StringBuffer( dialect.getCreateTableString() )
116 .append( ' ' )
117 .append( tableName )
118 .append( " ( " )
119 .append( pkColumnName )
120 .append( ' ' )
121 .append( dialect.getTypeName( Types.VARCHAR, keySize, 0, 0 ) )
122 .append( ", " )
123 .append( valueColumnName )
124 .append( ' ' )
125 .append( dialect.getTypeName( Types.INTEGER ) )
126 .append( " ) " )
127 .toString()
128 };
129 }
130
131 public String[] sqlDropStrings(Dialect dialect) throws HibernateException {
132 StringBuffer sqlDropString = new StringBuffer( "drop table " );
133 if ( dialect.supportsIfExistsBeforeTableName() ) {
134 sqlDropString.append( "if exists " );
135 }
136 sqlDropString.append( tableName ).append( dialect.getCascadeConstraintsString() );
137 if ( dialect.supportsIfExistsAfterTableName() ) {
138 sqlDropString.append( " if exists" );
139 }
140 return new String[] { sqlDropString.toString() };
141 }
142
143 public Object generatorKey() {
144 return tableName;
145 }
146
147 public Serializable doWorkInCurrentTransaction(Connection conn, String sql) throws SQLException {
148 int result;
149 int rows;
150 do {
151 // The loop ensures atomicity of the
152 // select + update even for no transaction
153 // or read committed isolation level
154
155 //sql = query;
156 SQL_STATEMENT_LOGGER.logStatement( sql, FormatStyle.BASIC );
157 PreparedStatement qps = conn.prepareStatement(query);
158 PreparedStatement ips = null;
159 try {
160 //qps.setString(1, key);
161 ResultSet rs = qps.executeQuery();
162 boolean isInitialized = rs.next();
163 if ( !isInitialized ) {
164 result = 0;
165 ips = conn.prepareStatement(insert);
166 //ips.setString(1, key);
167 ips.setInt(1, result);
168 ips.execute();
169 }
170 else {
171 result = rs.getInt(1);
172 }
173 rs.close();
174 }
175 catch (SQLException sqle) {
176 log.error("could not read or init a hi value", sqle);
177 throw sqle;
178 }
179 finally {
180 if (ips != null) {
181 ips.close();
182 }
183 qps.close();
184 }
185
186 //sql = update;
187 PreparedStatement ups = conn.prepareStatement(update);
188 try {
189 ups.setInt( 1, result + 1 );
190 ups.setInt( 2, result );
191 //ups.setString( 3, key );
192 rows = ups.executeUpdate();
193 }
194 catch (SQLException sqle) {
195 log.error("could not update hi value in: " + tableName, sqle);
196 throw sqle;
197 }
198 finally {
199 ups.close();
200 }
201 }
202 while (rows==0);
203 return new Integer(result);
204 }
205
206 public synchronized Serializable generate(SessionImplementor session, Object obj)
207 throws HibernateException {
208 if (maxLo < 1) {
209 //keep the behavior consistent even for boundary usages
210 int val = ( (Integer) doWorkInNewTransaction(session) ).intValue();
211 if (val == 0) val = ( (Integer) doWorkInNewTransaction(session) ).intValue();
212 return IdentifierGeneratorFactory.createNumber( val, returnClass );
213 }
214 if (lo>maxLo) {
215 int hival = ( (Integer) doWorkInNewTransaction(session) ).intValue();
216 lo = (hival == 0) ? 1 : 0;
217 hi = hival * (maxLo+1);
218 log.debug("new hi value: " + hival);
219 }
220 return IdentifierGeneratorFactory.createNumber( hi + lo++, returnClass );
221 }
222
223 public void configure(Type type, Properties params, Dialect dialect) throws MappingException {
224 tableName = PropertiesHelper.getString(ID_TABLE, params, DEFAULT_TABLE);
225 pkColumnName = PropertiesHelper.getString(PK_COLUMN_NAME, params, DEFAULT_PK_COLUMN);
226 valueColumnName = PropertiesHelper.getString(VALUE_COLUMN_NAME, params, DEFAULT_VALUE_COLUMN);
227 String schemaName = params.getProperty(SCHEMA);
228 String catalogName = params.getProperty(CATALOG);
229 keySize = PropertiesHelper.getInt(PK_LENGTH_NAME, params, DEFAULT_PK_LENGTH);
230 String keyValue = PropertiesHelper.getString(PK_VALUE_NAME, params, params.getProperty(TABLE) );
231
232 if ( tableName.indexOf( '.' )<0 ) {
233 tableName = Table.qualify( catalogName, schemaName, tableName );
234 }
235
236 query = "select " +
237 valueColumnName +
238 " from " +
239 dialect.appendLockHint(LockMode.UPGRADE, tableName) +
240 " where " + pkColumnName + " = '" + keyValue + "'" +
241 dialect.getForUpdateString();
242
243 update = "update " +
244 tableName +
245 " set " +
246 valueColumnName +
247 " = ? where " +
248 valueColumnName +
249 " = ? and " +
250 pkColumnName +
251 " = '" +
252 keyValue
253 + "'";
254
255 insert = "insert into " + tableName +
256 "(" + pkColumnName + ", " + valueColumnName + ") " +
257 "values('"+ keyValue +"', ?)";
258
259
260 //hilo config
261 maxLo = PropertiesHelper.getInt(MAX_LO, params, Short.MAX_VALUE);
262 lo = maxLo + 1; // so we "clock over" on the first invocation
263 returnClass = type.getReturnedClass();
264 }
265 }