From 3e8ac68af6305f2a5e177cdf1ec1038a67c2415e Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 17 Sep 2008 19:09:08 +0000 Subject: [PATCH] Properly handle @GeneratedValue(strategy=GenerationType.IDENTITY, initialValue=N) for non-zero values of N. This is complicated. Depot used to "initialize" value generators pretty much every time a table schema was migrated. For TableValueGenerator it could just NOOP if the proper row was in the sequence table. IdentityValueGenerator was just broken and ignored any request to configure an initialValue, so it never did anything. First I looked into preserving this frequent reinitialization behavior, but it turns out that it's not possible to read the current value of a sequence in Postgres without modifying it. So trying to do something like: SELECT setval('"FooRecord_fooId_seq"', max(currval('"FooRecord_fooId_seq"'), cast(initialValue as bigint))); doesn't work because you can't call currval() without first having called nextval() (and furthermore when you do that, the Postgres client grabs a block of sequence values for this client so that multiple clients can add rows to a table without having to coordinate with the server and one another to assign a new value every time a row is inserted). So I changed Depot around to not reinitialize value generators unless it really believes that the column in question is newly added. We can't rely on our own auto-column creation because it's possible for someone to create a column through a custom entity migration, so instead we just keep track of what columns we had before we started all of our migrations (automatic and custom) and check to see if there are any new columns after all is said and done and if any of those have value generators, we "create" them. This allows us to just setval() the sequence to the desired initial value without first reading it. This all fails if someone renames a column with a value generator. The column will appear to Depot to be new and it will try to recreate the value generator on that column. For TableValueGenerators that will basically NOOP and everything will be just like it was before. For IdentityValueGenerators, because of the way that Postgres magically creates a tablename_columnname_seq sequence for identity value generation, when the column is renamed, the sequence will retain the old column name and when Depot goes to try to setval() the sequence based on the new column name, it won't exist and the migration will fail. So at least we're not silently reinitializing the value generator to its initial value when a column (with an identity value generator) is renamed. We also don't try to do anything if the initialValue is not 1 (the default). So we avoid failing or doing anything wrong if those kinds of columns are renamed. So we only fail if someone renames a column with an identity value generator with a non-default initial value. Since one can only put a value generator on a primary key field and an identity value generator generally means you have something simple like FooRecord with fooId. This means someone is renaming their simple auto incrementing primary key field which seems extremely unlikely because those columns usually have an obvious name that you're never going to change. No problem! I also haven't fixed this problem for MySQL yet. I can only handle so much database befuckaroundery in one go. --- .../jdbc/depot/DepotMarshaller.java | 38 +++++++++++++++---- .../jdbc/depot/IdentityValueGenerator.java | 4 +- .../jdbc/depot/TableValueGenerator.java | 8 +--- .../samskivert/jdbc/depot/ValueGenerator.java | 8 +++- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 0697d21..c1ec6d3 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -705,9 +705,9 @@ public class DepotMarshaller getTableName() + "_" + idx.name(), idx.unique()); } - // initialize our value generators + // create our value generators for (ValueGenerator vg : _valueGenerators.values()) { - vg.init(conn, liaison); + vg.create(conn, liaison); } // and its full text search indexes @@ -744,6 +744,13 @@ public class DepotMarshaller // this is a little silly, but we need a copy for name disambiguation later Set indicesCopy = new HashSet(metaData.indexColumns.keySet()); + // figure out which columns we have in the table now, so that when all is said and done we + // can see what new columns we have in the table and run the creation code for any value + // generators that are defined on those columns (we can't just track the columns we add in + // our automatic migrations because someone might register custom migrations that add + // columns specially) + Set preMigrateColumns = new HashSet(metaData.tableColumns); + // add any missing columns for (String fname : _columnFields) { final FieldMarshaller fmarsh = _fields.get(fname); @@ -896,14 +903,31 @@ public class DepotMarshaller } } - // last of all (re-)initialize our value generators, since one might've been added - if (_valueGenerators.size() > 0) { + // now reload our table metadata so that we can see what columns we have now + metaData = TableMetaData.load(ctx, getTableName()); + + // initialize value generators for any columns that have been newly added + for (String column : metaData.tableColumns) { + if (preMigrateColumns.contains(column)) { + continue; + } + + // see if we have a value generator for this new column + final ValueGenerator valgen = _valueGenerators.get(column); + if (valgen == null) { + continue; + } + + // note: if someone renames a column that has an identity value generator, things will + // break because Postgres automatically creates a table_column_seq sequence that is + // used to generate values for that column and god knows what happens when that is + // renamed; plus we're potentially going to try to reinitialize it if it has a non-zero + // initialValue which will use the new column name to obtain the sequence name which + // ain't going to work either; we punt! ctx.invoke(new Modifier() { @Override public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - for (ValueGenerator vg : _valueGenerators.values()) { - vg.init(conn, liaison); - } + valgen.create(conn, liaison); return 0; } }); diff --git a/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java b/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java index 9870661..84d7f45 100644 --- a/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/IdentityValueGenerator.java @@ -43,10 +43,10 @@ public class IdentityValueGenerator extends ValueGenerator } @Override // from ValueGenerator - public void init (Connection conn, DatabaseLiaison liaison) + public void create (Connection conn, DatabaseLiaison liaison) throws SQLException { - // identity value generators are auto-created by the database + liaison.createGenerator(conn, _dm.getTableName(), _fm.getColumnName(), _initialValue); } @Override // from ValueGenerator diff --git a/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java b/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java index c060630..602c71a 100644 --- a/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/TableValueGenerator.java @@ -54,14 +54,9 @@ public class TableValueGenerator extends ValueGenerator } @Override // from ValueGenerator - public void init (Connection conn, DatabaseLiaison liaison) + public void create (Connection conn, DatabaseLiaison liaison) throws SQLException { - if (_initialized) { - return; - } - _initialized = true; - // make sure our table exists liaison.createTableIfMissing( conn, _valueTable, @@ -172,7 +167,6 @@ public class TableValueGenerator extends ValueGenerator return value; } - protected boolean _initialized; protected String _valueTable; protected String _pkColumnName; protected String _pkColumnValue; diff --git a/src/java/com/samskivert/jdbc/depot/ValueGenerator.java b/src/java/com/samskivert/jdbc/depot/ValueGenerator.java index c8bca36..e2e75f0 100644 --- a/src/java/com/samskivert/jdbc/depot/ValueGenerator.java +++ b/src/java/com/samskivert/jdbc/depot/ValueGenerator.java @@ -52,9 +52,13 @@ public abstract class ValueGenerator public abstract boolean isPostFactum (); /** - * Ensures the generator is prepared for operation, creating it (only) if necessary. + * Ensures the generator is prepared for operation, creating it if necessary. Care is taken to + * only run this the first time a column is created. However, if a column with a value + * generator is renamed, we can't distinguish that from a newly created column and we call this + * method again on the renamed column. If it's possible to not fail in that circumstance, try + * to avoid doing so. */ - public abstract void init (Connection conn, DatabaseLiaison liaison) + public abstract void create (Connection conn, DatabaseLiaison liaison) throws SQLException; /**