From 8e446fe93ffc9c010947c383c97968e1d0746490 Mon Sep 17 00:00:00 2001 From: Par Winzell Date: Tue, 21 Aug 2007 15:02:12 +0000 Subject: [PATCH] Generalize the automatic key value generation code so that it works on any column, and on any number of columns within a record. --- .../samskivert/jdbc/depot/BindVisitor.java | 5 +- .../samskivert/jdbc/depot/BuildVisitor.java | 5 +- .../jdbc/depot/DepotMarshaller.java | 123 ++++++++---------- .../jdbc/depot/DepotRepository.java | 46 +++---- .../jdbc/depot/FieldMarshaller.java | 6 +- ...rator.java => IdentityValueGenerator.java} | 14 +- ...enerator.java => TableValueGenerator.java} | 7 +- ...{KeyGenerator.java => ValueGenerator.java} | 29 ++--- .../jdbc/depot/clause/InsertClause.java | 12 +- 9 files changed, 118 insertions(+), 129 deletions(-) rename src/java/com/samskivert/jdbc/depot/{IdentityKeyGenerator.java => IdentityValueGenerator.java} (80%) rename src/java/com/samskivert/jdbc/depot/{TableKeyGenerator.java => TableValueGenerator.java} (95%) rename src/java/com/samskivert/jdbc/depot/{KeyGenerator.java => ValueGenerator.java} (75%) diff --git a/src/java/com/samskivert/jdbc/depot/BindVisitor.java b/src/java/com/samskivert/jdbc/depot/BindVisitor.java index cc7d09c..77eb6f3 100644 --- a/src/java/com/samskivert/jdbc/depot/BindVisitor.java +++ b/src/java/com/samskivert/jdbc/depot/BindVisitor.java @@ -22,6 +22,7 @@ package com.samskivert.jdbc.depot; import java.sql.PreparedStatement; import java.util.Map; +import java.util.Set; import com.samskivert.jdbc.depot.Key.WhereCondition; import com.samskivert.jdbc.depot.clause.DeleteClause; @@ -240,9 +241,9 @@ public class BindVisitor implements ExpressionVisitor DepotMarshaller marsh = _types.getMarshaller(insertClause.getPersistentClass()); Object pojo = insertClause.getPojo(); - String ixField = insertClause.getIndexField(); + Set generatedFields = insertClause.getIdentityFields(); for (String field : marsh.getColumnFieldNames()) { - if (ixField == null || !ixField.equals(field)) { + if (!generatedFields.contains(field)) { marsh.getFieldMarshaller(field).readFromObject(pojo, _stmt, _argIdx ++); } } diff --git a/src/java/com/samskivert/jdbc/depot/BuildVisitor.java b/src/java/com/samskivert/jdbc/depot/BuildVisitor.java index 5a0d651..3f35191 100644 --- a/src/java/com/samskivert/jdbc/depot/BuildVisitor.java +++ b/src/java/com/samskivert/jdbc/depot/BuildVisitor.java @@ -24,6 +24,7 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import com.samskivert.jdbc.depot.Key.WhereCondition; import com.samskivert.jdbc.depot.annotation.Computed; @@ -450,12 +451,12 @@ public abstract class BuildVisitor implements ExpressionVisitor } _builder.append(") values("); - String ixField = insertClause.getIndexField(); + Set generatedFields = insertClause.getIdentityFields(); for (int ii = 0; ii < fields.length; ii++) { if (ii > 0) { _builder.append(", "); } - if (ixField != null && ixField.equals(fields[ii])) { + if (generatedFields.contains(fields[ii])) { _builder.append("DEFAULT"); } else { _builder.append("?"); diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 60aa1ed..b2e059c 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -103,6 +103,8 @@ public class DepotMarshaller } } + boolean seenIdentityGenerator = false; + // introspect on the class and create marshallers for persistent fields ArrayList fields = new ArrayList(); for (Field field : _pclass.getFields()) { @@ -136,38 +138,19 @@ public class DepotMarshaller _pkColumns = new ArrayList(); } _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) { - // and if there is, make sure we've a single-column id - if (_pkColumns.size() > 1) { - throw new IllegalArgumentException( - "Cannot use @GeneratedValue on multiple-column @Id's"); - } + // check if this field defines a new TableGenerator + generator = field.getAnnotation(TableGenerator.class); + if (generator != null) { + context.tableGenerators.put(generator.name(), generator); + } - // the primary key must be numeric if we are to auto-assign it - Class ftype = keyField.getField().getType(); + // check if this field is auto-generated + GeneratedValue gv = fm.getGeneratedValue(); + if (gv != null) { + // we can only do this on numeric fields + Class ftype = field.getType(); boolean isNumeric = ( ftype.equals(Byte.TYPE) || ftype.equals(Byte.class) || ftype.equals(Short.TYPE) || ftype.equals(Short.class) || @@ -175,14 +158,17 @@ public class DepotMarshaller ftype.equals(Long.TYPE) || ftype.equals(Long.class)); if (!isNumeric) { throw new IllegalArgumentException( - "Cannot use @GeneratedValue on non-numeric column"); + "Cannot use @GeneratedValue on non-numeric column: " + field.getName()); } - switch(gv.strategy()) { case AUTO: case IDENTITY: - _keyGenerator = new IdentityKeyGenerator( - gv, getTableName(), keyField.getColumnName()); + if (seenIdentityGenerator) { + 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; case TABLE: @@ -192,11 +178,12 @@ public class DepotMarshaller throw new IllegalArgumentException( "Unknown generator [generator=" + name + "]"); } - _keyGenerator = new TableKeyGenerator( - generator, gv, getTableName(), keyField.getColumnName()); + _valueGenerators.put( + field.getName(), new TableValueGenerator(generator, gv, this, fm)); break; } } + } // generate our full list of fields/columns for use in queries @@ -270,12 +257,12 @@ public class DepotMarshaller } /** - * 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. */ - public KeyGenerator getKeyGenerator () + public Iterable getValueGenerators () { - return _keyGenerator; + return _valueGenerators.values(); } /** @@ -435,37 +422,40 @@ public class DepotMarshaller } /** - * Fills in the primary key just assigned to the supplied persistence object by the execution - * of the results of {@link #createInsert}. + * Go through the registered {@link ValueGenerator}s for our persistent object and run the + * 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 - * this is not the right time to assign the key. + * The return value is only non-empty for the !postFactum phase, in which case it is a set + * 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 generateFieldValues ( Connection conn, DatabaseLiaison liaison, Object po, boolean postFactum) - throws SQLException { - // if we have no primary key or no generator, then we're done - if (!hasPrimaryKey() || _keyGenerator == null) { - return null; - } + Set idFields = new HashSet(); - // run this generator either before or after the actual insertion - if (_keyGenerator.isPostFactum() != postFactum) { - return null; - } + for (ValueGenerator vg : _valueGenerators.values()) { + if (!postFactum && vg instanceof IdentityValueGenerator) { + idFields.add(vg.getFieldMarshaller().getField().getName()); + } + if (vg.isPostFactum() != postFactum) { + continue; + } - try { - int nextValue = _keyGenerator.nextGeneratedValue(conn, liaison); - _pkColumns.get(0).getField().set(po, nextValue); - return makePrimaryKey(nextValue); - } catch (Exception e) { - String errmsg = "Failed to assign primary key [type=" + _pclass + "]"; - throw (SQLException) new SQLException(errmsg).initCause(e); + try { + int nextValue = vg.nextGeneratedValue(conn, liaison); + vg.getFieldMarshaller().getField().set(po, nextValue); + + } catch (Exception e) { + throw new IllegalStateException( + "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 marshaller. @@ -575,9 +565,9 @@ public class DepotMarshaller getTableName() + "_" + idx.name(), idx.unique()); } - // its key generator - if (_keyGenerator != null) { - _keyGenerator.init(conn, liaison); + // its value generators + for (ValueGenerator vg : _valueGenerators.values()) { + vg.init(conn, liaison); } // and its full text search indexes @@ -969,6 +959,9 @@ public class DepotMarshaller /** The @Computed annotation of this entity, or null. */ protected Computed _computed; + /** A mapping of field names to value generators for that field. */ + protected Map _valueGenerators = new HashMap(); + /** A field marshaller for each persistent field in our object. */ protected Map _fields = new HashMap(); @@ -976,9 +969,6 @@ public class DepotMarshaller * define a primary key. */ protected ArrayList _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. */ protected String[] _allFields; @@ -999,4 +989,5 @@ public class DepotMarshaller /** 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 cea2dcd..24e254e 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java +++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java @@ -37,10 +37,7 @@ import com.samskivert.jdbc.JDBCUtil; import com.samskivert.jdbc.depot.Modifier.*; 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.Join; import com.samskivert.jdbc.depot.clause.QueryClause; import com.samskivert.jdbc.depot.clause.UpdateClause; 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 return _ctx.invoke(new CachingModifier(record, key, key) { 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 identityFields = marsh.generateFieldValues(conn, liaison, _result, false); + + // if needed, update our modifier's key so that it can cache our results if (_key == null) { - updateKey(marsh.assignPrimaryKey(conn, liaison, _result, false)); + updateKey(marsh.getPrimaryKey(_result, false)); } - String ixField = null; - if (_key == null && marsh.hasPrimaryKey() && marsh.getKeyGenerator() != null && - marsh.getKeyGenerator() instanceof IdentityKeyGenerator) { - ixField = ((IdentityKeyGenerator) marsh.getKeyGenerator()).getColumn(); - } - builder.newQuery(new InsertClause(pClass, _result, ixField)); + builder.newQuery(new InsertClause(pClass, _result, identityFields)); PreparedStatement stmt = builder.prepare(conn); try { 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) { - updateKey(marsh.assignPrimaryKey(conn, liaison, _result, true)); + updateKey(marsh.getPrimaryKey(_result, false)); } return mods; } finally { @@ -575,22 +574,25 @@ public abstract class DepotRepository } // 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 identityFields = marsh.generateFieldValues(conn, liaison, _result, false); + + // update our modifier's key so that it can cache our results if (_key == null) { - updateKey(marsh.assignPrimaryKey(conn, liaison, _result, false)); + updateKey(marsh.getPrimaryKey(_result, false)); } - String ixField = null; - if (_key == null && marsh.hasPrimaryKey() && marsh.getKeyGenerator() != null && - marsh.getKeyGenerator() instanceof IdentityKeyGenerator) { - ixField = ((IdentityKeyGenerator) marsh.getKeyGenerator()).getColumn(); - } - builder.newQuery(new InsertClause(pClass, _result, ixField)); + builder.newQuery(new InsertClause(pClass, _result, identityFields)); stmt = builder.prepare(conn); 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) { - updateKey(marsh.assignPrimaryKey(conn, liaison, _result, true)); + updateKey(marsh.getPrimaryKey(_result, false)); } created[0] = true; return mods; diff --git a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java index aaa3a92..2f7afb7 100644 --- a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java @@ -222,10 +222,8 @@ public abstract class FieldMarshaller return; } - if (field.getAnnotation(Id.class) != null) { - // figure out how we're going to generate our primary key values - _generatedValue = field.getAnnotation(GeneratedValue.class); - } + // figure out how we're going to generate our primary key values + _generatedValue = field.getAnnotation(GeneratedValue.class); } protected static class BooleanMarshaller extends FieldMarshaller { diff --git a/src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java b/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java similarity index 80% rename from src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java rename to src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java index 394b473..ae89f33 100644 --- a/src/java/com/samskivert/jdbc/depot/IdentityKeyGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java @@ -3,7 +3,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2006-2007 Michael Bayne, Pär Winzell -// +// // 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 @@ -24,17 +24,16 @@ import java.sql.Connection; import java.sql.SQLException; import com.samskivert.jdbc.DatabaseLiaison; -import com.samskivert.jdbc.LiaisonRegistry; import com.samskivert.jdbc.depot.annotation.GeneratedValue; /** * 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 @@ -47,13 +46,14 @@ public class IdentityKeyGenerator extends KeyGenerator public void init (Connection conn, DatabaseLiaison liaison) throws SQLException { - liaison.initializeGenerator(conn, _table, _column, _initialValue, _allocationSize); + liaison.initializeGenerator( + conn, _dm.getTableName(), _fm.getColumnName(), _initialValue, _allocationSize); } // from interface KeyGenerator public int nextGeneratedValue (Connection conn, DatabaseLiaison liaison) throws SQLException { - return liaison.lastInsertedId(conn, _table, _column); + return liaison.lastInsertedId(conn, _dm.getTableName(), _fm.getColumnName()); } } diff --git a/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java b/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java similarity index 95% rename from src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java rename to src/java/com/samskivert/jdbc/depot/TableValueGenerator.java index 7392030..7da8b5f 100644 --- a/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java @@ -30,16 +30,15 @@ import com.samskivert.jdbc.depot.annotation.TableGenerator; import com.samskivert.jdbc.DatabaseLiaison; import com.samskivert.jdbc.JDBCUtil; -import com.samskivert.jdbc.LiaisonRegistry; /** * 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"); _pkColumnName = defStr(tg.pkColumnName(), "sequence"); _pkColumnValue = defStr(tg.pkColumnValue(), "default"); diff --git a/src/java/com/samskivert/jdbc/depot/KeyGenerator.java b/src/java/com/samskivert/jdbc/depot/ValueGenerator.java similarity index 75% rename from src/java/com/samskivert/jdbc/depot/KeyGenerator.java rename to src/java/com/samskivert/jdbc/depot/ValueGenerator.java index cbbc96c..b12a3ab 100644 --- a/src/java/com/samskivert/jdbc/depot/KeyGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/ValueGenerator.java @@ -27,16 +27,16 @@ import com.samskivert.jdbc.DatabaseLiaison; 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(); _initialValue = gv.initialValue(); - _table = table; - _column = column; + _dm = dm; + _fm = fm; } /** @@ -57,24 +57,19 @@ public abstract class KeyGenerator public abstract int nextGeneratedValue (Connection conn, DatabaseLiaison liaison) throws SQLException; - /** - * Returns the name of the table for which we're generating a key value. - */ - public String getTable () + + public DepotMarshaller getDepotMarshaller () { - return _table; + return _dm; } - /** - * Returns the name of the column for which we're generating a key value. - */ - public String getColumn () + public FieldMarshaller getFieldMarshaller () { - return _column; + return _fm; } protected int _initialValue; protected int _allocationSize; - protected String _table; - protected String _column; + protected DepotMarshaller _dm; + protected FieldMarshaller _fm; } diff --git a/src/java/com/samskivert/jdbc/depot/clause/InsertClause.java b/src/java/com/samskivert/jdbc/depot/clause/InsertClause.java index bde7dc9..039d07e 100644 --- a/src/java/com/samskivert/jdbc/depot/clause/InsertClause.java +++ b/src/java/com/samskivert/jdbc/depot/clause/InsertClause.java @@ -21,6 +21,7 @@ package com.samskivert.jdbc.depot.clause; import java.util.Collection; +import java.util.Set; import com.samskivert.jdbc.depot.PersistentRecord; import com.samskivert.jdbc.depot.expression.ExpressionVisitor; @@ -30,11 +31,12 @@ import com.samskivert.jdbc.depot.expression.ExpressionVisitor; */ public class InsertClause extends QueryClause { - public InsertClause (Class pClass, Object pojo, String ixField) + public InsertClause ( + Class pClass, Object pojo, Set identityFields) { _pClass = pClass; _pojo = pojo; - _ixField = ixField; + _idFields = identityFields; } public Class getPersistentClass () @@ -47,9 +49,9 @@ public class InsertClause extends QueryClause return _pojo; } - public String getIndexField () + public Set getIdentityFields () { - return _ixField; + return _idFields; } // from SQLExpression @@ -70,5 +72,5 @@ public class InsertClause extends QueryClause /** The object from which to fetch values, or null. */ protected Object _pojo; - protected String _ixField; + protected Set _idFields; }