Files
depot/src/main/java/com/samskivert/depot/impl/DepotMarshaller.java
T

1144 lines
47 KiB
Java

//
// Depot library - a Java relational persistence library
// http://code.google.com/p/depot/source/browse/trunk/LICENSE
package com.samskivert.depot.impl;
import static com.google.common.base.Preconditions.checkArgument;
import static com.samskivert.depot.Log.log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.samskivert.depot.DatabaseException;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.SchemaMigration;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Computed;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.FullTextIndex;
import com.samskivert.depot.annotation.GeneratedValue;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.annotation.Index;
import com.samskivert.depot.annotation.TableGenerator;
import com.samskivert.depot.annotation.Transient;
import com.samskivert.depot.annotation.UniqueConstraint;
import com.samskivert.depot.clause.OrderBy.Order;
import com.samskivert.depot.clause.QueryClause;
import com.samskivert.depot.expression.ColumnExp;
import com.samskivert.depot.expression.SQLExpression;
import com.samskivert.depot.impl.clause.CreateIndexClause;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import com.samskivert.jdbc.ColumnDefinition;
import com.samskivert.jdbc.DatabaseLiaison;
/**
* Handles the marshalling and unmarshalling of persistent instances to JDBC primitives ({@link
* PreparedStatement} and {@link ResultSet}).
*/
public class DepotMarshaller<T extends PersistentRecord> implements QueryMarshaller<T,T>
{
/** The name of a private static field that must be defined for all persistent object classes.
* It is used to handle schema migration. If automatic schema migration is not desired, define
* this field and set its value to -1. */
public static final String SCHEMA_VERSION_FIELD = "SCHEMA_VERSION";
/**
* Creates a marshaller for the specified persistent object class.
*/
public DepotMarshaller (Class<T> pClass, PersistenceContext context)
{
_pClass = pClass;
checkArgument(!java.lang.reflect.Modifier.isAbstract(pClass.getModifiers()),
"Can't handle reference to abstract record: " + pClass.getName());
Entity entity = pClass.getAnnotation(Entity.class);
// see if this is a computed entity
_computed = pClass.getAnnotation(Computed.class);
if (_computed == null) {
// if not, this class has a corresponding SQL table
_tableName = DepotUtil.justClassName(_pClass);
// see if there are Entity values specified
if (entity != null) {
if (entity.name().length() > 0) {
_tableName = entity.name();
}
}
}
// if the entity defines a new TableGenerator, map that in our static table as those are
// shared across all entities
TableGenerator generator = pClass.getAnnotation(TableGenerator.class);
if (generator != null) {
context.tableGenerators.put(generator.name(), generator);
}
boolean seenIdentityGenerator = false;
// introspect on the class and create marshallers and indices for persistent fields
List<ColumnExp<?>> fields = Lists.newArrayList();
ListMultimap<String, Tuple<SQLExpression<?>, Order>> namedFieldIndices =
ArrayListMultimap.create();
ListMultimap<String, Tuple<SQLExpression<?>, Order>> uniqueNamedFieldIndices =
ArrayListMultimap.create();
for (Field field : _pClass.getFields()) {
int mods = field.getModifiers();
// check for a static constant schema version
if (java.lang.reflect.Modifier.isStatic(mods) &&
field.getName().equals(SCHEMA_VERSION_FIELD)) {
try {
_schemaVersion = (Integer)field.get(null);
} catch (Exception e) {
log.warning("Failed to read schema version", "class", pClass, e);
}
}
// the field must be public, non-static and non-@Transient
if (!java.lang.reflect.Modifier.isPublic(mods) ||
java.lang.reflect.Modifier.isStatic(mods) ||
field.getAnnotation(Transient.class) != null) {
continue;
}
FieldMarshaller<?> fm = FieldMarshaller.createMarshaller(field);
_fields.put(field.getName(), fm);
ColumnExp<?> fieldColumn = new ColumnExp<Object>(_pClass, field.getName());
fields.add(fieldColumn);
// check to see if this is our primary key
if (field.getAnnotation(Id.class) != null) {
_pkColumns.add(fm);
}
// check if this field defines a new TableGenerator
generator = field.getAnnotation(TableGenerator.class);
if (generator != null) {
context.tableGenerators.put(generator.name(), generator);
}
// check if this field is auto-generated
GeneratedValue gv = fm.getGeneratedValue();
if (gv != null) {
// we can only do this on numeric fields
Class<?> ftype = field.getType();
boolean isNumeric = (
ftype.equals(Byte.TYPE) || ftype.equals(Byte.class) ||
ftype.equals(Short.TYPE) || ftype.equals(Short.class) ||
ftype.equals(Integer.TYPE) || ftype.equals(Integer.class) ||
ftype.equals(Long.TYPE) || ftype.equals(Long.class));
checkArgument(isNumeric, "Cannot use @GeneratedValue on non-numeric column: %s",
field.getName());
switch(gv.strategy()) {
case AUTO:
case IDENTITY:
checkArgument(!seenIdentityGenerator, "Persistent records can have at " +
"most one AUTO/IDENTITY generator.");
_valueGenerators.put(field.getName(), new IdentityValueGenerator(gv, this, fm));
seenIdentityGenerator = true;
break;
case TABLE:
String name = gv.generator();
generator = context.tableGenerators.get(name);
checkArgument(generator != null, "Unknown generator [generator=" + name + "]");
_valueGenerators.put(
field.getName(), new TableValueGenerator(generator, gv, this, fm));
break;
case SEQUENCE: // TODO
throw new IllegalArgumentException(
"SEQUENCE key generation strategy not yet supported.");
}
}
// check whether this field is indexed
Index index = field.getAnnotation(Index.class);
if (index != null) {
Column column = field.getAnnotation(Column.class);
checkArgument(column == null || !column.unique(),
"Unique columns are implicitly indexed and should not be @Index'd.");
String name = index.name().equals("") ? field.getName() + "Index" : index.name();
Tuple<SQLExpression<?>, Order> entry =
new Tuple<SQLExpression<?>, Order>(fieldColumn, Order.ASC);
if (index.unique()) {
checkArgument(!namedFieldIndices.containsKey(index.name()),
"All @Index for a particular name must be unique or non-unique");
uniqueNamedFieldIndices.put(name, entry);
} else {
checkArgument(!uniqueNamedFieldIndices.containsKey(index.name()),
"All @Index for a particular name must be unique or non-unique");
namedFieldIndices.put(name, entry);
}
}
}
for (String indexName : namedFieldIndices.keySet()) {
_indexes.add(buildIndex(indexName, false, namedFieldIndices.get(indexName)));
}
for (String indexName : uniqueNamedFieldIndices.keySet()) {
_indexes.add(buildIndex(indexName, true, uniqueNamedFieldIndices.get(indexName)));
}
// if we did not find a schema version field, freak out (but not for computed records, for
// whom there is no table)
if (_tableName != null && _schemaVersion <= 0) {
throw new IllegalStateException(
pClass.getName() + "." + SCHEMA_VERSION_FIELD + " must be greater than zero.");
}
// generate our full list of fields/columns for use in queries
_allFields = fields.toArray(new ColumnExp<?>[fields.size()]);
// now check for @Entity annotations on the entire superclass chain
Class<? extends PersistentRecord> iterClass = pClass.asSubclass(PersistentRecord.class);
do {
entity = iterClass.getAnnotation(Entity.class);
if (entity != null) {
// add any indices needed for uniqueness constraints
for (UniqueConstraint constraint : entity.uniqueConstraints()) {
ColumnExp<?>[] colExps = new ColumnExp<?>[constraint.fields().length];
int ii = 0;
for (String field : constraint.fields()) {
FieldMarshaller<?> fm = _fields.get(field);
checkArgument(fm != null, "Unknown unique constraint field: " + field);
colExps[ii ++] = new ColumnExp<Object>(_pClass, field);
}
_indexes.add(buildIndex(constraint.name(), true, colExps));
}
// add any explicit multicolumn or complex indices
for (Index index : entity.indices()) {
_indexes.add(buildIndex(index.name(), index.unique()));
}
// note any FTS indices
for (FullTextIndex fti : entity.fullTextIndices()) {
if (_fullTextIndexes.containsKey(fti.name())) {
continue;
}
_fullTextIndexes.put(fti.name(), fti);
}
}
iterClass = iterClass.getSuperclass().asSubclass(PersistentRecord.class);
} while (PersistentRecord.class.isAssignableFrom(iterClass) &&
!PersistentRecord.class.equals(iterClass));
}
/**
* Returns the persistent class this is object is a marshaller for.
*/
public Class<T> getPersistentClass ()
{
return _pClass;
}
/**
* Returns the @Computed annotation definition of this entity, or null if none.
*/
public Computed getComputed ()
{
return _computed;
}
/**
* Returns the name of the table in which persistent instances of our class are stored. By
* default this is the classname of the persistent object without the package.
*/
public String getTableName ()
{
return _tableName;
}
// from QueryMarshaller
public SQLExpression<?>[] getSelections ()
{
// when we're used in a query, we select all of our fields, in order
return _allFields;
}
/**
* Returns all the persistent fields that correspond to concrete table columns.
*/
public ColumnExp<?>[] getColumnFieldNames ()
{
return _columnFields;
}
/**
* Return the {@link FullTextIndex} registered under the given name.
*
* @exception IllegalArgumentException thrown if the requested full text index does not exist
* on this record.
*/
public FullTextIndex getFullTextIndex (String name)
{
FullTextIndex fti = _fullTextIndexes.get(name);
checkArgument(fti != null, "Persistent class missing full text index " +
"[class=" + _pClass + ", index=" + name + "]");
return fti;
}
/**
* Returns the {@link FieldMarshaller} for a named field on our persistent class.
*/
public FieldMarshaller<?> getFieldMarshaller (String fieldName)
{
return _fields.get(fieldName);
}
/**
* Returns true if our persistent object defines a primary key.
*/
public boolean hasPrimaryKey ()
{
return !_pkColumns.isEmpty();
}
/**
* Returns the {@link ValueGenerator} objects used to automatically generate field values for
* us when a new record is inserted.
*/
public Iterable<ValueGenerator> getValueGenerators ()
{
return _valueGenerators.values();
}
/**
* Return the names of the columns that constitute the primary key of our associated persistent
* record.
*/
public ColumnExp<?>[] getPrimaryKeyFields ()
{
ColumnExp<?>[] pkcols = new ColumnExp<?>[_pkColumns.size()];
for (int ii = 0; ii < pkcols.length; ii ++) {
pkcols[ii] = new ColumnExp<Object>(_pClass, _pkColumns.get(ii).getField().getName());
}
return pkcols;
}
/**
* Returns a key configured with the primary key of the supplied object. If all the fields are
* null, this method returns null. An exception is thrown if some of the fields are null and
* some are not, or if the object does not declare a primary key.
*/
public Key<T> getPrimaryKey (Object object)
{
return getPrimaryKey(object, true);
}
/**
* Returns a key configured with the primary key of the supplied object. If all the fields are
* null, this method returns null. If some of the fields are null and some are not, an
* exception is thrown. If the object does not declare a primary key and the second argument is
* true, this method throws an exception; if it's false, the method returns null.
*/
public Key<T> getPrimaryKey (Object object, boolean requireKey)
{
if (requireKey) {
checkHasPrimaryKey();
} else if (!hasPrimaryKey()) {
return null;
}
try {
Comparable<?>[] values = new Comparable<?>[_pkColumns.size()];
int nulls = 0, zeros = 0;
for (int ii = 0; ii < _pkColumns.size(); ii++) {
FieldMarshaller<?> field = _pkColumns.get(ii);
values[ii] = (Comparable<?>)field.getField().get(object);
if (values[ii] == null) {
nulls++;
} else if (values[ii] instanceof Number && ((Number)values[ii]).intValue() == 0) {
nulls++; // zeros are considered nulls; see below
zeros++;
}
}
// make sure the keys are all null or all non-null
if (nulls == 0) {
return new Key<T>(_pClass, values);
} else if (nulls == values.length) {
return null;
} else if (nulls == zeros) {
// we also allow primary keys where there are zero-valued primitive primary key
// columns as along as there is at least one non-zero valued additional key column;
// this is a compromise that allows sensible things like (id=99, type=0) but
// unfortunately also allows less sensible things like (id=0, type=5) while
// continuing to disallow the dangerous (id=0)
return new Key<T>(_pClass, values);
}
// throw an informative error message
StringBuilder keys = new StringBuilder();
for (int ii = 0; ii < _pkColumns.size(); ii++) {
keys.append(", ").append(_pkColumns.get(ii).getField().getName());
keys.append("=").append(values[ii]);
}
throw new IllegalArgumentException("Primary key fields are mixed null and non-null " +
"[class=" + _pClass.getName() + keys + "].");
} catch (IllegalAccessException iae) {
throw new RuntimeException(iae);
}
}
/**
* Creates a primary key record for the type of object handled by this marshaller, using the
* supplied primary key value. This is only allowed for records with single column keys.
*/
public Key<T> makePrimaryKey (Comparable<?> value)
{
checkHasNonCompositePrimaryKey();
return new Key<T>(_pClass, new Comparable<?>[] { value });
}
/**
* Creates a Function that changes Comparables into primary keys.
*/
public Function<Comparable<?>, Key<T>> primaryKeyFunction ()
{
checkHasNonCompositePrimaryKey();
return new Function<Comparable<?>, Key<T>>() {
public Key<T> apply (Comparable<?> value) {
return new Key<T>(_pClass, new Comparable<?>[] { value });
}
};
}
/**
* Creates a primary key record for the type of object handled by this marshaller, using the
* supplied result set.
*/
public Key<T> makePrimaryKey (ResultSet rs)
throws SQLException
{
if (!hasPrimaryKey()) {
throw new UnsupportedOperationException(
getClass().getName() + " does not define a primary key");
}
Comparable<?>[] values = new Comparable<?>[_pkColumns.size()];
for (int ii = 0; ii < _pkColumns.size(); ii++) {
Object keyValue = _pkColumns.get(ii).getFromSet(rs);
checkArgument((keyValue instanceof Comparable<?>),
"Key field must be Comparable<?> [field=%s]",
_pkColumns.get(ii).getColumnName());
values[ii] = (Comparable<?>) keyValue;
}
return new Key<T>(_pClass, values);
}
/**
* 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 boolean isInitialized ()
{
return _meta != null;
}
/**
* 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.
*/
public void init (PersistenceContext ctx, DepotMetaData meta)
throws DatabaseException
{
if (_meta != null) { // sanity check
throw new IllegalStateException(
"Cannot re-initialize marshaller [type=" + _pClass + "].");
}
_meta = meta;
final SQLBuilder builder = ctx.getSQLBuilder(new DepotTypes(ctx, _pClass));
// perform the context-sensitive initialization of the field marshallers
for (FieldMarshaller<?> fm : _fields.values()) {
fm.init(builder);
}
// if we have no table (i.e. we're a computed entity), we have nothing to create
if (getTableName() == null) {
return;
}
// figure out the list of fields that correspond to actual table columns and generate the
// SQL used to create and migrate our table (unless we're a computed entity)
_columnFields = new ColumnExp<?>[_allFields.length];
ColumnDefinition[] declarations = new ColumnDefinition[_allFields.length];
int jj = 0;
for (ColumnExp<?> field : _allFields) {
FieldMarshaller<?> fm = _fields.get(field.name);
// include all persistent non-computed fields
ColumnDefinition colDef = fm.getColumnDefinition();
if (colDef != null) {
_columnFields[jj] = field;
declarations[jj] = colDef;
jj ++;
}
}
_columnFields = ArrayUtil.splice(_columnFields, jj);
declarations = ArrayUtil.splice(declarations, jj);
// determine whether or not this record has ever been seen
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
_meta.initializeVersion(getTableName());
currentVersion = 0;
}
// now check whether we need to migrate our database schema
while (true) {
if (currentVersion >= _schemaVersion) {
// no migrations to do, but maybe we should do an explicit staleness check
if (Boolean.getBoolean("com.samskivert.depot.verifyschema")) {
checkForStaleness(TableMetaData.load(ctx, getTableName()), ctx, builder);
}
return;
}
// check whether migrations are allowed by our context
switch (ctx.canMigrate()) {
case WARN:
log.warning(_pClass.getName() + " requires migration, which is disallowed. " +
"Failures may be encountered later.");
return;
case FAIL:
throw new DatabaseException(
_pClass.getName() + " requires migration, which is disallowed.");
case ALLOWED:
// great, fall through and do our migrations
break;
}
// 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 (_meta.updateMigratingVersion(getTableName(), currentVersion, _schemaVersion, 0)) {
break; // we got the lock, let's go
}
// 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 " + _pClass.getName() + ".");
Thread.sleep(5000);
} 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());
int expectedDbVersion = currentVersion;
try {
if (!metaData.tableExists) {
// if the table does not exist, create it
createTable(ctx, builder, declarations);
metaData = TableMetaData.load(ctx, getTableName());
} else {
// if it does exist, run our migrations
metaData = runMigrations(ctx, metaData, builder, currentVersion);
}
// check for stale columns now that the table is up to date
checkForStaleness(metaData, ctx, builder);
// and update our version in the schema version table
_meta.updateVersion(getTableName(), _schemaVersion);
expectedDbVersion = _schemaVersion;
} finally {
// set our migrating version back to zero
try {
if (!_meta.updateMigratingVersion(
getTableName(), expectedDbVersion, 0, _schemaVersion)) {
log.warning("Failed to restore migrating version to zero!", "record", _pClass);
}
} catch (Exception e) {
log.warning("Failure restoring migrating version! Bad bad!", "record", _pClass, e);
}
}
}
/**
* This is called by the persistence context to register a migration for the entity managed by
* this marshaller.
*/
public void registerMigration (SchemaMigration migration)
{
_schemaMigs.add(migration);
}
/**
* Creates a persistent object from the supplied result set. The result set must have come from
* a properly constructed query (see {@link BuildVisitor}).
*/
public T createObject (ResultSet rs)
throws SQLException
{
try {
// first, build a set of the fields that we actually received
Set<String> fields = Sets.newHashSet();
ResultSetMetaData metadata = rs.getMetaData();
for (int ii = 1; ii <= metadata.getColumnCount(); ii ++) {
fields.add(metadata.getColumnLabel(ii));
}
// then create and populate the persistent object
T po = _pClass.newInstance();
for (FieldMarshaller<?> fm : _fields.values()) {
if (!fields.contains(fm.getColumnName())) {
// this field was not in the result set, make sure that's OK
if (fm.getComputed() != null && !fm.getComputed().required()) {
continue;
}
throw new SQLException(
"ResultSet missing field: " + fm.getField().getName() + " for " + _pClass);
}
fm.getAndWriteToObject(rs, po);
}
return po;
} catch (SQLException sqe) {
// pass this on through
throw sqe;
} catch (Exception e) {
String errmsg = "Failed to unmarshall persistent object [class=" +
_pClass.getName() + "]";
throw (SQLException)new SQLException(errmsg).initCause(e);
}
}
/**
* Go through the registered {@link ValueGenerator}s for our persistent object and run the ones
* that match the current postFactum phase, filling in the fields on the supplied object while
* we go.
*
* The return value is only non-empty for the !postFactum phase, in which case it is a set of
* field names that are associated with {@link IdentityValueGenerator}, because these need
* special handling in the INSERT (specifically, 'DEFAULT' must be supplied as a value in the
* eventual SQL).
*/
public Set<String> generateFieldValues (
Connection conn, DatabaseLiaison liaison, Object po, boolean postFactum)
{
Set<String> idFields = Sets.newHashSet();
for (ValueGenerator vg : _valueGenerators.values()) {
if (!postFactum && vg instanceof IdentityValueGenerator) {
idFields.add(vg.getFieldMarshaller().getField().getName());
}
if (vg.isPostFactum() != postFactum) {
continue;
}
try {
int nextValue = vg.nextGeneratedValue(conn, liaison);
vg.getFieldMarshaller().getField().set(po, nextValue);
} catch (Exception e) {
throw new IllegalStateException(
"Failed to assign primary key [type=" + _pClass + "]", e);
}
}
return idFields;
}
protected void createTable (PersistenceContext ctx, final SQLBuilder builder,
final ColumnDefinition[] declarations)
throws DatabaseException
{
log.info("Creating initial table '" + getTableName() + "'.");
ctx.invoke(new Modifier() {
@Override
protected int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
// create the table
String[] primaryKeyColumns = null;
if (hasPrimaryKey()) {
primaryKeyColumns = new String[_pkColumns.size()];
for (int ii = 0; ii < primaryKeyColumns.length; ii ++) {
primaryKeyColumns[ii] = _pkColumns.get(ii).getColumnName();
}
}
liaison.createTableIfMissing(conn, getTableName(), fieldsToColumns(_columnFields),
declarations, null, primaryKeyColumns);
// add its indexen
for (CreateIndexClause iclause : _indexes) {
execute(conn, builder, iclause);
}
// create our value generators
for (ValueGenerator vg : _valueGenerators.values()) {
vg.create(conn, liaison);
}
// and its full text search indexes
for (FullTextIndex fti : _fullTextIndexes.values()) {
builder.addFullTextSearch(conn, DepotMarshaller.this, fti);
}
return 0;
}
});
}
protected int execute (Connection conn, SQLBuilder builder, QueryClause clause)
throws SQLException
{
if (builder.newQuery(clause)) {
return builder.prepare(conn).executeUpdate();
}
return 0;
}
protected TableMetaData runMigrations (final PersistenceContext ctx, TableMetaData metaData,
final SQLBuilder builder, int currentVersion)
throws DatabaseException
{
log.info("Migrating " + getTableName() + " from " + currentVersion + " to " +
_schemaVersion + "...");
if (_schemaMigs.size() > 0) {
// run our pre-default-migrations
for (SchemaMigration migration : _schemaMigs) {
if (migration.runBeforeDefault() &&
migration.shouldRunMigration(currentVersion, _schemaVersion)) {
migration.init(getTableName(), _fields);
ctx.invoke(migration);
}
}
// we don't know what the pre-migrations did so we have to re-read metadata
metaData = TableMetaData.load(ctx, getTableName());
}
// figure out which columns we have in the table now, so that when all is said and done we
// can see what new columns we have in the table and run the creation code for any value
// generators that are defined on those columns (we can't just track the columns we add in
// our automatic migrations because someone might register custom migrations that add
// columns specially)
Set<String> preMigrateColumns = Sets.newHashSet(metaData.tableColumns);
// add any missing columns
for (ColumnExp<?> field : _columnFields) {
final FieldMarshaller<?> fmarsh = _fields.get(field.name);
if (metaData.tableColumns.remove(fmarsh.getColumnName())) {
continue;
}
// otherwise add the column
final ColumnDefinition coldef = fmarsh.getColumnDefinition();
log.info("Adding column to " + getTableName() + ": " + fmarsh.getColumnName());
ctx.invoke(new Modifier.Simple() {
@Override protected String createQuery (DatabaseLiaison liaison) {
return "alter table " + liaison.tableSQL(getTableName()) +
" add column " + liaison.columnSQL(fmarsh.getColumnName()) + " " +
liaison.expandDefinition(coldef);
}
});
// if the column is a TIMESTAMP or DATETIME column, we need to run a special query to
// update all existing rows to the current time because MySQL annoyingly assigns
// TIMESTAMP columns a value of "0000-00-00 00:00:00" regardless of whether we
// explicitly provide a "DEFAULT" value for the column or not, and DATETIME columns
// cannot accept CURRENT_TIME or NOW() defaults at all.
if (!coldef.nullable && coldef.defaultValue == null &&
(coldef.type.equalsIgnoreCase("timestamp") ||
coldef.type.equalsIgnoreCase("datetime"))) {
log.info("Assigning current time to " + fmarsh.getColumnName() + ".");
ctx.invoke(new Modifier.Simple() {
@Override protected String createQuery (DatabaseLiaison liaison) {
// TODO: is NOW() standard SQL?
return "update " + liaison.tableSQL(getTableName()) +
" set " + liaison.columnSQL(fmarsh.getColumnName()) + " = NOW()";
}
});
}
}
// add, remove or change the primary key as needed
if (hasPrimaryKey() && metaData.pkName == null) {
log.info("Adding primary key.");
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
liaison.addPrimaryKey(
conn, getTableName(), fieldsToColumns(getPrimaryKeyFields()));
return 0;
}
});
} else if (!hasPrimaryKey() && metaData.pkName != null) {
final String pkName = metaData.pkName;
log.info("Dropping primary key: " + pkName);
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
liaison.dropPrimaryKey(conn, getTableName(), pkName);
return 0;
}
});
} else if (!metaData.pkMatches(_pkColumns)) {
final String pkName = metaData.pkName;
log.info("Primary key has changed: dropping and readding: " + pkName);
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
liaison.dropPrimaryKey(conn, getTableName(), pkName);
liaison.addPrimaryKey(
conn, getTableName(), fieldsToColumns(getPrimaryKeyFields()));
return 0;
}
});
}
// add any named indices that exist on the record but not yet on the table
for (final CreateIndexClause iclause : _indexes) {
if (metaData.indexColumns.containsKey(iclause.getName())) {
metaData.indexColumns.remove(iclause.getName()); // this index already exists
continue;
}
// but this is a new, named index, so we create it
log.info("Creating new index: " + iclause.getName());
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
execute(conn, builder, iclause);
return 0;
}
});
}
// next we create any full text search indexes that exist on the record but not in the
// table, first step being to do a dialect-sensitive enumeration of existing indexes
Set<String> tableFts = Sets.newHashSet();
builder.getFtsIndexes(metaData.tableColumns, metaData.indexColumns.keySet(), tableFts);
// then iterate over what should be there
for (final FullTextIndex recordFts : _fullTextIndexes.values()) {
if (tableFts.contains(recordFts.name())) {
// the table already contains this one
continue;
}
// but not this one, so let's create it
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
builder.addFullTextSearch(conn, DepotMarshaller.this, recordFts);
return 0;
}
});
}
// we do not auto-remove columns but rather require that SchemaMigration.Drop records be
// registered by hand to avoid accidentally causing the loss of data
// we don't auto-remove indices either because we'd have to sort out the potentially
// complex origins of an index (which might be because of a @Unique column or maybe the
// index was hand defined in a @Column clause)
// run our post-default-migrations
for (SchemaMigration migration : _schemaMigs) {
if (!migration.runBeforeDefault() &&
migration.shouldRunMigration(currentVersion, _schemaVersion)) {
migration.init(getTableName(), _fields);
ctx.invoke(migration);
}
}
// now reload our table metadata so that we can see what columns we have now
metaData = TableMetaData.load(ctx, getTableName());
// initialize value generators for any columns that have been newly added
for (String column : metaData.tableColumns) {
if (preMigrateColumns.contains(column)) {
continue;
}
// see if we have a value generator for this new column
final ValueGenerator valgen = _valueGenerators.get(column);
if (valgen == null) {
continue;
}
// note: if someone renames a column that has an identity value generator, things will
// break because Postgres automatically creates a table_column_seq sequence that is
// used to generate values for that column and god knows what happens when that is
// renamed; plus we're potentially going to try to reinitialize it if it has a non-zero
// initialValue which will use the new column name to obtain the sequence name which
// ain't going to work either; we punt!
ctx.invoke(new Modifier() {
@Override protected int invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException {
valgen.create(conn, liaison);
return 0;
}
});
}
return metaData;
}
protected CreateIndexClause buildIndex (String name, boolean unique)
{
Method method;
try {
method = _pClass.getMethod(name);
} catch (NoSuchMethodException nsme) {
throw new IllegalArgumentException(
"Index flagged as complex, but no defining method '" + name + "' found.", nsme);
}
try {
return buildIndex(name, unique, method.invoke(null));
} catch (Exception e) {
throw new IllegalArgumentException(
"Error calling index definition method '" + name + "'", e);
}
}
protected CreateIndexClause buildIndex (String name, boolean unique, Object config)
{
List<Tuple<SQLExpression<?>, Order>> definition = Lists.newArrayList();
if (config instanceof ColumnExp<?>) {
definition.add(new Tuple<SQLExpression<?>, Order>((ColumnExp<?>)config, Order.ASC));
} else if (config instanceof ColumnExp<?>[]) {
for (ColumnExp<?> column : (ColumnExp<?>[])config) {
definition.add(new Tuple<SQLExpression<?>, Order>(column, Order.ASC));
}
} else if (config instanceof SQLExpression) {
definition.add(new Tuple<SQLExpression<?>, Order>((SQLExpression<?>)config, Order.ASC));
} else if (config instanceof Tuple<?,?>) {
@SuppressWarnings("unchecked") Tuple<SQLExpression<?>, Order> tuple =
(Tuple<SQLExpression<?>, Order>)config;
definition.add(tuple);
} else if (config instanceof List<?>) {
@SuppressWarnings("unchecked") List<Tuple<SQLExpression<?>, Order>> defs =
(List<Tuple<SQLExpression<?>, Order>>)config;
definition.addAll(defs);
} else {
throw new IllegalArgumentException(
"Method '" + name + "' must return ColumnExp[], SQLExpression or " +
"List<Tuple<SQLExpression, Order>>");
}
return new CreateIndexClause(_pClass, getTableName() + "_" + name, unique, definition);
}
// translate an array of field names to an array of column names
protected String[] fieldsToColumns (ColumnExp<?>[] fields)
{
String[] columns = new String[fields.length];
for (int ii = 0; ii < columns.length; ii ++) {
FieldMarshaller<?> fm = _fields.get(fields[ii].name);
checkArgument(fm != null, "Unknown field on record [field=%s, class=%s]",
fields[ii], _pClass);
columns[ii] = fm.getColumnName();
}
return columns;
}
/**
* Checks that there are no database columns for which we no longer have Java fields.
*/
protected void checkForStaleness (
TableMetaData meta, PersistenceContext ctx, SQLBuilder builder)
throws DatabaseException
{
for (ColumnExp<?> field : _columnFields) {
FieldMarshaller<?> fmarsh = _fields.get(field.name);
meta.tableColumns.remove(fmarsh.getColumnName());
}
for (String column : meta.tableColumns) {
if (builder.isPrivateColumn(column, _fullTextIndexes)) {
continue;
}
log.warning("Stale column", "table", getTableName(), "column", column);
}
for (CreateIndexClause clause : _indexes) {
meta.indexColumns.remove(clause.getName());
}
for (String index : meta.indexColumns.keySet()) {
if (builder.isPrivateIndex(index, _fullTextIndexes)) {
continue;
}
log.warning("Stale index", "table", getTableName(), "index", index);
}
}
protected void checkHasPrimaryKey ()
{
if (!hasPrimaryKey()) {
throw new UnsupportedOperationException(
getClass().getName() + " does not define a primary key");
}
}
protected void checkHasNonCompositePrimaryKey ()
{
if (_pkColumns.size() != 1) {
throw new UnsupportedOperationException(
getClass().getName() + " does not define a single column primary key");
}
}
protected static class TableMetaData
{
public boolean tableExists;
public Set<String> tableColumns = Sets.newHashSet();
public Map<String, Set<String>> indexColumns = Maps.newHashMap();
public String pkName;
public Set<String> pkColumns = Sets.newHashSet();
public static TableMetaData load (PersistenceContext ctx, final String tableName)
throws DatabaseException
{
return ctx.invoke(new Fetcher.Trivial<TableMetaData>() {
public TableMetaData invoke (PersistenceContext ctx, Connection conn,
DatabaseLiaison dl) throws SQLException {
return new TableMetaData(conn.getMetaData(), tableName);
}
});
}
public TableMetaData (DatabaseMetaData meta, String tableName)
throws SQLException
{
tableExists = meta.getTables(null, null, tableName, null).next();
if (!tableExists) {
return;
}
ResultSet rs = meta.getColumns(null, null, tableName, "%");
while (rs.next()) {
tableColumns.add(rs.getString("COLUMN_NAME"));
}
rs = meta.getIndexInfo(null, null, tableName, false, false);
while (rs.next()) {
String indexName = rs.getString("INDEX_NAME");
Set<String> set = indexColumns.get(indexName);
if (rs.getBoolean("NON_UNIQUE")) {
// not a unique index: just make sure there's an entry in the keyset
if (set == null) {
indexColumns.put(indexName, null);
}
} else {
// for unique indices we collect the column names
if (set == null) {
set = Sets.newHashSet();
indexColumns.put(indexName, set);
}
set.add(rs.getString("COLUMN_NAME"));
}
}
rs = meta.getPrimaryKeys(null, null, tableName);
while (rs.next()) {
pkName = rs.getString("PK_NAME");
pkColumns.add(rs.getString("COLUMN_NAME"));
}
}
public boolean pkMatches (List<FieldMarshaller<?>> declaredPkColumns)
{
if (pkColumns.size() != declaredPkColumns.size()) {
return false;
}
for (FieldMarshaller<?> column : declaredPkColumns) {
if (!pkColumns.contains(column.getColumnName())) {
return false;
}
}
return true;
}
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
/** Provides access to certain internal metadata. */
protected DepotMetaData _meta;
/** The persistent object class that we manage. */
protected Class<T> _pClass;
/** The name of our persistent object table. */
protected String _tableName;
/** The @Computed annotation of this entity, or null. */
protected Computed _computed;
/** A mapping of field names to value generators for that field. */
protected Map<String, ValueGenerator> _valueGenerators = Maps.newHashMap();
/** A field marshaller for each persistent field in our object. */
protected Map<String, FieldMarshaller<?>> _fields = Maps.newHashMap();
/** The field marshallers for our persistent object's primary key columns or null if it did not
* define a primary key. */
protected List<FieldMarshaller<?>> _pkColumns = Lists.newArrayList();
/** The persisent fields of our object, in definition order. */
protected ColumnExp<?>[] _allFields;
/** The fields of our object with directly corresponding table columns. */
protected ColumnExp<?>[] _columnFields;
/** The indexes defined for this record. */
protected List<CreateIndexClause> _indexes = Lists.newArrayList();
/** Any full text indices defined on this entity. */
protected Map<String, FullTextIndex> _fullTextIndexes = Maps.newHashMap();
/** The version of our persistent object schema as specified in the class definition. */
protected int _schemaVersion = -1;
/** A list of hand registered schema migrations to run prior to doing the default migration. */
protected List<SchemaMigration> _schemaMigs = Lists.newArrayList();
}