diff --git a/src/java/com/samskivert/depot/PersistenceContext.java b/src/java/com/samskivert/depot/PersistenceContext.java index 68c6cca..8ec6fce 100644 --- a/src/java/com/samskivert/depot/PersistenceContext.java +++ b/src/java/com/samskivert/depot/PersistenceContext.java @@ -46,6 +46,7 @@ import com.samskivert.depot.CacheAdapter.CacheCategory; import com.samskivert.depot.CacheAdapter.CachedValue; import com.samskivert.depot.annotation.TableGenerator; import com.samskivert.depot.impl.DepotMarshaller; +import com.samskivert.depot.impl.DepotMetaData; import com.samskivert.depot.impl.DepotTypes; import com.samskivert.depot.impl.HSQLBuilder; import com.samskivert.depot.impl.Modifier; @@ -173,18 +174,8 @@ public class PersistenceContext _liaison = LiaisonRegistry.getLiaison(conprov.getURL(ident)); _cache = adapter; - // determine our JDBC major version - Connection conn = null; - try { - conn = _conprov.getConnection(_ident, true); - _jdbcMajorVersion = conn.getMetaData().getJDBCMajorVersion(); - } catch (Exception e) { - throw new DatabaseException("getJDBCMajorVersion failed [ident=" + ident + "].", e); - } finally { - if (conn != null) { - _conprov.releaseConnection(_ident, true, conn); - } - } + // set up some basic meta-meta-data + _meta.init(this); } /** @@ -212,26 +203,11 @@ public class PersistenceContext } /** - * Create and return a new {@link SQLBuilder} for the appropriate dialect. - * - * TODO: At some point perhaps use a more elegant way of discerning our dialect. + * Creates and return a new {@link SQLBuilder} for the appropriate dialect. */ public SQLBuilder getSQLBuilder (DepotTypes types) { - if (_liaison instanceof PostgreSQLLiaison) { - if (_jdbcMajorVersion >= 4) { - return new PostgreSQL4Builder(types); - } else { - return new PostgreSQLBuilder(types); - } - } - if (_liaison instanceof MySQLLiaison) { - return new MySQLBuilder(types); - } - if (_liaison instanceof HsqldbLiaison) { - return new HSQLBuilder(types); - } - throw new IllegalArgumentException("Unknown liaison type: " + _liaison.getClass()); + return _meta.getSQLBuilder(types, _liaison); } /** @@ -284,7 +260,7 @@ public class PersistenceContext if (!marshaller.isInitialized()) { // initialize the marshaller which may create or migrate the table for its // underlying persistent object - marshaller.init(this); + marshaller.init(this, _meta); if (marshaller.getTableName() != null && _warnOnLazyInit) { log.warning("Record initialized lazily", "type", type.getName(), new Exception()); @@ -621,8 +597,8 @@ public class PersistenceContext protected String _ident; protected ConnectionProvider _conprov; protected DatabaseLiaison _liaison; + protected DepotMetaData _meta = new DepotMetaData(); protected boolean _warnOnLazyInit; - protected int _jdbcMajorVersion; /** Used to track various statistics. */ protected Stats _stats = new Stats(); diff --git a/src/java/com/samskivert/depot/impl/DepotMarshaller.java b/src/java/com/samskivert/depot/impl/DepotMarshaller.java index 5fb1452..110be11 100644 --- a/src/java/com/samskivert/depot/impl/DepotMarshaller.java +++ b/src/java/com/samskivert/depot/impl/DepotMarshaller.java @@ -469,7 +469,7 @@ public class DepotMarshaller */ public boolean isInitialized () { - return _initialized; + return _meta != null; } /** @@ -478,14 +478,14 @@ public class DepotMarshaller * be created. If the schema version specified by the persistent object is newer than the * database schema, it will be migrated. */ - public void init (PersistenceContext ctx) + public void init (PersistenceContext ctx, DepotMetaData meta) throws DatabaseException { - if (_initialized) { // sanity check + if (_meta != null) { // sanity check throw new IllegalStateException( "Cannot re-initialize marshaller [type=" + _pClass + "]."); } - _initialized = true; + _meta = meta; final SQLBuilder builder = ctx.getSQLBuilder(new DepotTypes(ctx, _pClass)); @@ -517,67 +517,26 @@ public class DepotMarshaller _columnFields = ArrayUtil.splice(_columnFields, jj); declarations = ArrayUtil.splice(declarations, jj); - // check to see if our schema version table exists, create it if not - ctx.invoke(new Modifier() { - @Override - protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - liaison.createTableIfMissing( - conn, SCHEMA_VERSION_TABLE, - new String[] { P_COLUMN, V_COLUMN, MV_COLUMN }, - new ColumnDefinition[] { - new ColumnDefinition("VARCHAR(255)", false, true, null), - new ColumnDefinition("INTEGER", false, false, null), - new ColumnDefinition("INTEGER", false, false, null) - }, - null, - new String[] { P_COLUMN }); - // add our new "migratingVersion" column if it's not already there - liaison.addColumn(conn, SCHEMA_VERSION_TABLE, MV_COLUMN, - "integer not null default 0", true); - return 0; - } - }); - - // fetch all relevant information regarding our table from the database - TableMetaData metaData = TableMetaData.load(ctx, getTableName()); - // determine whether or not this record has ever been seen - int currentVersion = ctx.invoke(new ReadVersion()); + int currentVersion = _meta.getVersion(getTableName(), false); if (currentVersion == -1) { log.info("Creating initial version record for " + _pClass.getName() + "."); // if not, create a version entry with version zero - ctx.invoke(new SimpleModifier() { - @Override - protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { - try { - return stmt.executeUpdate( - "insert into " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + - " values('" + getTableName() + "', 0 , 0)"); - } catch (SQLException e) { - // someone else might be doing this at the exact same time which is OK, - // we'll coordinate with that other process in the next phase - if (liaison.isDuplicateRowException(e)) { - return 0; - } else { - throw e; - } - } - } - }); + _meta.initializeVersion(getTableName()); } // now check whether we need to migrate our database schema - boolean gotMigrationLock = false; - while (!gotMigrationLock) { - currentVersion = ctx.invoke(new ReadVersion()); + while (true) { if (currentVersion >= _schemaVersion) { - checkForStaleness(metaData, ctx, builder); + // TODO: we used to check for staleness here, but that's slow; currently we do it + // only after migration, we should reinstate a check maybe the first time a table + // is read or something so that developers are more likely to get the warning return; } // try to update migratingVersion to the new version to indicate to other processes // that we are handling the migration and that they should wait - if (ctx.invoke(new UpdateMigratingVersion(_schemaVersion, 0)) > 0) { + if (_meta.updateMigratingVersion(getTableName(), _schemaVersion, 0)) { break; // we got the lock, let's go } @@ -589,8 +548,13 @@ public class DepotMarshaller } catch (InterruptedException ie) { throw new DatabaseException("Interrupted while waiting on migration lock."); } + + currentVersion = _meta.getVersion(getTableName(), true); } + // fetch all relevant information regarding our table from the database + TableMetaData metaData = TableMetaData.load(ctx, getTableName()); + try { if (!metaData.tableExists) { // if the table does not exist, create it @@ -605,20 +569,12 @@ public class DepotMarshaller checkForStaleness(metaData, ctx, builder); // and update our version in the schema version table - ctx.invoke(new SimpleModifier() { - @Override - protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { - return stmt.executeUpdate( - "update " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + - " set " + liaison.columnSQL(V_COLUMN) + " = " + _schemaVersion + - " where " + liaison.columnSQL(P_COLUMN) + " = '" + getTableName() + "'"); - } - }); + _meta.updateVersion(getTableName(), _schemaVersion); } finally { // set our migrating version back to zero try { - if (ctx.invoke(new UpdateMigratingVersion(0, _schemaVersion)) == 0) { + if (!_meta.updateMigratingVersion(getTableName(), 0, _schemaVersion)) { log.warning("Failed to restore migrating version to zero!", "record", _pClass); } } catch (Exception e) { @@ -1020,50 +976,6 @@ public class DepotMarshaller } } - protected abstract class SimpleModifier extends Modifier { - @Override - protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - Statement stmt = conn.createStatement(); - try { - return invoke(liaison, stmt); - } finally { - stmt.close(); - } - } - - protected abstract int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException; - } - - // this is a Modifier not a Query because we want to be sure we're talking to the database - // server to whom we would talk if we were doing a modification (ie. the master, not a - // read-only slave) - protected class ReadVersion extends SimpleModifier { - @Override - protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { - ResultSet rs = stmt.executeQuery( - " select " + liaison.columnSQL(V_COLUMN) + - " from " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + - " where " + liaison.columnSQL(P_COLUMN) + " = '" + getTableName() + "'"); - return (rs.next()) ? rs.getInt(1) : -1; - } - } - - protected class UpdateMigratingVersion extends SimpleModifier { - public UpdateMigratingVersion (int newMigratingVersion, int guardVersion) { - _newMigratingVersion = newMigratingVersion; - _guardVersion = guardVersion; - } - @Override - protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { - return stmt.executeUpdate( - "update " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + - " set " + liaison.columnSQL(MV_COLUMN) + " = " + _newMigratingVersion + - " where " + liaison.columnSQL(P_COLUMN) + " = '" + getTableName() + "'" + - " and " + liaison.columnSQL(MV_COLUMN) + " = " + _guardVersion); - } - protected int _newMigratingVersion, _guardVersion; - } - protected static class TableMetaData { public boolean tableExists; @@ -1133,6 +1045,9 @@ public class DepotMarshaller } } + /** Provides access to certain internal metadata. */ + protected DepotMetaData _meta; + /** The persistent object class that we manage. */ protected Class _pClass; @@ -1167,21 +1082,6 @@ public class DepotMarshaller /** The version of our persistent object schema as specified in the class definition. */ protected int _schemaVersion = -1; - /** Indicates that we have been initialized (created or migrated our tables). */ - protected boolean _initialized; - /** A list of hand registered schema migrations to run prior to doing the default migration. */ protected List _schemaMigs = Lists.newArrayList(); - - /** The name of the table we use to track schema versions. */ - protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion"; - - /** The name of the 'persistentClass' column in the {@link #SCHEMA_VERSION_TABLE}. */ - protected static final String P_COLUMN = "persistentClass"; - - /** The name of the 'version' column in the {@link #SCHEMA_VERSION_TABLE}. */ - protected static final String V_COLUMN = "version"; - - /** The name of the 'migratingVersion' column in the {@link #SCHEMA_VERSION_TABLE}. */ - protected static final String MV_COLUMN = "migratingVersion"; } diff --git a/src/java/com/samskivert/depot/impl/DepotMetaData.java b/src/java/com/samskivert/depot/impl/DepotMetaData.java new file mode 100644 index 0000000..0f60ef2 --- /dev/null +++ b/src/java/com/samskivert/depot/impl/DepotMetaData.java @@ -0,0 +1,222 @@ +// +// $Id$ +// +// Depot library - a Java relational persistence library +// Copyright (C) 2006-2009 Michael Bayne and 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 +// (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.depot.impl; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; + +import com.google.common.collect.Maps; + +import com.samskivert.jdbc.ColumnDefinition; +import com.samskivert.jdbc.DatabaseLiaison; +import com.samskivert.jdbc.HsqldbLiaison; +import com.samskivert.jdbc.MySQLLiaison; +import com.samskivert.jdbc.PostgreSQLLiaison; + +import com.samskivert.depot.PersistenceContext; + +/** + * Does something extraordinary. + */ +public class DepotMetaData +{ + public void init (PersistenceContext ctx) + { + _ctx = ctx; + _ctx.invoke(new SimpleModifier() { + @Override + protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { + // determine our JDBC major version + _jdbcMajorVersion = stmt.getConnection().getMetaData().getJDBCMajorVersion(); + + // create our schema version table if needed + liaison.createTableIfMissing( + stmt.getConnection(), SCHEMA_VERSION_TABLE, + new String[] { P_COLUMN, V_COLUMN, MV_COLUMN }, + new ColumnDefinition[] { + new ColumnDefinition("VARCHAR(255)", false, true, null), + new ColumnDefinition("INTEGER", false, false, null), + new ColumnDefinition("INTEGER", false, false, null) + }, + null, + new String[] { P_COLUMN }); + + // slurp in the current versions of all records + readVersions(liaison, stmt); + return 0; + } + }); + } + + /** + * Returns the current version of the specified persistent class. + * + * @param forceUpdate if true the latest version will be read from the database rather than + * returned from memory. Only force an update when you know that the value may have changed in + * the database since Depot was initialized. + * + * @return the current version of the specified persistent class or -1 if the class has no + * recorded version. + */ + public int getVersion (String pClass, boolean forceUpdate) + { + if (forceUpdate) { + _ctx.invoke(new SimpleModifier() { + @Override + protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { + readVersions(liaison, stmt); + return 0; + } + }); + } + Integer curvers = _curvers.get(pClass); + return (curvers == null) ? -1 : curvers; + } + + /** + * Initializes the version of the specified persistent class to zero. + */ + public void initializeVersion (final String pClass) + { + _ctx.invoke(new SimpleModifier() { + @Override + protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { + try { + return stmt.executeUpdate( + "insert into " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + + " values('" + pClass + "', 0 , 0)"); + } catch (SQLException e) { + // someone else might be doing this at the exact same time which is OK, + // we'll coordinate with that other process in the next phase + if (liaison.isDuplicateRowException(e)) { + return 0; + } else { + throw e; + } + } + } + }); + } + + /** + * Updates the version of the specified persistent class to the specified version. + */ + public void updateVersion (final String pClass, final int newVersion) + { + _ctx.invoke(new SimpleModifier() { + @Override + protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { + return stmt.executeUpdate( + "update " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + + " set " + liaison.columnSQL(V_COLUMN) + " = " + newVersion + + " where " + liaison.columnSQL(P_COLUMN) + " = '" + pClass + "'"); + } + }); + } + + /** + * Updates the current migrating version of the supplied persistent class. + * + * @return true if the migration lock was obtained and the migrating version was updated, false + * if some other process acquired the lock. + */ + public boolean updateMigratingVersion ( + final String pClass, final int newMigratingVersion, final int guardVersion) + { + return _ctx.invoke(new SimpleModifier() { + @Override + protected int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException { + return stmt.executeUpdate( + "update " + liaison.tableSQL(SCHEMA_VERSION_TABLE) + + " set " + liaison.columnSQL(MV_COLUMN) + " = " + newMigratingVersion + + " where " + liaison.columnSQL(P_COLUMN) + " = '" + pClass + "'" + + " and " + liaison.columnSQL(MV_COLUMN) + " = " + guardVersion); + } + }) > 0; + } + + /** + * Creates and return a new {@link SQLBuilder} for the appropriate dialect. + * + * TODO: At some point perhaps use a more elegant way of discerning our dialect. + */ + public SQLBuilder getSQLBuilder (DepotTypes types, DatabaseLiaison liaison) + { + if (liaison instanceof PostgreSQLLiaison) { + if (_jdbcMajorVersion >= 4) { + return new PostgreSQL4Builder(types); + } else { + return new PostgreSQLBuilder(types); + } + } + if (liaison instanceof MySQLLiaison) { + return new MySQLBuilder(types); + } + if (liaison instanceof HsqldbLiaison) { + return new HSQLBuilder(types); + } + throw new IllegalArgumentException("Unknown liaison type: " + liaison.getClass()); + } + + protected void readVersions (DatabaseLiaison liaison, Statement stmt) + throws SQLException + { + ResultSet rs = stmt.executeQuery( + " select " + liaison.columnSQL(P_COLUMN) + ", " + liaison.columnSQL(V_COLUMN) + + " from " + liaison.tableSQL(SCHEMA_VERSION_TABLE)); + while (rs.next()) { + _curvers.put(rs.getString(1), rs.getInt(2)); + } + } + + protected abstract class SimpleModifier extends Modifier { + @Override + protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + Statement stmt = conn.createStatement(); + try { + return invoke(liaison, stmt); + } finally { + stmt.close(); + } + } + + protected abstract int invoke (DatabaseLiaison liaison, Statement stmt) throws SQLException; + } + + protected PersistenceContext _ctx; + protected int _jdbcMajorVersion; + protected Map _curvers = Maps.newHashMap(); + + /** The name of the table we use to track schema versions. */ + protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion"; + + /** The name of the 'persistentClass' column in the {@link #SCHEMA_VERSION_TABLE}. */ + protected static final String P_COLUMN = "persistentClass"; + + /** The name of the 'version' column in the {@link #SCHEMA_VERSION_TABLE}. */ + protected static final String V_COLUMN = "version"; + + /** The name of the 'migratingVersion' column in the {@link #SCHEMA_VERSION_TABLE}. */ + protected static final String MV_COLUMN = "migratingVersion"; +}