Generalize the automatic key value generation code so that it works on any column, and on any number of columns within a record.

This commit is contained in:
Par Winzell
2007-08-21 15:02:12 +00:00
parent 60d63b13f1
commit 8e446fe93f
9 changed files with 118 additions and 129 deletions
@@ -22,6 +22,7 @@ package com.samskivert.jdbc.depot;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.util.Map; import java.util.Map;
import java.util.Set;
import com.samskivert.jdbc.depot.Key.WhereCondition; import com.samskivert.jdbc.depot.Key.WhereCondition;
import com.samskivert.jdbc.depot.clause.DeleteClause; import com.samskivert.jdbc.depot.clause.DeleteClause;
@@ -240,9 +241,9 @@ public class BindVisitor implements ExpressionVisitor
DepotMarshaller marsh = _types.getMarshaller(insertClause.getPersistentClass()); DepotMarshaller marsh = _types.getMarshaller(insertClause.getPersistentClass());
Object pojo = insertClause.getPojo(); Object pojo = insertClause.getPojo();
String ixField = insertClause.getIndexField(); Set<String> generatedFields = insertClause.getIdentityFields();
for (String field : marsh.getColumnFieldNames()) { for (String field : marsh.getColumnFieldNames()) {
if (ixField == null || !ixField.equals(field)) { if (!generatedFields.contains(field)) {
marsh.getFieldMarshaller(field).readFromObject(pojo, _stmt, _argIdx ++); marsh.getFieldMarshaller(field).readFromObject(pojo, _stmt, _argIdx ++);
} }
} }
@@ -24,6 +24,7 @@ import java.sql.SQLException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import com.samskivert.jdbc.depot.Key.WhereCondition; import com.samskivert.jdbc.depot.Key.WhereCondition;
import com.samskivert.jdbc.depot.annotation.Computed; import com.samskivert.jdbc.depot.annotation.Computed;
@@ -450,12 +451,12 @@ public abstract class BuildVisitor implements ExpressionVisitor
} }
_builder.append(") values("); _builder.append(") values(");
String ixField = insertClause.getIndexField(); Set<String> generatedFields = insertClause.getIdentityFields();
for (int ii = 0; ii < fields.length; ii++) { for (int ii = 0; ii < fields.length; ii++) {
if (ii > 0) { if (ii > 0) {
_builder.append(", "); _builder.append(", ");
} }
if (ixField != null && ixField.equals(fields[ii])) { if (generatedFields.contains(fields[ii])) {
_builder.append("DEFAULT"); _builder.append("DEFAULT");
} else { } else {
_builder.append("?"); _builder.append("?");
@@ -103,6 +103,8 @@ public class DepotMarshaller<T extends PersistentRecord>
} }
} }
boolean seenIdentityGenerator = false;
// introspect on the class and create marshallers for persistent fields // introspect on the class and create marshallers for persistent fields
ArrayList<String> fields = new ArrayList<String>(); ArrayList<String> fields = new ArrayList<String>();
for (Field field : _pclass.getFields()) { for (Field field : _pclass.getFields()) {
@@ -136,38 +138,19 @@ public class DepotMarshaller<T extends PersistentRecord>
_pkColumns = new ArrayList<FieldMarshaller>(); _pkColumns = new ArrayList<FieldMarshaller>();
} }
_pkColumns.add(fm); _pkColumns.add(fm);
// check if this field defines a new TableGenerator
generator = field.getAnnotation(TableGenerator.class);
if (generator != null) {
context.tableGenerators.put(generator.name(), generator);
}
}
}
// if the entity defines a single-columnar primary key, figure out if we will be generating
// values for it
if (_pkColumns != null) {
GeneratedValue gv = null;
FieldMarshaller keyField = null;
// loop over fields to see if there's a @GeneratedValue at all
for (FieldMarshaller field : _pkColumns) {
gv = field.getGeneratedValue();
if (gv != null) {
keyField = field;
break;
}
} }
if (keyField != null) { // check if this field defines a new TableGenerator
// and if there is, make sure we've a single-column id generator = field.getAnnotation(TableGenerator.class);
if (_pkColumns.size() > 1) { if (generator != null) {
throw new IllegalArgumentException( context.tableGenerators.put(generator.name(), generator);
"Cannot use @GeneratedValue on multiple-column @Id's"); }
}
// the primary key must be numeric if we are to auto-assign it // check if this field is auto-generated
Class<?> ftype = keyField.getField().getType(); GeneratedValue gv = fm.getGeneratedValue();
if (gv != null) {
// we can only do this on numeric fields
Class<?> ftype = field.getType();
boolean isNumeric = ( boolean isNumeric = (
ftype.equals(Byte.TYPE) || ftype.equals(Byte.class) || ftype.equals(Byte.TYPE) || ftype.equals(Byte.class) ||
ftype.equals(Short.TYPE) || ftype.equals(Short.class) || ftype.equals(Short.TYPE) || ftype.equals(Short.class) ||
@@ -175,14 +158,17 @@ public class DepotMarshaller<T extends PersistentRecord>
ftype.equals(Long.TYPE) || ftype.equals(Long.class)); ftype.equals(Long.TYPE) || ftype.equals(Long.class));
if (!isNumeric) { if (!isNumeric) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Cannot use @GeneratedValue on non-numeric column"); "Cannot use @GeneratedValue on non-numeric column: " + field.getName());
} }
switch(gv.strategy()) { switch(gv.strategy()) {
case AUTO: case AUTO:
case IDENTITY: case IDENTITY:
_keyGenerator = new IdentityKeyGenerator( if (seenIdentityGenerator) {
gv, getTableName(), keyField.getColumnName()); throw new IllegalArgumentException(
"Persistent records can have at most one AUTO/IDENTITY generator.");
}
_valueGenerators.put(field.getName(), new IdentityValueGenerator(gv, this, fm));
seenIdentityGenerator = true;
break; break;
case TABLE: case TABLE:
@@ -192,11 +178,12 @@ public class DepotMarshaller<T extends PersistentRecord>
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Unknown generator [generator=" + name + "]"); "Unknown generator [generator=" + name + "]");
} }
_keyGenerator = new TableKeyGenerator( _valueGenerators.put(
generator, gv, getTableName(), keyField.getColumnName()); field.getName(), new TableValueGenerator(generator, gv, this, fm));
break; break;
} }
} }
} }
// generate our full list of fields/columns for use in queries // generate our full list of fields/columns for use in queries
@@ -270,12 +257,12 @@ public class DepotMarshaller<T extends PersistentRecord>
} }
/** /**
* Returns the {@link KeyGenerator} used to generate primary keys for this persistent object, * Returns the {@link ValueGenerator} used to generate primary keys for this persistent object,
* or null if it does not use a key generator. * or null if it does not use a key generator.
*/ */
public KeyGenerator getKeyGenerator () public Iterable<ValueGenerator> getValueGenerators ()
{ {
return _keyGenerator; return _valueGenerators.values();
} }
/** /**
@@ -435,37 +422,40 @@ public class DepotMarshaller<T extends PersistentRecord>
} }
/** /**
* Fills in the primary key just assigned to the supplied persistence object by the execution * Go through the registered {@link ValueGenerator}s for our persistent object and run the
* of the results of {@link #createInsert}. * ones that match the current postFactum phase, filling in the fields on the supplied object
* while we go. This method used to generate a key; that is now a separate step.
* *
* @return the newly assigned primary key or null if the object does not use primary keys or * The return value is only non-empty for the !postFactum phase, in which case it is a set
* this is not the right time to assign the key. * of field names that are associated with {@link IdentityValueGenerator}, because these need
* special handling in the INSERT (specifically, 'DEFAULT' must be supplied as a value in
* the eventual SQL).
*/ */
public Key assignPrimaryKey ( public Set<String> generateFieldValues (
Connection conn, DatabaseLiaison liaison, Object po, boolean postFactum) Connection conn, DatabaseLiaison liaison, Object po, boolean postFactum)
throws SQLException
{ {
// if we have no primary key or no generator, then we're done Set<String> idFields = new HashSet<String>();
if (!hasPrimaryKey() || _keyGenerator == null) {
return null;
}
// run this generator either before or after the actual insertion for (ValueGenerator vg : _valueGenerators.values()) {
if (_keyGenerator.isPostFactum() != postFactum) { if (!postFactum && vg instanceof IdentityValueGenerator) {
return null; idFields.add(vg.getFieldMarshaller().getField().getName());
} }
if (vg.isPostFactum() != postFactum) {
continue;
}
try { try {
int nextValue = _keyGenerator.nextGeneratedValue(conn, liaison); int nextValue = vg.nextGeneratedValue(conn, liaison);
_pkColumns.get(0).getField().set(po, nextValue); vg.getFieldMarshaller().getField().set(po, nextValue);
return makePrimaryKey(nextValue);
} catch (Exception e) { } catch (Exception e) {
String errmsg = "Failed to assign primary key [type=" + _pclass + "]"; throw new IllegalStateException(
throw (SQLException) new SQLException(errmsg).initCause(e); "Failed to assign primary key [type=" + _pclass + "]", e);
}
} }
return idFields;
} }
/** /**
* This is called by the persistence context to register a migration for the entity managed by * This is called by the persistence context to register a migration for the entity managed by
* this marshaller. * this marshaller.
@@ -575,9 +565,9 @@ public class DepotMarshaller<T extends PersistentRecord>
getTableName() + "_" + idx.name(), idx.unique()); getTableName() + "_" + idx.name(), idx.unique());
} }
// its key generator // its value generators
if (_keyGenerator != null) { for (ValueGenerator vg : _valueGenerators.values()) {
_keyGenerator.init(conn, liaison); vg.init(conn, liaison);
} }
// and its full text search indexes // and its full text search indexes
@@ -969,6 +959,9 @@ public class DepotMarshaller<T extends PersistentRecord>
/** The @Computed annotation of this entity, or null. */ /** The @Computed annotation of this entity, or null. */
protected Computed _computed; protected Computed _computed;
/** A mapping of field names to value generators for that field. */
protected Map<String, ValueGenerator> _valueGenerators = new HashMap<String, ValueGenerator>();
/** A field marshaller for each persistent field in our object. */ /** A field marshaller for each persistent field in our object. */
protected Map<String, FieldMarshaller> _fields = new HashMap<String, FieldMarshaller>(); protected Map<String, FieldMarshaller> _fields = new HashMap<String, FieldMarshaller>();
@@ -976,9 +969,6 @@ public class DepotMarshaller<T extends PersistentRecord>
* define a primary key. */ * define a primary key. */
protected ArrayList<FieldMarshaller> _pkColumns; protected ArrayList<FieldMarshaller> _pkColumns;
/** The generator to use for auto-generating primary key values, or null. */
protected KeyGenerator _keyGenerator;
/** The persisent fields of our object, in definition order. */ /** The persisent fields of our object, in definition order. */
protected String[] _allFields; protected String[] _allFields;
@@ -999,4 +989,5 @@ public class DepotMarshaller<T extends PersistentRecord>
/** The name of the table we use to track schema versions. */ /** The name of the table we use to track schema versions. */
protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion"; protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion";
} }
@@ -37,10 +37,7 @@ import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.depot.Modifier.*; import com.samskivert.jdbc.depot.Modifier.*;
import com.samskivert.jdbc.depot.clause.DeleteClause; import com.samskivert.jdbc.depot.clause.DeleteClause;
import com.samskivert.jdbc.depot.clause.FieldOverride;
import com.samskivert.jdbc.depot.clause.FromOverride;
import com.samskivert.jdbc.depot.clause.InsertClause; import com.samskivert.jdbc.depot.clause.InsertClause;
import com.samskivert.jdbc.depot.clause.Join;
import com.samskivert.jdbc.depot.clause.QueryClause; import com.samskivert.jdbc.depot.clause.QueryClause;
import com.samskivert.jdbc.depot.clause.UpdateClause; import com.samskivert.jdbc.depot.clause.UpdateClause;
import com.samskivert.jdbc.depot.expression.SQLExpression; import com.samskivert.jdbc.depot.expression.SQLExpression;
@@ -175,24 +172,26 @@ public abstract class DepotRepository
// key will be null if record was supplied without a primary key // key will be null if record was supplied without a primary key
return _ctx.invoke(new CachingModifier<T>(record, key, key) { return _ctx.invoke(new CachingModifier<T>(record, key, key) {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
// update our modifier's key so that it can cache our results // set any auto-generated column values
Set<String> identityFields = marsh.generateFieldValues(conn, liaison, _result, false);
// if needed, update our modifier's key so that it can cache our results
if (_key == null) { if (_key == null) {
updateKey(marsh.assignPrimaryKey(conn, liaison, _result, false)); updateKey(marsh.getPrimaryKey(_result, false));
} }
String ixField = null; builder.newQuery(new InsertClause<T>(pClass, _result, identityFields));
if (_key == null && marsh.hasPrimaryKey() && marsh.getKeyGenerator() != null &&
marsh.getKeyGenerator() instanceof IdentityKeyGenerator) {
ixField = ((IdentityKeyGenerator) marsh.getKeyGenerator()).getColumn();
}
builder.newQuery(new InsertClause<T>(pClass, _result, ixField));
PreparedStatement stmt = builder.prepare(conn); PreparedStatement stmt = builder.prepare(conn);
try { try {
int mods = stmt.executeUpdate(); int mods = stmt.executeUpdate();
// check again in case we have a post-factum key generator
// run any post-factum value generators
marsh.generateFieldValues(conn, liaison, _result, true);
// and check once more if a key now exists
if (_key == null) { if (_key == null) {
updateKey(marsh.assignPrimaryKey(conn, liaison, _result, true)); updateKey(marsh.getPrimaryKey(_result, false));
} }
return mods; return mods;
} finally { } finally {
@@ -575,22 +574,25 @@ public abstract class DepotRepository
} }
// if the update modified zero rows or the primary key was obviously unset, do // if the update modified zero rows or the primary key was obviously unset, do
// an insertion // an insertion: first, set any auto-generated column values
Set<String> identityFields = marsh.generateFieldValues(conn, liaison, _result, false);
// update our modifier's key so that it can cache our results
if (_key == null) { if (_key == null) {
updateKey(marsh.assignPrimaryKey(conn, liaison, _result, false)); updateKey(marsh.getPrimaryKey(_result, false));
} }
String ixField = null; builder.newQuery(new InsertClause<T>(pClass, _result, identityFields));
if (_key == null && marsh.hasPrimaryKey() && marsh.getKeyGenerator() != null &&
marsh.getKeyGenerator() instanceof IdentityKeyGenerator) {
ixField = ((IdentityKeyGenerator) marsh.getKeyGenerator()).getColumn();
}
builder.newQuery(new InsertClause<T>(pClass, _result, ixField));
stmt = builder.prepare(conn); stmt = builder.prepare(conn);
int mods = stmt.executeUpdate(); int mods = stmt.executeUpdate();
// run any post-factum value generators
marsh.generateFieldValues(conn, liaison, _result, true);
// and check once more if a key now exists
if (_key == null) { if (_key == null) {
updateKey(marsh.assignPrimaryKey(conn, liaison, _result, true)); updateKey(marsh.getPrimaryKey(_result, false));
} }
created[0] = true; created[0] = true;
return mods; return mods;
@@ -222,10 +222,8 @@ public abstract class FieldMarshaller<T>
return; return;
} }
if (field.getAnnotation(Id.class) != null) { // figure out how we're going to generate our primary key values
// figure out how we're going to generate our primary key values _generatedValue = field.getAnnotation(GeneratedValue.class);
_generatedValue = field.getAnnotation(GeneratedValue.class);
}
} }
protected static class BooleanMarshaller extends FieldMarshaller<Boolean> { protected static class BooleanMarshaller extends FieldMarshaller<Boolean> {
@@ -3,7 +3,7 @@
// //
// samskivert library - useful routines for java programs // samskivert library - useful routines for java programs
// Copyright (C) 2006-2007 Michael Bayne, Pär Winzell // Copyright (C) 2006-2007 Michael Bayne, Pär Winzell
// //
// This library is free software; you can redistribute it and/or modify it // 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 // 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 // by the Free Software Foundation; either version 2.1 of the License, or
@@ -24,17 +24,16 @@ import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.LiaisonRegistry;
import com.samskivert.jdbc.depot.annotation.GeneratedValue; import com.samskivert.jdbc.depot.annotation.GeneratedValue;
/** /**
* Generates primary keys using an identity column. * Generates primary keys using an identity column.
*/ */
public class IdentityKeyGenerator extends KeyGenerator public class IdentityValueGenerator extends ValueGenerator
{ {
public IdentityKeyGenerator (GeneratedValue gv, String table, String column) public IdentityValueGenerator (GeneratedValue gv, DepotMarshaller dm, FieldMarshaller fm)
{ {
super(gv, table, column); super(gv, dm, fm);
} }
// from interface KeyGenerator // from interface KeyGenerator
@@ -47,13 +46,14 @@ public class IdentityKeyGenerator extends KeyGenerator
public void init (Connection conn, DatabaseLiaison liaison) public void init (Connection conn, DatabaseLiaison liaison)
throws SQLException throws SQLException
{ {
liaison.initializeGenerator(conn, _table, _column, _initialValue, _allocationSize); liaison.initializeGenerator(
conn, _dm.getTableName(), _fm.getColumnName(), _initialValue, _allocationSize);
} }
// from interface KeyGenerator // from interface KeyGenerator
public int nextGeneratedValue (Connection conn, DatabaseLiaison liaison) public int nextGeneratedValue (Connection conn, DatabaseLiaison liaison)
throws SQLException throws SQLException
{ {
return liaison.lastInsertedId(conn, _table, _column); return liaison.lastInsertedId(conn, _dm.getTableName(), _fm.getColumnName());
} }
} }
@@ -30,16 +30,15 @@ import com.samskivert.jdbc.depot.annotation.TableGenerator;
import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil; import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.LiaisonRegistry;
/** /**
* Generates primary keys using an external table . * Generates primary keys using an external table .
*/ */
public class TableKeyGenerator extends KeyGenerator public class TableValueGenerator extends ValueGenerator
{ {
public TableKeyGenerator (TableGenerator tg, GeneratedValue gv, String table, String column) public TableValueGenerator (TableGenerator tg, GeneratedValue gv, DepotMarshaller dm, FieldMarshaller fm)
{ {
super(gv, table, column); super(gv, dm, fm);
_valueTable = defStr(tg.table(), "IdSequences"); _valueTable = defStr(tg.table(), "IdSequences");
_pkColumnName = defStr(tg.pkColumnName(), "sequence"); _pkColumnName = defStr(tg.pkColumnName(), "sequence");
_pkColumnValue = defStr(tg.pkColumnValue(), "default"); _pkColumnValue = defStr(tg.pkColumnValue(), "default");
@@ -27,16 +27,16 @@ import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.depot.annotation.GeneratedValue; import com.samskivert.jdbc.depot.annotation.GeneratedValue;
/** /**
* Defines the interface to our primary key generators. * Defines the interface to our value generators.
*/ */
public abstract class KeyGenerator public abstract class ValueGenerator
{ {
public KeyGenerator (GeneratedValue gv, String table, String column) public ValueGenerator (GeneratedValue gv, DepotMarshaller dm, FieldMarshaller fm)
{ {
_allocationSize = gv.allocationSize(); _allocationSize = gv.allocationSize();
_initialValue = gv.initialValue(); _initialValue = gv.initialValue();
_table = table; _dm = dm;
_column = column; _fm = fm;
} }
/** /**
@@ -57,24 +57,19 @@ public abstract class KeyGenerator
public abstract int nextGeneratedValue (Connection conn, DatabaseLiaison liaison) public abstract int nextGeneratedValue (Connection conn, DatabaseLiaison liaison)
throws SQLException; throws SQLException;
/**
* Returns the name of the table for which we're generating a key value. public DepotMarshaller getDepotMarshaller ()
*/
public String getTable ()
{ {
return _table; return _dm;
} }
/** public FieldMarshaller getFieldMarshaller ()
* Returns the name of the column for which we're generating a key value.
*/
public String getColumn ()
{ {
return _column; return _fm;
} }
protected int _initialValue; protected int _initialValue;
protected int _allocationSize; protected int _allocationSize;
protected String _table; protected DepotMarshaller _dm;
protected String _column; protected FieldMarshaller _fm;
} }
@@ -21,6 +21,7 @@
package com.samskivert.jdbc.depot.clause; package com.samskivert.jdbc.depot.clause;
import java.util.Collection; import java.util.Collection;
import java.util.Set;
import com.samskivert.jdbc.depot.PersistentRecord; import com.samskivert.jdbc.depot.PersistentRecord;
import com.samskivert.jdbc.depot.expression.ExpressionVisitor; import com.samskivert.jdbc.depot.expression.ExpressionVisitor;
@@ -30,11 +31,12 @@ import com.samskivert.jdbc.depot.expression.ExpressionVisitor;
*/ */
public class InsertClause<T extends PersistentRecord> extends QueryClause public class InsertClause<T extends PersistentRecord> extends QueryClause
{ {
public InsertClause (Class<? extends PersistentRecord> pClass, Object pojo, String ixField) public InsertClause (
Class<? extends PersistentRecord> pClass, Object pojo, Set<String> identityFields)
{ {
_pClass = pClass; _pClass = pClass;
_pojo = pojo; _pojo = pojo;
_ixField = ixField; _idFields = identityFields;
} }
public Class<? extends PersistentRecord> getPersistentClass () public Class<? extends PersistentRecord> getPersistentClass ()
@@ -47,9 +49,9 @@ public class InsertClause<T extends PersistentRecord> extends QueryClause
return _pojo; return _pojo;
} }
public String getIndexField () public Set<String> getIdentityFields ()
{ {
return _ixField; return _idFields;
} }
// from SQLExpression // from SQLExpression
@@ -70,5 +72,5 @@ public class InsertClause<T extends PersistentRecord> extends QueryClause
/** The object from which to fetch values, or null. */ /** The object from which to fetch values, or null. */
protected Object _pojo; protected Object _pojo;
protected String _ixField; protected Set<String> _idFields;
} }