Added support for hand-registered migrations (untested).
git-svn-id: https://samskivert.googlecode.com/svn/trunk@2005 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
@@ -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 "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user