Added support for hand-registered migrations (untested).

This commit is contained in:
Michael Bayne
2006-12-21 19:06:18 +00:00
parent 79c8d020c3
commit d9a5cdaec8
5 changed files with 301 additions and 130 deletions
@@ -21,7 +21,6 @@
package com.samskivert.jdbc.depot;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
@@ -37,6 +36,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.depot.annotation.Computed;
import com.samskivert.jdbc.depot.annotation.Entity;
import com.samskivert.jdbc.depot.annotation.GeneratedValue;
@@ -101,7 +101,8 @@ public class DepotMarshaller<T>
int mods = field.getModifiers();
// check for a static constant schema version
if ((mods & Modifier.STATIC) != 0 && field.getName().equals(SCHEMA_VERSION_FIELD)) {
if (java.lang.reflect.Modifier.isStatic(mods) &&
field.getName().equals(SCHEMA_VERSION_FIELD)) {
try {
_schemaVersion = (Integer)field.get(null);
} catch (Exception e) {
@@ -111,7 +112,8 @@ public class DepotMarshaller<T>
}
// the field must be public, non-static and non-transient
if (((mods & Modifier.PUBLIC) == 0) || ((mods & Modifier.STATIC) != 0) ||
if (!java.lang.reflect.Modifier.isPublic(mods) ||
java.lang.reflect.Modifier.isStatic(mods) ||
field.getAnnotation(Transient.class) != null) {
continue;
}
@@ -292,104 +294,12 @@ public class DepotMarshaller<T>
}
/**
* Initializes the table used by this marshaller. If the table does not exist, it will be
* created. If the schema version specified by the persistent object is newer than the database
* schema, it will be migrated.
* Returns true if this marshaller has been initialized ({@link #init} has been called), its
* migrations run and it is ready for operation. False otherwise.
*/
public void init (Connection conn)
throws SQLException
public boolean isInitialized ()
{
// if we have no table (i.e. we're a computed entity), we have nothing to create
if (getTableName() == null) {
return;
}
// check to see if our schema version table exists, create it if not
JDBCUtil.createTableIfMissing(conn, SCHEMA_VERSION_TABLE,
new String[] { "persistentClass VARCHAR(255) NOT NULL",
"version INTEGER NOT NULL" }, "");
// now create the table for our persistent class if it does not exist
if (!JDBCUtil.tableExists(conn, getTableName())) {
log.fine("Creating table " + getTableName() + " (" +
StringUtil.join(_columnDefinitions, ", ") + ") " + _postamble);
JDBCUtil.createTableIfMissing(conn, getTableName(), _columnDefinitions, _postamble);
updateVersion(conn, 1);
}
// if we have a key generator, initialize that too
if (_keyGenerator != null) {
_keyGenerator.init(conn);
}
// if schema versioning is disabled, stop now
if (_schemaVersion < 0) {
return;
}
// make sure the versions match
int currentVersion = readVersion(conn);
if (currentVersion == _schemaVersion) {
return;
}
log.info("Migrating " + getTableName() + " from " +
currentVersion + " to " + _schemaVersion + "...");
// otherwise try to migrate the schema; doing column additions magically and running any
// registered hand-migrations
DatabaseMetaData meta = conn.getMetaData();
ResultSet rs = meta.getColumns(null, null, getTableName(), "%");
HashSet<String> columns = new HashSet<String>();
while (rs.next()) {
columns.add(rs.getString("COLUMN_NAME"));
}
for (String fname : _columnFields) {
FieldMarshaller fmarsh = _fields.get(fname);
if (columns.contains(fmarsh.getColumnName())) {
continue;
}
// otherwise add the column
String coldef = fmarsh.getColumnDefinition();
String query = "alter table " + getTableName() + " add column " + coldef;
// try to add it to the appropriate spot
int fidx = ListUtil.indexOf(_allFields, fmarsh.getColumnName());
if (fidx == 0) {
query += " first";
} else {
query += " after " + _allFields[fidx-1];
}
log.info("Adding column to " + getTableName() + ": " + coldef);
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate(query);
} finally {
stmt.close();
}
// if the column is a TIMESTAMP column, we need to run a special query to update all
// existing rows to the current time because MySQL annoyingly assigns them a default
// value of "0000-00-00 00:00:00" regardless of whether we explicitly provide a
// "DEFAULT" value for the column or not
if (coldef.toLowerCase().indexOf(" timestamp") != -1) {
query = "update " + getTableName() + " set " + fmarsh.getColumnName() + " = NOW()";
log.info("Assigning current time to TIMESTAMP column: " + query);
stmt = conn.createStatement();
try {
stmt.executeUpdate(query);
} finally {
stmt.close();
}
}
}
// TODO: run any registered hand migrations
updateVersion(conn, _schemaVersion);
return _initialized;
}
/**
@@ -628,6 +538,167 @@ public class DepotMarshaller<T>
return pstmt;
}
/**
* This is called by the persistence context to register a migration for the entity managed by
* this marshaller.
*/
protected void registerMigration (EntityMigration migration)
{
_migrations.add(migration);
}
/**
* Initializes the table used by this marshaller. This is called automatically by the {@link
* PersistenceContext} the first time an entity is used. If the table does not exist, it will
* be created. If the schema version specified by the persistent object is newer than the
* database schema, it will be migrated.
*/
protected void init (PersistenceContext ctx)
throws PersistenceException
{
if (_initialized) { // sanity check
throw new IllegalStateException(
"Cannot re-initialize marshaller [type=" + _pclass + "].");
}
_initialized = true;
// if we have no table (i.e. we're a computed entity), we have nothing to create
if (getTableName() == null) {
return;
}
// check to see if our schema version table exists, create it if not
ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
JDBCUtil.createTableIfMissing(
conn, SCHEMA_VERSION_TABLE,
new String[] { "persistentClass VARCHAR(255) NOT NULL",
"version INTEGER NOT NULL" }, "");
return 0;
}
});
// now create the table for our persistent class if it does not exist
ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
if (!JDBCUtil.tableExists(conn, getTableName())) {
log.fine("Creating table " + getTableName() + " (" +
StringUtil.join(_columnDefinitions, ", ") + ") " + _postamble);
JDBCUtil.createTableIfMissing(
conn, getTableName(), _columnDefinitions, _postamble);
updateVersion(conn, 1);
}
return 0;
}
});
// if we have a key generator, initialize that too
if (_keyGenerator != null) {
ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
_keyGenerator.init(conn);
return 0;
}
});
}
// if schema versioning is disabled, stop now
if (_schemaVersion < 0) {
return;
}
// make sure the versions match
int currentVersion = ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
String query = "select version from " + SCHEMA_VERSION_TABLE +
" where persistentClass = '" + getTableName() + "'";
Statement stmt = conn.createStatement();
try {
ResultSet rs = stmt.executeQuery(query);
return (rs.next()) ? rs.getInt(1) : 1;
} finally {
stmt.close();
}
}
});
if (currentVersion == _schemaVersion) {
return;
}
log.info("Migrating " + getTableName() + " from " +
currentVersion + " to " + _schemaVersion + "...");
// otherwise try to migrate the schema
final HashSet<String> columns = new HashSet<String>();
ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
DatabaseMetaData meta = conn.getMetaData();
ResultSet rs = meta.getColumns(null, null, getTableName(), "%");
while (rs.next()) {
columns.add(rs.getString("COLUMN_NAME"));
}
return 0;
}
});
// run our pre-default-migrations
for (EntityMigration migration : _migrations) {
if (migration.runBeforeDefault() &&
migration.shouldRunMigration(currentVersion, _schemaVersion)) {
ctx.invoke(migration);
}
}
// do our "default" migrations magically
for (String fname : _columnFields) {
FieldMarshaller fmarsh = _fields.get(fname);
if (columns.contains(fmarsh.getColumnName())) {
continue;
}
// otherwise add the column
String coldef = fmarsh.getColumnDefinition();
String query = "alter table " + getTableName() + " add column " + coldef;
// try to add it to the appropriate spot
int fidx = ListUtil.indexOf(_allFields, fmarsh.getColumnName());
if (fidx == 0) {
query += " first";
} else {
query += " after " + _allFields[fidx-1];
}
log.info("Adding column to " + getTableName() + ": " + coldef);
ctx.invoke(new Modifier.Simple(query));
// if the column is a TIMESTAMP column, we need to run a special query to update all
// existing rows to the current time because MySQL annoyingly assigns them a default
// value of "0000-00-00 00:00:00" regardless of whether we explicitly provide a
// "DEFAULT" value for the column or not
if (coldef.toLowerCase().indexOf(" timestamp") != -1) {
query = "update " + getTableName() + " set " + fmarsh.getColumnName() + " = NOW()";
log.info("Assigning current time to TIMESTAMP column: " + query);
ctx.invoke(new Modifier.Simple(query));
}
}
// run our post-default-migrations
for (EntityMigration migration : _migrations) {
if (!migration.runBeforeDefault() &&
migration.shouldRunMigration(currentVersion, _schemaVersion)) {
ctx.invoke(migration);
}
}
// record our new version in the database
ctx.invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
updateVersion(conn, _schemaVersion);
return 0;
}
});
}
protected void updateVersion (Connection conn, int version)
throws SQLException
{
@@ -645,20 +716,6 @@ public class DepotMarshaller<T>
}
}
protected int readVersion (Connection conn)
throws SQLException
{
String query = "select version from " + SCHEMA_VERSION_TABLE +
" where persistentClass = '" + getTableName() + "'";
Statement stmt = conn.createStatement();
try {
ResultSet rs = stmt.executeQuery(query);
return (rs.next()) ? rs.getInt(1) : 1;
} finally {
stmt.close();
}
}
protected void requireNotComputed (String action)
throws SQLException
{
@@ -700,6 +757,12 @@ public class DepotMarshaller<T>
/** Used when creating and migrating our table schema. */
protected String _postamble = "";
/** Indicates that we have been initialized (created or migrated our tables). */
protected boolean _initialized;
/** A list of hand registered entity migrations to run prior to doing the default migration. */
protected ArrayList<EntityMigration> _migrations = new ArrayList<EntityMigration>();
/** The name of the table we use to track schema versions. */
protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion";
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 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;
/**
* These can be registered with the {@link PersistenceContext} to effect hand-coded migrations
* between entity versions. The modifier should override {@link #invoke} to perform its
* migrations. See {@link PersistenceContext#registerPreMigration} and {@link
* PersistenceContext#registerPostMigration}.
*/
public abstract class EntityMigration extends Modifier
{
/**
* If this method returns true, this migration will be run <b>before</b> the default
* migrations, if false it will be run after.
*/
public boolean runBeforeDefault ()
{
return true;
}
/**
* When an Entity is being migrated, this method will be called to check whether this migration
* should be run.
*/
public abstract boolean shouldRunMigration (int currentVersion, int targetVersion);
protected EntityMigration ()
{
super(null);
}
}
@@ -22,6 +22,7 @@ package com.samskivert.jdbc.depot;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import com.samskivert.jdbc.depot.clause.Where;
@@ -30,6 +31,27 @@ import com.samskivert.jdbc.depot.clause.Where;
*/
public abstract class Modifier
{
/** A simple modifier that executes a single SQL statement. No cache flushing is done as a
* result of this operation. */
public static class Simple extends Modifier
{
public Simple (String query) {
super(null);
_query = query;
}
public int invoke (Connection conn) throws SQLException {
Statement stmt = conn.createStatement();
try {
return stmt.executeUpdate(_query);
} finally {
stmt.close();
}
}
protected String _query;
}
public abstract int invoke (Connection conn) throws SQLException;
protected Modifier (Where key)
@@ -57,26 +57,48 @@ public class PersistenceContext
}
/**
* Returns the marshaller for the specified persistent object class,
* creating and initializing it if necessary.
* Registers a migration for the specified entity class.
*
* <p> This method must be called <b>before</b> an Entity is used by any repository. Thus you
* should register all migrations immediately after creating your persistence context or if you
* are careful to ensure that your Entities are only used by a single repository, you can
* register your migrations in the constructor for that repository.
*
* <p> Note that the migration process is as follows:
*
* <ul><li> Note the difference between the entity's declared version and the version recorded
* in the database.
* <li> Run all registered pre-migrations
* <li> Perform all default migrations (column additions and removals)
* <li> Run all registered post-migrations </ul>
*
* Thus you must either be prepared for the entity to be at <b>any</b> version prior to your
* migration target version because we may start up, find the schema at version 1 and the
* Entity class at version 8 and do all "standard" migrations in one fell swoop. So if a column
* got added in version 2 and renamed in version 6 and your migration was registered for
* version 6 to do that migration, it must be prepared for the column not to exist at all.
*
* <p> If you want a completely predictable migration process, never use the default migrations
* and register a pre-migration for every single schema migration and they will then be
* guaranteed to be run in registration order and with predictable pre- and post-conditions.
*/
public <T> void registerMigration (Class<T> type, EntityMigration migration)
{
getRawMarshaller(type).registerMigration(migration);
}
/**
* Returns the marshaller for the specified persistent object class, creating and initializing
* it if necessary.
*/
public <T> DepotMarshaller<T> getMarshaller (Class<T> type)
throws PersistenceException
{
@SuppressWarnings("unchecked") DepotMarshaller<T> marshaller =
(DepotMarshaller<T>)_marshallers.get(type);
if (marshaller == null) {
_marshallers.put(
type, marshaller = new DepotMarshaller<T>(type, this));
// initialize the marshaller which may create or migrate the table
// for its underlying persistent object
final DepotMarshaller<T> fm = marshaller;
invoke(new Modifier(null) {
public int invoke (Connection conn) throws SQLException {
fm.init(conn);
return 0;
}
});
DepotMarshaller<T> marshaller = getRawMarshaller(type);
if (!marshaller.isInitialized()) {
// initialize the marshaller which may create or migrate the table for its underlying
// persistent object
marshaller.init(this);
}
return marshaller;
}
@@ -105,6 +127,20 @@ public class PersistenceContext
return result;
}
/**
* Looks up and creates, but does not initialize, the marshaller for the specified Entity
* type.
*/
protected <T> DepotMarshaller<T> getRawMarshaller (Class<T> type)
{
@SuppressWarnings("unchecked") DepotMarshaller<T> marshaller =
(DepotMarshaller<T>)_marshallers.get(type);
if (marshaller == null) {
_marshallers.put(type, marshaller = new DepotMarshaller<T>(type, this));
}
return marshaller;
}
/**
* Internal invoke method that takes care of transient retries
* for both queries and modifiers.
@@ -36,6 +36,6 @@ public @interface Entity
String name () default "";
/** Additional SQL to be added when creating this entity's table for the first time. You can
* specify additional indices here or supply a MySQL table storage type for example. */
* specify a MySQL table storage type for example. */
String postamble () default "";
}