001: /*
002: * Copyright (c) 1998 - 2005 Versant Corporation
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * Versant Corporation - initial API and implementation
010: */
011: package com.versant.core.jdbc.sql;
012:
013: import com.versant.core.metadata.MDStatics;
014: import com.versant.core.jdbc.JdbcKeyGenerator;
015: import com.versant.core.jdbc.JdbcKeyGeneratorFactory;
016: import com.versant.core.jdbc.JdbcMetaDataBuilder;
017: import com.versant.core.jdbc.metadata.JdbcColumn;
018: import com.versant.core.jdbc.metadata.JdbcTable;
019: import com.versant.core.jdbc.metadata.JdbcMappingResolver;
020:
021: import java.sql.*;
022: import java.util.HashSet;
023: import java.util.Iterator;
024:
025: import com.versant.core.common.BindingSupportImpl;
026:
027: /**
028: * This key generator uses a last used id table and a grab size to generate
029: * primary keys. Each instance generates keys for a single class.
030: */
031: public class HighLowJdbcKeyGenerator implements JdbcKeyGenerator {
032:
033: /**
034: * Our args bean.
035: */
036: public static class Args {
037:
038: private String tableName = "jdo_keygen";
039:
040: private String keyColumnName = "table_name";
041: private String valueColumnName = "last_used_id";
042: private int keyColumnLength = 64;
043: private int grabSize = 10;
044: private int start;
045: private boolean createTable = true;
046: private String pkConstraint;
047:
048: public Args() {
049: }
050:
051: public String getTableName() {
052: return tableName;
053: }
054:
055: public void setTableName(String tableName) {
056: this .tableName = tableName;
057: }
058:
059: public String getKeyColumnName() {
060: return keyColumnName;
061: }
062:
063: public void setKeyColumnName(String keyColumnName) {
064: this .keyColumnName = keyColumnName;
065: }
066:
067: public String getValueColumnName() {
068: return valueColumnName;
069: }
070:
071: public void setValueColumnName(String valueColumnName) {
072: this .valueColumnName = valueColumnName;
073: }
074:
075: public int getKeyColumnLength() {
076: return keyColumnLength;
077: }
078:
079: public void setKeyColumnLength(int keyColumnLength) {
080: this .keyColumnLength = keyColumnLength;
081: }
082:
083: public int getGrabSize() {
084: return grabSize;
085: }
086:
087: public void setGrabSize(int grabSize) {
088: this .grabSize = grabSize;
089: }
090:
091: public int getStart() {
092: return start;
093: }
094:
095: public void setStart(int start) {
096: this .start = start;
097: }
098:
099: public boolean isCreateTable() {
100: return createTable;
101: }
102:
103: public void setCreateTable(boolean createTable) {
104: this .createTable = createTable;
105: }
106:
107: public String getPkConstraint() {
108: return pkConstraint;
109: }
110:
111: public void setPkConstraint(String pkConstraint) {
112: this .pkConstraint = pkConstraint;
113: }
114: }
115:
116: /**
117: * Our factory.
118: */
119: public static class Factory implements JdbcKeyGeneratorFactory {
120:
121: /**
122: * Create a javabean to hold args for a createJdbcKeyGenerator call or null
123: * if the key generator does not accept any arguments.
124: */
125: public Object createArgsBean() {
126: return new Args();
127: }
128:
129: /**
130: * Create a JdbcKeyGenerator for class using props as parameters. The
131: * instance returned may be new or may be a shared instance.
132: */
133: public JdbcKeyGenerator createJdbcKeyGenerator(
134: String className, JdbcTable classTable, Object args) {
135: HighLowJdbcKeyGenerator kg = new HighLowJdbcKeyGenerator(
136: classTable, (Args) args);
137: return kg;
138: }
139: }
140:
141: protected JdbcTable classTable;
142: protected JdbcColumn classPk;
143: protected int pkJavaTypeCode;
144:
145: protected String tableName;
146: protected String keyColumnName;
147: protected String valueColumnName;
148: protected int keyColumnLength;
149: protected int grabSize;
150: protected int start;
151: protected boolean createTable;
152: protected String pkConstraint;
153:
154: protected String updateSql;
155: protected String selectSql;
156: protected int lastUsed;
157: protected int grabLeft;
158:
159: public HighLowJdbcKeyGenerator(JdbcTable classTable, Args args) {
160: if (classTable.pk.length > 1) {
161: throw new IllegalArgumentException(
162: "Cannot use HIGH/LOW key generator on a table with multiple "
163: + "primary key columns");
164: }
165: this .classTable = classTable;
166: classPk = classTable.pk[0];
167: pkJavaTypeCode = classPk.javaTypeCode;
168: tableName = args.getTableName();
169: keyColumnName = args.getKeyColumnName();
170: valueColumnName = args.getValueColumnName();
171: keyColumnLength = args.getKeyColumnLength();
172: grabSize = args.getGrabSize();
173: start = args.getStart();
174: createTable = args.isCreateTable();
175: pkConstraint = args.getPkConstraint();
176: }
177:
178: /**
179: * Initialize this key generator. This is called when the JDO
180: * implementation initializes before any keys are generated. Key
181: * generators should use this to avoid popular race conditions and
182: * deadlock opportunities (e.g. multiple 'select max(id) from table'
183: * statements executing at the same time). If the same key generator
184: * instance is used on more than one class this will be called once
185: * for each class.
186: *
187: * @param className The name of the class
188: * @param classTable The table for the class
189: * @param con Connection to the DataSource for the class
190: */
191: public void init(String className, JdbcTable classTable,
192: Connection con) throws SQLException {
193:
194: // generate our update and select statements
195: String where = " where " + keyColumnName + " = '"
196: + classTable.name + "'";
197: updateSql = "update " + tableName + " set " + valueColumnName
198: + " = " + valueColumnName + " + ?" + where;
199: selectSql = "select " + valueColumnName + " from " + tableName
200: + where;
201:
202: // make sure there is a row in our keygen table for our class
203: Statement stat = null;
204: PreparedStatement ps = null;
205: try {
206: ps = con.prepareStatement(updateSql);
207: ps.setInt(1, 0);
208: if (ps.executeUpdate() == 0) {
209: stat = con.createStatement();
210: int first = start;
211: if (first == 0) {
212: String sql = "select max(" + classPk.name
213: + ") from " + classTable.name;
214: ResultSet rs = null;
215: try {
216: rs = stat.executeQuery(sql);
217: rs.next();
218: first = rs.getInt(1);
219: } finally {
220: cleanup(rs);
221: }
222: }
223: String sql = "insert into " + tableName + " ("
224: + keyColumnName + ", " + valueColumnName
225: + ") values ('" + classTable.name + "', "
226: + first + ")";
227: stat.execute(sql);
228: }
229: } finally {
230: cleanup(ps);
231: cleanup(stat);
232: }
233: }
234:
235: /**
236: * If the new key can only be detirmined after the new row has been
237: * inserted (e.g. if using a database autoincrement column) then this
238: * should return true.
239: */
240: public boolean isPostInsertGenerator() {
241: return false;
242: }
243:
244: /**
245: * Does this key generator require its own connection? If it does then
246: * one will be obtained to generate the key and committed after the
247: * key has been generated.
248: */
249: public boolean isRequiresOwnConnection() {
250: return grabSize > 1 && grabLeft == 0;
251: }
252:
253: /**
254: * Add any JdbcTable instances that this key generator requires to the
255: * supplied set. This method is called once per key generator during meta
256: * data generation. Any tables returned will be added to the meta data and
257: * will get into SQL scripts and so on. If the same key generator
258: * instance is returned more than once by a factory then this method
259: * will still only be called once per instance.
260: */
261: public void addKeyGenTables(HashSet set, JdbcMetaDataBuilder mdb) {
262: if (!createTable)
263: return;
264:
265: // do not create a table if there is already one with our tableName
266: for (Iterator i = set.iterator(); i.hasNext();) {
267: JdbcTable t = (JdbcTable) i.next();
268: if (t.name.equals(tableName))
269: return;
270: }
271:
272: // create the table and add it
273: JdbcTable t = new JdbcTable();
274: t.sqlDriver = mdb.getSqlDriver();
275: t.name = tableName;
276: t.comment = getClass().getName();
277: t.pkConstraintName = pkConstraint == null ? "pk_" + tableName
278: : pkConstraint;
279: JdbcMappingResolver mr = mdb.getMappingResolver();
280: JdbcColumn keyCol = new JdbcColumn(mr
281: .resolveMapping(String.class), mr);
282: keyCol.name = keyColumnName;
283: keyCol.length = keyColumnLength;
284: keyCol.nulls = false;
285: JdbcColumn valueCol = new JdbcColumn(mr
286: .resolveMapping(Integer.TYPE), mr);
287:
288: valueCol.name = valueColumnName;
289: valueCol.nulls = false;
290: t.cols = new JdbcColumn[] { keyCol, valueCol };
291: t.setPk(new JdbcColumn[] { keyCol });
292: set.add(t);
293: }
294:
295: /**
296: * Generate a new primary key value for a new instance of the supplied
297: * class prior to the row being inserted. The values generated will be used
298: * to populate a new OID and then set on a PreparedStatement for the
299: * insert. This is called if isPostInsertGenerator returns false.
300: * <p/>
301: * The newObjectCount parameter indicates the number of new objects that
302: * will be inserted (including this one) in the same transaction using
303: * this key generator. This may be used to optimize the behavior of the
304: * key generator or be ignored. The highlow key generator uses this value
305: * instead of its grabSize to avoid executing redundant updates and
306: * selects.<p>
307: *
308: * @param className The name of the class
309: * @param classTable The table for the class
310: * @param newObjectCount The number of new objects being created
311: * @param data The array to store the key values in.
312: * @param con Connection to the DataSource for the class.
313: * @throws SQLException on errors
314: */
315: public synchronized void generatePrimaryKeyPre(String className,
316: JdbcTable classTable, int newObjectCount, Object[] data,
317: Connection con) throws SQLException {
318: int pk;
319: if (grabSize == 1) {
320: pk = lookupNewNumber(con, grabSize);
321: } else {
322: if (grabLeft == 0) {
323: int effectiveGrabSize = newObjectCount;
324: if (effectiveGrabSize < grabSize)
325: effectiveGrabSize = grabSize;
326: lastUsed = lookupNewNumber(con, effectiveGrabSize);
327: grabLeft = effectiveGrabSize - 1;
328: } else {
329: --grabLeft;
330: }
331: pk = lastUsed++;
332: }
333: switch (pkJavaTypeCode) {
334: case MDStatics.INTW:
335: case MDStatics.INT:
336: data[0] = new Integer((int) pk);
337: break;
338: case MDStatics.SHORTW:
339: case MDStatics.SHORT:
340: data[0] = new Short((short) pk);
341: break;
342: case MDStatics.BYTEW:
343: case MDStatics.BYTE:
344: data[0] = new Byte((byte) pk);
345: break;
346: case MDStatics.LONGW:
347: case MDStatics.LONG:
348: data[0] = new Long(pk);
349: break;
350: default:
351: throw BindingSupportImpl.getInstance().internal(
352: "Unhandled java type code: " + pkJavaTypeCode);
353: }
354: }
355:
356: /**
357: * Run SQL to get a new number. This does an update and a select for
358: * our classes row in the keygen table.
359: */
360: protected int lookupNewNumber(Connection con, int effectiveGrabSize)
361: throws SQLException {
362: PreparedStatement ps = null;
363: try {
364: ps = con.prepareStatement(updateSql);
365: ps.setInt(1, effectiveGrabSize);
366: if (ps.executeUpdate() == 0) {
367: throw BindingSupportImpl.getInstance().fatalDatastore(
368: "Row not found in keygen table:\n" + updateSql);
369: }
370: Statement stat = null;
371: ResultSet rs = null;
372: try {
373: stat = con.createStatement();
374: rs = stat.executeQuery(selectSql);
375: rs.next();
376: return rs.getInt(1) - (effectiveGrabSize - 1);
377: } finally {
378: cleanup(rs);
379: cleanup(stat);
380: }
381: } finally {
382: cleanup(ps);
383: }
384: }
385:
386: private void cleanup(ResultSet rs) {
387: try {
388: if (rs != null)
389: rs.close();
390: } catch (SQLException e) {
391: // ignore
392: }
393: }
394:
395: private void cleanup(Statement s) {
396: try {
397: if (s != null)
398: s.close();
399: } catch (SQLException e) {
400: // ignore
401: }
402: }
403:
404: /**
405: * Generate a new primary key value for a new instance of the supplied
406: * class after the row has been inserted. The values generated will be used
407: * to populate a new OID and then set on a PreparedStatement for the
408: * insert. This is called if isPostInsertGenerator returns true.
409: *
410: * @param className The name of the class
411: * @param classTable The table for the class
412: * @param data The array to store the key values in.
413: * @param con Connection to the DataSource for the class.
414: * @param stat Statement created from con. Do not close it. This will have
415: * just been used to insert the new row.
416: * @throws SQLException on errors
417: */
418: public void generatePrimaryKeyPost(String className,
419: JdbcTable classTable, Object[] data, Connection con,
420: Statement stat) throws SQLException {
421: throw BindingSupportImpl.getInstance().internal(
422: "not a postInsertGenerator");
423: }
424:
425: /**
426: * Get extra SQL to be appended to the insert statement. This is only
427: * called for post insert key generators. Return null if no extra SQL
428: * is required. Key generators can use this as an alternative to running
429: * a separate query to get the primary key for the just inserted row.
430: */
431: public String getPostInsertSQLSuffix(JdbcTable classTable) {
432: throw BindingSupportImpl.getInstance().internal(
433: "not a postInsertGenerator");
434: }
435:
436: }
|