diff --git a/src/java/com/samskivert/jdbc/BaseLiaison.java b/src/java/com/samskivert/jdbc/BaseLiaison.java index e1ce2fd1..c7ed3c9a 100644 --- a/src/java/com/samskivert/jdbc/BaseLiaison.java +++ b/src/java/com/samskivert/jdbc/BaseLiaison.java @@ -84,7 +84,8 @@ public abstract class BaseLiaison implements DatabaseLiaison } // from DatabaseLiaison - public boolean addIndexToTable (Connection conn, String table, String[] columns, String ixName) + public boolean addIndexToTable ( + Connection conn, String table, String[] columns, String ixName, boolean unique) throws SQLException { if (tableContainsIndex(conn, table, ixName)) { @@ -92,8 +93,12 @@ public abstract class BaseLiaison implements DatabaseLiaison } ixName = (ixName != null ? ixName : StringUtil.join(columns, "_")); - StringBuilder update = new StringBuilder("CREATE INDEX ").append(indexSQL(ixName)). - append(" on ").append(tableSQL(table)).append(" ("); + StringBuilder update = new StringBuilder("CREATE "); + if (unique) { + update.append("UNIQUE "); + } + update.append("INDEX ").append(indexSQL(ixName)).append(" ON "). + append(tableSQL(table)).append(" ("); for (int ii = 0; ii < columns.length; ii ++) { if (ii > 0) { update.append(", "); @@ -113,6 +118,49 @@ public abstract class BaseLiaison implements DatabaseLiaison return true; } + // from DatabaseLiaison + public void addPrimaryKey (Connection conn, String table, String[] columns) + throws SQLException + { + StringBuilder fields = new StringBuilder("("); + for (int ii = 0; ii < columns.length; ii ++) { + if (ii > 0) { + fields.append(", "); + } + fields.append(columnSQL(columns[ii])); + } + fields.append(")"); + String update = "ALTER TABLE " + tableSQL(table) + " ADD PRIMARY KEY " + fields.toString(); + + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate(update.toString()); + } finally { + JDBCUtil.close(stmt); + } + + Log.info("Primary key " + fields + " added to table '" + table + "'"); + } + + // from DatabaseLiaison + public void dropIndex (Connection conn, String table, String index) throws SQLException + { + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate( + "ALTER TABLE " + tableSQL(table) + " DROP CONSTRAINT " + columnSQL(index)); + } finally { + JDBCUtil.close(stmt); + } + } + + // from DatabaseLiaison + public void dropPrimaryKey (Connection conn, String table, String pkName) + throws SQLException + { + dropIndex(conn, table, pkName); + } + // from DatabaseLiaison public boolean addColumn ( Connection conn, String table, String column, String definition, boolean check) diff --git a/src/java/com/samskivert/jdbc/DatabaseLiaison.java b/src/java/com/samskivert/jdbc/DatabaseLiaison.java index 3f3048e6..2c4e84cb 100644 --- a/src/java/com/samskivert/jdbc/DatabaseLiaison.java +++ b/src/java/com/samskivert/jdbc/DatabaseLiaison.java @@ -84,7 +84,27 @@ public interface DatabaseLiaison * Adds a named index to a table on the given columns. Returns true or false if the database * did or did not report a schema modification. */ - public boolean addIndexToTable (Connection conn, String table, String[] columns, String index) + public boolean addIndexToTable ( + Connection conn, String table, String[] columns, String index, boolean unique) + throws SQLException; + + /** + * Drops the named index from the given table. + */ + public void dropIndex (Connection conn, String table, String index) + throws SQLException; + + /** + * Adds a primary key to a table of the given name and on the given columns. Returns true or + * false if the database did nor did not report a schema modification. + */ + public void addPrimaryKey (Connection conn, String table, String[] columns) + throws SQLException; + + /** + * Deletes the primary key from a table, if it exists. + */ + public void dropPrimaryKey (Connection conn, String table, String pkName) throws SQLException; /** diff --git a/src/java/com/samskivert/jdbc/MySQLLiaison.java b/src/java/com/samskivert/jdbc/MySQLLiaison.java index 210f8a97..e8bada1d 100644 --- a/src/java/com/samskivert/jdbc/MySQLLiaison.java +++ b/src/java/com/samskivert/jdbc/MySQLLiaison.java @@ -23,7 +23,6 @@ package com.samskivert.jdbc; import java.sql.*; import com.samskivert.Log; -import com.samskivert.util.StringUtil; /** * A database liaison for the MySQL database. @@ -39,15 +38,15 @@ public class MySQLLiaison extends BaseLiaison // from DatabaseLiaison public boolean isDuplicateRowException (SQLException sqe) { - String msg = sqe.getMessage(); - return (msg != null && msg.indexOf("Duplicate entry") != -1); + String msg = sqe.getMessage(); + return (msg != null && msg.indexOf("Duplicate entry") != -1); } // from DatabaseLiaison public boolean isTransientException (SQLException sqe) { - String msg = sqe.getMessage(); - return (msg != null && (msg.indexOf("Lost connection") != -1 || + String msg = sqe.getMessage(); + return (msg != null && (msg.indexOf("Lost connection") != -1 || msg.indexOf("link failure") != -1 || msg.indexOf("Broken pipe") != -1)); } @@ -77,18 +76,21 @@ public class MySQLLiaison extends BaseLiaison } @Override // from BaseLiaison - public boolean addIndexToTable (Connection conn, String table, String[] columns, String ixName) + public boolean addIndexToTable ( + Connection conn, String table, String[] columns, String ixName, boolean unique) throws SQLException { if (tableContainsIndex(conn, table, ixName)) { return false; } - ixName = (ixName != null ? ixName : StringUtil.join(columns, "_")); // MySQL's "CREATE INDEX" is buggy, it actually changes the case of the table names you're // working with. Luckily (?) ALTER TABLE ADD INDEX works, so we do that here. - StringBuilder update = new StringBuilder("ALTER TABLE ").append(table). - append(" ADD INDEX ").append(indexSQL(ixName)).append(" ("); + StringBuilder update = new StringBuilder("ALTER TABLE ").append(table).append(" ADD "); + if (unique) { + update.append("UNIQUE "); + } + update.append("INDEX ").append(indexSQL(ixName)).append(" ("); for (int ii = 0; ii < columns.length; ii ++) { if (ii > 0) { update.append(", "); @@ -109,6 +111,31 @@ public class MySQLLiaison extends BaseLiaison return true; } + @Override // from BaseLiaison + public void dropIndex (Connection conn, String table, String index) + throws SQLException + { + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate( + "ALTER TABLE " + tableSQL(table) + " DROP INDEX " + columnSQL(index)); + } finally { + JDBCUtil.close(stmt); + } + } + + @Override // from BaseLiaison + public void dropPrimaryKey (Connection conn, String table, String pkName) + throws SQLException + { + Statement stmt = conn.createStatement(); + try { + stmt.executeUpdate("ALTER TABLE " + tableSQL(table) + " DROP PRIMARY KEY"); + } finally { + JDBCUtil.close(stmt); + } + } + // from DatabaseLiaison public String columnSQL (String column) { diff --git a/src/java/com/samskivert/jdbc/PostgreSQLLiaison.java b/src/java/com/samskivert/jdbc/PostgreSQLLiaison.java index 329adfa5..b6de347c 100644 --- a/src/java/com/samskivert/jdbc/PostgreSQLLiaison.java +++ b/src/java/com/samskivert/jdbc/PostgreSQLLiaison.java @@ -36,7 +36,7 @@ public class PostgreSQLLiaison extends BaseLiaison // from DatabaseLiaison public boolean isDuplicateRowException (SQLException sqe) { - String msg = sqe.getMessage(); + String msg = sqe.getMessage(); return (msg != null && msg.indexOf("duplicate key") != -1); } @@ -44,8 +44,8 @@ public class PostgreSQLLiaison extends BaseLiaison public boolean isTransientException (SQLException sqe) { // TODO: Add more error messages here as we encounter them. - String msg = sqe.getMessage(); - return (msg != null && + String msg = sqe.getMessage(); + return (msg != null && msg.indexOf("An I/O error occured while sending to the backend") != -1); } diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 74cc70d9..fbe858d3 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -31,6 +31,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -551,60 +552,61 @@ public class DepotMarshaller } }); - // now create the table for our persistent class if it does not exist - final String[] fDeclarations = declarations; - final String[][] fUniqueConstraintColumns = uniqueConstraintColumns; - ctx.invoke(new Modifier() { - public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - String[] columns = new String[_columnFields.length]; - for (int ii = 0; ii < columns.length; ii ++) { - columns[ii] = _fields.get(_columnFields[ii]).getColumnName(); - } - String[] primaryKeyColumns = null; - if (_pkColumns != null) { - primaryKeyColumns = new String[_pkColumns.size()]; - for (int ii = 0; ii < primaryKeyColumns.length; ii ++) { - primaryKeyColumns[ii] = _pkColumns.get(ii).getColumnName(); - } - } - if (liaison.createTableIfMissing(conn, getTableName(), columns, fDeclarations, - fUniqueConstraintColumns, primaryKeyColumns)) { - for (FullTextIndex fti : _fullTextIndexes.values()) { - builder.addFullTextSearch(conn, DepotMarshaller.this, fti); - } - } - - updateVersion(conn, liaison, _schemaVersion); - return 0; + // fetch all relevant information regarding our table from the database + final TableMetaData metaData = ctx.invoke(new Query.TrivialQuery() { + public TableMetaData invoke (Connection conn, DatabaseLiaison dl) throws SQLException { + return new TableMetaData(conn.getMetaData()); } }); - // ensure that all indices are created - final Entity entity = _pclass.getAnnotation(Entity.class); - if (entity != null && entity.indices().length > 0) { - ctx.invoke(new Modifier() { - public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - for (Index idx : entity.indices()) { - liaison.addIndexToTable(conn, getTableName(), idx.columns(), idx.name()); - } - return entity.indices().length; - } - }); - } + // if the table does not exist, create it + if (!metaData.tableExists) { + final Entity entity = _pclass.getAnnotation(Entity.class); + final String[] fDeclarations = declarations; + final String[][] fUniqueConstraintColumns = uniqueConstraintColumns; - // if we have a key generator, initialize that too - if (_keyGenerator != null) { ctx.invoke(new Modifier() { public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { - _keyGenerator.init(conn, liaison); + // create the table + String[] columns = new String[_columnFields.length]; + for (int ii = 0; ii < columns.length; ii ++) { + columns[ii] = _fields.get(_columnFields[ii]).getColumnName(); + } + String[] primaryKeyColumns = null; + if (_pkColumns != null) { + primaryKeyColumns = new String[_pkColumns.size()]; + for (int ii = 0; ii < primaryKeyColumns.length; ii ++) { + primaryKeyColumns[ii] = _pkColumns.get(ii).getColumnName(); + } + } + liaison.createTableIfMissing(conn, getTableName(), columns, fDeclarations, + fUniqueConstraintColumns, primaryKeyColumns); + + // add its indexen + for (Index idx : entity.indices()) { + liaison.addIndexToTable( + conn, getTableName(), idx.columns(), idx.name(), idx.unique()); + } + if (_keyGenerator != null) { + _keyGenerator.init(conn, liaison); + } + for (FullTextIndex fti : _fullTextIndexes.values()) { + builder.addFullTextSearch(conn, DepotMarshaller.this, fti); + } + + updateVersion(conn, liaison, _schemaVersion); return 0; } }); + + // and we're done + return; } - // if schema versioning is disabled, stop now + // if the table exists, see if should attempt automatic schema migration if (_schemaVersion < 0) { - verifySchemasMatch(ctx); + // nope, versioning disabled + verifySchemasMatch(metaData, ctx); return; } @@ -625,20 +627,15 @@ public class DepotMarshaller } } }); - if (currentVersion == _schemaVersion) { - verifySchemasMatch(ctx); - return; - } - if (true) { - log.warning("Automatic schema migration is temporarily disabled."); - verifySchemasMatch(ctx); + if (currentVersion == _schemaVersion) { + verifySchemasMatch(metaData, ctx); return; } // otherwise try to migrate the schema - log.info("Migrating " + getTableName() + " from " + - currentVersion + " to " + _schemaVersion + "..."); + log.info("Migrating " + getTableName() + " from " + currentVersion + " to " + + _schemaVersion + "..."); // run our pre-default-migrations for (EntityMigration migration : _migrations) { @@ -649,9 +646,6 @@ public class DepotMarshaller } } - // enumerate all columns and indexes now that we've run our pre-migrations - TableMetaData metaData = loadMetaData(ctx); - // this is a little silly, but we need a copy for name disambiguation later Set indicesCopy = new HashSet(metaData.indexColumns.keySet()); @@ -691,70 +685,77 @@ public class DepotMarshaller } } - // TODO: This needs more work to be database-independent, I think. Assuming the index name - // is going to be PRIMARY seems like a MySQL-ism, and we'll do the rest of the index work - // when we do that. + // add or remove the primary key as needed + if (hasPrimaryKey() && metaData.pkName == null) { + log.info("Adding primary key."); + ctx.invoke(new Modifier() { + public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + liaison.addPrimaryKey(conn, getTableName(), getPrimaryKeyFields()); + return 0; + } + }); -// // add or remove the primary key as needed -// if (hasPrimaryKey() && !indexColumns.containsKey("PRIMARY")) { -// String pkdef = "primary key (" + getJoinedKeyColumns() + ")"; -// log.info("Adding primary key to " + getTableName() + ": " + pkdef); -// ctx.invoke(new Modifier.Simple("alter table " + getTableName() + " add " + pkdef)); -// -// } else if (!hasPrimaryKey() && indexColumns.containsKey("PRIMARY")) { -// indexColumns.remove("PRIMARY"); -// log.info("Dropping primary from " + getTableName()); -// ctx.invoke(new Modifier.Simple("alter table " + getTableName() + " drop primary key")); -// } -// -// // add any missing indices -// for (Index index : (entity == null ? new Index[0] : entity.indices())) { -// if (indexColumns.containsKey(index.name())) { -// indexColumns.remove(index.name()); -// continue; -// } -// String indexdef = -// "create " + index.type() + " index " + _liaison.indexSQL(index.name()) + -// " on " + _liaison.tableSQL(getTableName()) + " (" + StringUtil.join(index.columns(), ", ") + ")"; -// log.info("Adding index: " + indexdef); -// ctx.invoke(new Modifier.Simple(indexdef)); -// } -// -// // to get the @Table(uniqueIndices...) indices, we use our clever set of column name sets -// Set> uniqueIndices = new HashSet>(indexColumns.values()); -// Table table; -// if (getTableName() != null && (table = _pclass.getAnnotation(Table.class)) != null) { -// Set colSet = new HashSet(); -// for (UniqueConstraint constraint : table.uniqueConstraints()) { -// // for each given UniqueConstraint, build a new column set -// colSet.clear(); -// for (String column : constraint.columnNames()) { -// colSet.add(column); -// } -// // and check if the table contained this set -// if (uniqueIndices.contains(colSet)) { -// continue; // good, carry on -// } -// -// // else build the index; we'll use mysql's convention of naming it after a column, -// // with possible _N disambiguation; luckily we made a copy of the index names! -// String indexName = colSet.iterator().next(); -// if (indicesCopy.contains(indexName)) { -// int num = 1; -// indexName += "_"; -// while (indicesCopy.contains(indexName + num)) { -// num ++; -// } -// indexName += num; -// } -// -// String[] columnArr = colSet.toArray(new String[colSet.size()]); -// String indexdef = "create unique index " + indexName + " on " + -// getTableName() + " (" + StringUtil.join(columnArr, ", ") + ")"; -// log.info("Adding unique index: " + indexdef); -// ctx.invoke(new Modifier.Simple(indexdef)); -// } -// } + } else if (!hasPrimaryKey() && metaData.pkName != null) { + log.info("Dropping primary key."); + ctx.invoke(new Modifier() { + public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + liaison.dropPrimaryKey(conn, getTableName(), metaData.pkName); + return 0; + } + }); + } + + Entity entity = _pclass.getAnnotation(Entity.class); + // add any missing indices + for (final Index index : (entity == null ? new Index[0] : entity.indices())) { + if (metaData.indexColumns.containsKey(index.name())) { + metaData.indexColumns.remove(index.name()); + continue; + } + ctx.invoke(new Modifier() { + public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException { + liaison.addIndexToTable( + conn, getTableName(), index.columns(), index.name(), index.unique()); + return 0; + } + }); + } + + // to get the @Table(uniqueIndices...) indices, we use our clever set of column name sets + Set> uniqueIndices = new HashSet>(metaData.indexColumns.values()); + + if (getTableName() != null && table != null) { + for (UniqueConstraint constraint : table.uniqueConstraints()) { + // for each given UniqueConstraint, build a new column set + Set colSet = new HashSet(Arrays.asList(constraint.fieldNames())); + + // and check if the table contained this set + if (uniqueIndices.contains(colSet)) { + continue; // good, carry on + } + + // else build the index; we'll use mysql's convention of naming it after a column, + // with possible _N disambiguation; luckily we made a copy of the index names! + String indexName = colSet.iterator().next(); + if (indicesCopy.contains(indexName)) { + int num = 1; + indexName += "_"; + while (indicesCopy.contains(indexName + num)) { + num ++; + } + indexName += num; + } + + final String[] colArr = colSet.toArray(new String[colSet.size()]); + final String fName = indexName; + ctx.invoke(new Modifier() { + public int invoke (Connection conn, DatabaseLiaison dl) throws SQLException { + dl.addIndexToTable(conn, getTableName(), colArr, fName, true); + return 0; + } + }); + } + } // we do not auto-remove columns but rather require that EntityMigration.Drop records be // registered by hand to avoid accidentally causing the loss of data @@ -781,19 +782,9 @@ public class DepotMarshaller }); } - protected TableMetaData loadMetaData (PersistenceContext ctx) - throws PersistenceException - { - return ctx.invoke(new Query.TrivialQuery() { - public TableMetaData invoke (Connection conn, DatabaseLiaison liaison) - throws SQLException { - return new TableMetaData(conn.getMetaData()); - } - }); - } - protected class TableMetaData { + public boolean tableExists; public Set tableColumns = new HashSet(); public Map> indexColumns = new HashMap>(); public String pkName; @@ -802,6 +793,11 @@ public class DepotMarshaller public TableMetaData (DatabaseMetaData meta) throws SQLException { + tableExists = meta.getTables("", "", getTableName(), null).next(); + if (!tableExists) { + return; + } + ResultSet rs = meta.getColumns(null, null, getTableName(), "%"); while (rs.next()) { tableColumns.add(rs.getString("COLUMN_NAME")); @@ -838,10 +834,9 @@ public class DepotMarshaller /** * Checks that there are no database columns for which we no longer have Java fields. */ - protected void verifySchemasMatch (PersistenceContext ctx) + protected void verifySchemasMatch (TableMetaData meta, PersistenceContext ctx) throws PersistenceException { - TableMetaData meta = loadMetaData(ctx); for (String fname : _columnFields) { FieldMarshaller fmarsh = _fields.get(fname); meta.tableColumns.remove(fmarsh.getColumnName()); diff --git a/src/java/com/samskivert/jdbc/depot/FindOneQuery.java b/src/java/com/samskivert/jdbc/depot/FindOneQuery.java index e83fa6cb..90d64850 100644 --- a/src/java/com/samskivert/jdbc/depot/FindOneQuery.java +++ b/src/java/com/samskivert/jdbc/depot/FindOneQuery.java @@ -3,7 +3,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2006-2007 Michael Bayne, 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 diff --git a/src/java/com/samskivert/jdbc/depot/Query.java b/src/java/com/samskivert/jdbc/depot/Query.java index e54b8a01..4a912d99 100644 --- a/src/java/com/samskivert/jdbc/depot/Query.java +++ b/src/java/com/samskivert/jdbc/depot/Query.java @@ -3,7 +3,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2006-2007 Michael Bayne, 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 @@ -46,13 +46,13 @@ public interface Query public void updateCache (PersistenceContext ctx, T result) { } } - + /** * Any query may elect to utilize the built-in cache by returning a non-null {@link CacheKey} * in this method. This is done automatically by the {@link DepotRepository} when looking up * single entities by primary key, but even entire collections can be cached under a single * key. - * + * * Great care must be taken to invalidate such cached collections when their constituent * entities are invalidated. This is generally done using {@link CacheListener} and * {@link CacheInvalidator}. @@ -60,7 +60,7 @@ public interface Query public CacheKey getCacheKey (); /** - * Performs the actual JDBC operations associated with this query. + * Performs the actual JDBC operations associated with this query. */ public T invoke (Connection conn, DatabaseLiaison liaison) throws SQLException; diff --git a/src/java/com/samskivert/jdbc/depot/annotation/Index.java b/src/java/com/samskivert/jdbc/depot/annotation/Index.java index 304a3a99..9a7b5203 100644 --- a/src/java/com/samskivert/jdbc/depot/annotation/Index.java +++ b/src/java/com/samskivert/jdbc/depot/annotation/Index.java @@ -3,7 +3,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2006-2007 Michael Bayne, 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 @@ -34,9 +34,8 @@ public @interface Index /** Defines the name of the index. */ String name (); - /** Configures the type of this index if necessary. MySQL supports FULLTEXT, SPATIAL and - * UNIQUE. */ - String type () default ""; + /** Does this index enforce a uniqueness constraint? */ + boolean unique () default false; /** Defines the columns on which the index operates. */ String[] columns () default {};