From a69c139e3d9afa754514d807c44cdafc331f4a9c Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 22 Sep 2006 18:06:43 +0000 Subject: [PATCH] External table primary key generation from Zell, with some rearranging. --- .../jdbc/depot/DepotMarshaller.java | 135 ++++++++++++------ .../jdbc/depot/DepotRepository.java | 6 +- .../jdbc/depot/FieldMarshaller.java | 23 ++- .../jdbc/depot/IdentityKeyGenerator.java | 63 ++++++++ .../samskivert/jdbc/depot/KeyGenerator.java | 38 +++++ .../jdbc/depot/TableKeyGenerator.java | 133 +++++++++++++++++ 6 files changed, 343 insertions(+), 55 deletions(-) create mode 100644 src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java create mode 100644 src/java/com/samskivert/jdbc/depot/KeyGenerator.java create mode 100644 src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 5531a85..9748003 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -1,7 +1,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2001-2006 Michael Bayne -// +// // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or @@ -18,7 +18,6 @@ package com.samskivert.jdbc.depot; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -32,7 +31,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.logging.Level; +import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.TableGenerator; import javax.persistence.Transient; import com.samskivert.jdbc.JDBCUtil; @@ -40,6 +41,7 @@ import com.samskivert.util.StringUtil; import static com.samskivert.jdbc.depot.Log.log; + /** * Handles the marshalling and unmarshalling of persistent instances to JDBC * primitives ({@link PreparedStatement} and {@link ResultSet}). @@ -63,6 +65,13 @@ public class DepotMarshaller _tableName = _pclass.getName(); _tableName = _tableName.substring(_tableName.lastIndexOf(".")+1); + // if the entity defines a new TableGenerator, map that in our static + // table as those are shared across all entities + TableGenerator generator = pclass.getAnnotation(TableGenerator.class); + if (generator != null) { + _generators.put(generator.name(), generator); + } + // introspect on the class and create marshallers for persistent fields ArrayList fields = new ArrayList(); for (Field field : _pclass.getFields()) { @@ -94,6 +103,45 @@ public class DepotMarshaller if (field.getAnnotation(Id.class) != null) { // TODO: handle multiple field primary keys _primaryKey = fm; + + // check if this field defines a new TableGenerator + generator = field.getAnnotation(TableGenerator.class); + if (generator != null) { + _generators.put(generator.name(), generator); + } + } + } + + // if the entity defines a primary key, figure out how we will be + // generating values for it + if (_primaryKey != null) { + // the primary key must be numeric if we are to auto-assign it + Class ftype = _primaryKey.getField().getType(); + boolean isNumeric = (ftype.equals(Byte.TYPE) || + ftype.equals(Byte.class) || ftype.equals(Short.TYPE) || + ftype.equals(Short.class) || ftype.equals(Integer.TYPE) || + ftype.equals(Integer.class) || ftype.equals(Long.TYPE) || + ftype.equals(Long.class)); + + // and it will have to have some sort of annotation + GeneratedValue gv = _primaryKey.getGeneratedValue(); + if (isNumeric && gv != null) { + switch(gv.strategy()) { + case AUTO: + case IDENTITY: + _keyGenerator = new IdentityKeyGenerator(); + break; + + case TABLE: + String name = gv.generator(); + generator = _generators.get(name); + if (generator == null) { + throw new IllegalArgumentException( + "Unknown generator [generator=" + name + "]"); + } + _keyGenerator = new TableKeyGenerator(generator); + break; + } } } @@ -104,7 +152,7 @@ public class DepotMarshaller // create the SQL used to create and migrate our table _columnDefinitions = new String[_allFields.length]; for (int ii = 0; ii < _allFields.length; ii++) { - _columnDefinitions[ii] = + _columnDefinitions[ii] = _fields.get(_allFields[ii]).getColumnDefinition(); } _postamble = ""; // TODO: add annotations for the postamble @@ -177,7 +225,10 @@ public class DepotMarshaller JDBCUtil.createTableIfMissing( conn, getTableName(), _columnDefinitions, _postamble); // TODO: insert current version into version table - return; + } + // if there is a key generator, initialize that too + if (_keyGenerator != null) { + _keyGenerator.init(conn); } // if schema versioning is disabled, stop now @@ -280,44 +331,29 @@ public class DepotMarshaller /** * Fills in the primary key just assigned to the supplied persistence - * object by the execution of the results of {@link #createInsert}. The - * structure of primary key assignment will probably have to change when we - * support other databases. + * object by the execution of the results of {@link #createInsert}. */ - public void assignPrimaryKey (Connection conn, Object po) + public void assignPrimaryKey ( + Connection conn, Object po, boolean postFactum) throws SQLException { - // no primary key, no problem! - if (_primaryKey == null) { + // if we have no primary key or no generator, then we're done + if (_primaryKey == null || _keyGenerator == null) { return; } - // if the primary key is non-numeric, we can't auto-assign it - Class ftype = _primaryKey.getField().getType(); - if (!ftype.equals(Byte.TYPE) && !ftype.equals(Byte.class) && - !ftype.equals(Short.TYPE) && !ftype.equals(Short.class) && - !ftype.equals(Integer.TYPE) && !ftype.equals(Integer.class) && - !ftype.equals(Long.TYPE) && !ftype.equals(Long.class)) { + // run this generator either before or after the actual insertion + if (_keyGenerator.isPostFactum() != postFactum) { return; } - // load up the last inserted ID mysql style; eventually this will be - // fancier and pluggable - Statement stmt = null; try { - stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()"); - if (rs.next()) { - _primaryKey.getField().set(po, rs.getInt(1)); - } - - } catch (IllegalAccessException iae) { + int nextValue = _keyGenerator.nextGeneratedValue(conn); + _primaryKey.getField().set(po, nextValue); + } catch (Exception e) { String errmsg = "Failed to assign primary key " + "[type=" + _pclass + "]"; - throw (SQLException)new SQLException(errmsg).initCause(iae); - - } finally { - JDBCUtil.close(stmt); + throw (SQLException) new SQLException(errmsg).initCause(e); } } @@ -406,6 +442,20 @@ public class DepotMarshaller return pstmt; } + /** + * Creates a statement that will delete all rows matching the supplied key. + */ + public PreparedStatement createDelete ( + Connection conn, DepotRepository.Key key) + throws SQLException + { + String query = "delete from " + getTableName() + + " where " + key.toWhereClause(); + PreparedStatement pstmt = conn.prepareStatement(query); + key.bindArguments(pstmt, 1); + return pstmt; + } + /** * Creates a statement that will update the specified set of fields, using * the supplied literal SQL values, for all persistent objects that match @@ -432,23 +482,12 @@ public class DepotMarshaller return pstmt; } - /** - * Creates a statement that will delete all rows matching the supplied key. - */ - public PreparedStatement createDelete ( - Connection conn, DepotRepository.Key key) - throws SQLException - { - String query = "delete from " + getTableName() + - " where " + key.toWhereClause(); - PreparedStatement pstmt = conn.prepareStatement(query); - key.bindArguments(pstmt, 1); - return pstmt; - } - /** The persistent object class that we manage. */ protected Class _pclass; + /** The name of our persistent object table. */ + protected String _tableName; + /** A field marshaller for each persistent field in our object. */ protected HashMap _fields = new HashMap(); @@ -457,8 +496,8 @@ public class DepotMarshaller * it did not define a primary key. */ protected FieldMarshaller _primaryKey; - /** The name of our persistent object table. */ - protected String _tableName; + /** The generator to use for auto-generating primary key values, or null. */ + protected KeyGenerator _keyGenerator; /** The persisent fields of our object, in definition order, separated by * commas for easy use in a select statement. */ @@ -477,6 +516,10 @@ public class DepotMarshaller /** Used when creating and migrating our table schema. */ protected String _postamble; + /** A map of name to {@link TableGenerator}, scoped over all classes. */ + protected static HashMap _generators = + new HashMap(); + /** The name of the table we use to track schema versions. */ protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion"; } diff --git a/src/java/com/samskivert/jdbc/depot/DepotRepository.java b/src/java/com/samskivert/jdbc/depot/DepotRepository.java index 66a8c35..7a1f81b 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java +++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java @@ -127,10 +127,11 @@ public class DepotRepository final DepotMarshaller marsh = getMarshaller(record.getClass()); return invoke(new Modifier(marsh.getPrimaryKey(record)) { public int invoke (Connection conn) throws SQLException { + marsh.assignPrimaryKey(conn, record, false); PreparedStatement stmt = marsh.createInsert(conn, record); try { int mods = stmt.executeUpdate(); - marsh.assignPrimaryKey(conn, record); + marsh.assignPrimaryKey(conn, record, true); return mods; } finally { stmt.close(); @@ -342,9 +343,10 @@ public class DepotRepository // if the update modified zero rows or the primary key was // obviously unset, do an insertion + marsh.assignPrimaryKey(conn, record, false); stmt = marsh.createInsert(conn, record); int mods = stmt.executeUpdate(); - marsh.assignPrimaryKey(conn, record); + marsh.assignPrimaryKey(conn, record, true); return mods; } finally { diff --git a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java index fddd599..d235634 100644 --- a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java @@ -108,6 +108,14 @@ public abstract class FieldMarshaller return _field; } + /** + * Returns the GeneratedValue annotation on this field, if any. + */ + public GeneratedValue getGeneratedValue () + { + return _generatedValue; + } + /** * Returns the name of the table column used to store this field. */ @@ -195,19 +203,19 @@ public abstract class FieldMarshaller builder.append(" PRIMARY KEY"); // figure out how we're going to generate our primary key values - GeneratedValue gv = field.getAnnotation(GeneratedValue.class); - if (gv != null) { - switch (gv.strategy()) { + _generatedValue = field.getAnnotation(GeneratedValue.class); + if (_generatedValue != null) { + switch (_generatedValue.strategy()) { case AUTO: case IDENTITY: builder.append(" AUTO_INCREMENT"); break; case SEQUENCE: // TODO throw new IllegalArgumentException( - "TABLE key generation strategy not yet supported."); - case TABLE: // TODO - throw new IllegalArgumentException( - "TABLE key generation strategy not yet supported."); + "SEQUENCE key generation strategy not yet supported."); + case TABLE: + // nothing to do here, it'll be handled later + break; } } } @@ -371,4 +379,5 @@ public abstract class FieldMarshaller protected Field _field; protected String _columnName, _columnDefinition; + protected GeneratedValue _generatedValue; } diff --git a/src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java b/src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java new file mode 100644 index 0000000..fc63f26 --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java @@ -0,0 +1,63 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2006 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import com.samskivert.jdbc.JDBCUtil; + +/** + * Generates primary keys using an identity column. + */ +public class IdentityKeyGenerator implements KeyGenerator +{ + // from interface KeyGenerator + public boolean isPostFactum () + { + return true; + } + + // from interface KeyGenerator + public void init (Connection conn) + throws SQLException + { + // nothing to do here + } + + // from interface KeyGenerator + public int nextGeneratedValue (Connection conn) + throws SQLException + { + // load up the last inserted ID mysql style + Statement stmt = conn.createStatement(); + try { + ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()"); + if (rs.next()) { + return rs.getInt(1); + } + throw new SQLException("Failed to generate next ID"); + + } finally { + JDBCUtil.close(stmt); + } + } +} diff --git a/src/java/com/samskivert/jdbc/depot/KeyGenerator.java b/src/java/com/samskivert/jdbc/depot/KeyGenerator.java new file mode 100644 index 0000000..75479b4 --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/KeyGenerator.java @@ -0,0 +1,38 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2006 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Defines the interface to our primary key generators. + */ +public interface KeyGenerator +{ + /** If true, this key generator will be run after the insert statement, if + * false, it will be run before. */ + public boolean isPostFactum (); + + /** Prepares the generator for operation. */ + public void init (Connection conn) throws SQLException; + + /** Fetch/generate the next primary key value. */ + public int nextGeneratedValue (Connection conn) throws SQLException; +} diff --git a/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java b/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java new file mode 100644 index 0000000..c8c05be --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java @@ -0,0 +1,133 @@ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2006 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.jdbc.depot; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.persistence.TableGenerator; + +import com.samskivert.jdbc.JDBCUtil; + +/** + * Generates primary keys using an external table + */ +public class TableKeyGenerator implements KeyGenerator +{ + public TableKeyGenerator (TableGenerator annotation) + { + _table = defStr(annotation.table(), "IdSequences"); + _pkColumnName = defStr(annotation.pkColumnName(), "sequence"); + _pkColumnValue = defStr(annotation.pkColumnValue(), "default"); + _valueColumnName = defStr(annotation.valueColumnName(), "value"); + _allocationSize = annotation.allocationSize(); + _initialValue = _allocationSize > 0 ? _allocationSize : 1; + } + + // from interface KeyGenerator + public boolean isPostFactum () + { + return false; + } + + // from interface KeyGenerator + public void init (Connection conn) + throws SQLException + { + // make sure our table exists + JDBCUtil.createTableIfMissing(conn, _table, new String[] { + _pkColumnName + " VARCHAR(255) PRIMARY KEY", + _valueColumnName + " INTEGER NOT NULL" + }, ""); + + // and also that there's a row in it for us + PreparedStatement stmt = null; + try { + stmt = conn.prepareStatement( + "SELECT * FROM " + _table + " WHERE " + _pkColumnName + " = ?"); + stmt.setString(1, _pkColumnValue); + if (stmt.executeQuery().next()) { + return; + } + + JDBCUtil.close(stmt); + stmt = null; + stmt = conn.prepareStatement( + "INSERT INTO " + _table + " SET " + _pkColumnName + " = ?, " + + _valueColumnName + " = ? "); + stmt.setString(1, _pkColumnValue); + stmt.setInt(2, _initialValue); + stmt.executeUpdate(); + + } finally { + JDBCUtil.close(stmt); + } + } + + // from interface KeyGenerator + public int nextGeneratedValue (Connection conn) + throws SQLException + { + PreparedStatement stmt = null; + try { + String query = "SELECT " + _valueColumnName + " FROM " + _table + + " WHERE " + _pkColumnName + "= ? FOR UPDATE"; + stmt = conn.prepareStatement(query); + stmt.setString(1, _pkColumnValue); + + ResultSet rs = stmt.executeQuery(); + if (!rs.next()) { + throw new SQLException( + "Failed to find next primary key value [table=" + _table + + ", column=" + _valueColumnName + + ", where=" + _pkColumnName + "=" + _pkColumnValue + "]"); + } + int val = rs.getInt(1); + JDBCUtil.close(stmt); + + stmt = conn.prepareStatement( + "UPDATE " + _table + " SET " + _valueColumnName + " = ? " + + "WHERE " + _pkColumnName + " = ?"); + stmt.setInt(1, val + _allocationSize); + stmt.setString(2, _pkColumnValue); + stmt.executeUpdate(); + return val; + + } finally { + JDBCUtil.close(stmt); + } + } + + /** Convenience function to return a value or a default fallback. */ + protected static String defStr (String value, String def) { + if (value == null || value.trim().length() == 0) { + return def; + } + return value; + } + + protected String _table; + protected String _pkColumnName; + protected String _pkColumnValue; + protected String _valueColumnName; + protected int _initialValue; + protected int _allocationSize; +}