External table primary key generation from Zell, with some rearranging.

This commit is contained in:
Michael Bayne
2006-09-22 18:06:43 +00:00
parent 17e63530c4
commit a69c139e3d
6 changed files with 343 additions and 55 deletions
@@ -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<T>
_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<String> fields = new ArrayList<String>();
for (Field field : _pclass.getFields()) {
@@ -94,6 +103,45 @@ public class DepotMarshaller<T>
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;
}
}
}
@@ -177,7 +225,10 @@ public class DepotMarshaller<T>
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<T>
/**
* 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<T>
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<T>
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<T> _pclass;
/** The name of our persistent object table. */
protected String _tableName;
/** A field marshaller for each persistent field in our object. */
protected HashMap<String, FieldMarshaller> _fields =
new HashMap<String, FieldMarshaller>();
@@ -457,8 +496,8 @@ public class DepotMarshaller<T>
* 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<T>
/** 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<String, TableGenerator> _generators =
new HashMap<String, TableGenerator>();
/** The name of the table we use to track schema versions. */
protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion";
}
@@ -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 {
@@ -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;
}
@@ -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);
}
}
}
@@ -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;
}
@@ -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;
}