diff --git a/src/java/com/samskivert/jdbc/depot/DataMigration.java b/src/java/com/samskivert/jdbc/depot/DataMigration.java new file mode 100644 index 0000000..c05548f --- /dev/null +++ b/src/java/com/samskivert/jdbc/depot/DataMigration.java @@ -0,0 +1,73 @@ +// +// $Id$ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001-2008 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; + +/** + * Encapsulates a migration of data between entities that should be run only once and using the + * same safeguards applied to entity migrations. Note: this should not be used for schema + * migrations, use {@link SchemaMigration} for that. Data migrations are registered on a specific + * repository via {@link DepotRepository#registerMigration} and should be registered in the + * repository's constructor as they will be invoked (if appropriate) in the repository's {@link + * DepotRepository#init} method. + * + *
In general one will register an anonymous inner class in a repository's constructor and can + * then access methods in the repository directly: + * + *
+ * public class FooRepository extends DepotRepository {
+ * public FooRepository (PersistenceContext ctx) {
+ * super(ctx);
+ * registerMigration(new DataMigration("2008_09_25_referral_to_tracking_id") {
+ * public void invoke () throws DatabaseException {
+ * // feel free to use load() findAll(), update(), etc.
+ * }
+ * });
+ * }
+ *
+ */
+public abstract class DataMigration
+{
+ /**
+ * Creates a data migration with the specified unique identifier. The identifier must be unique
+ * across all users of the database and for all time, so be careful. Best to include the date
+ * and pertintent information, e.g. "2008_09_25_referral_to_tracking_id".
+ */
+ public DataMigration (String ident)
+ {
+ _ident = ident;
+ }
+
+ /**
+ * Returns the identifier of this migration.
+ */
+ public String getIdent ()
+ {
+ return _ident;
+ }
+
+ /**
+ * Effects the data migration.
+ */
+ public abstract void invoke () throws DatabaseException;
+
+ /** The unique identifier for this migration. */
+ protected String _ident;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
index 5f024de..ac517de 100644
--- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
+++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
@@ -630,10 +630,10 @@ public class DepotMarshaller See {@link PersistenceContext#registerMigration} for details on how schema migrations
+ * operate and how they might interact with data migrations.
+ */
+ protected void registerMigration (DataMigration migration)
+ {
+ if (_dataMigs == null) {
+ // we've already been initialized, so we have to run this migration immediately
+ runMigration(migration);
+ } else {
+ _dataMigs.add(migration);
+ }
+ }
+
/**
* Adds the persistent classes used by this repository to the supplied set.
*/
@@ -889,5 +927,67 @@ public abstract class DepotRepository
}
}
+ /**
+ * If the supplied migration has not already been run, it will be run and if it completes, we
+ * will note in the DepotMigrationHistory table that it has been run.
+ */
+ protected void runMigration (DataMigration migration)
+ throws DatabaseException
+ {
+ // attempt to get a lock to run this migration (or detect that it has already been run)
+ DepotMigrationHistoryRecord record;
+ while (true) {
+ // check to see if the migration has already been completed
+ record = load(DepotMigrationHistoryRecord.class, migration.getIdent());
+ if (record != null && record.whenCompleted != null) {
+ return; // great, no need to do anything
+ }
+
+ // if no record exists at all, try to insert one and thereby obtain the migration lock
+ if (record == null) {
+ try {
+ record = new DepotMigrationHistoryRecord();
+ record.ident = migration.getIdent();
+ insert(record);
+ break; // we got the lock, break out of this loop and run the migration
+ } catch (DuplicateKeyException dke) {
+ // someone beat us to the punch, so we have to wait for them to finish
+ }
+ }
+
+ // we didn't get the lock, so wait 5 seconds and then check to see if the other process
+ // finished the update or failed in which case we'll try to grab the lock ourselves
+ try {
+ log.info("Waiting on migration lock for " + migration.getIdent() + ".");
+ Thread.sleep(5000);
+ } catch (InterruptedException ie) {
+ throw new DatabaseException("Interrupted while waiting on migration lock.");
+ }
+ }
+
+ log.info("Running data migration", "ident", migration.getIdent());
+ try {
+ // run the migration
+ migration.invoke();
+
+ // report to the world that we've done so
+ record.whenCompleted = new Timestamp(System.currentTimeMillis());
+ update(record);
+
+ } finally {
+ // clear out our migration history record if we failed to get the job done
+ if (record.whenCompleted == null) {
+ try {
+ delete(record);
+ } catch (Throwable dt) {
+ log.warning("Oh noez! Failed to delete history record for failed migration. " +
+ "All clients will loop forever waiting for the lock.",
+ "ident", migration.getIdent(), dt);
+ }
+ }
+ }
+ }
+
protected PersistenceContext _ctx;
+ protected List 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.
+ *
+ * Note that if {@link PersistenceContext#initializeRepositories} is used, then all schema
+ * migrations for all known repositories will be run and then all data migrations for all known
+ * repositories will be run. This is recommeneded because schema migrations may fail and it is
+ * generally better to have not yet done the data migration rather than having schema and data
+ * migrations interleaved and potentially leaving the database in a strange state.
*/
public Note: these should only be used for actual schema changes (column additions, removals,
+ * renames, retypes, etc.). It should not be used for data migration, use {@link DataMigration} for
+ * that.
*/
public abstract class SchemaMigration extends Modifier
{