Files
samskivert/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
T
mdb bc41237e00 Classes go after methods.
git-svn-id: https://samskivert.googlecode.com/svn/trunk@2169 6335cc39-0255-0410-8fd6-9bcaacd3b74c
2007-08-13 21:59:25 +00:00

948 lines
37 KiB
Java

//
// $Id$
//
// 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
// (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;
import java.lang.reflect.Field;
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.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
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.FullTextIndex;
import com.samskivert.jdbc.depot.annotation.GeneratedValue;
import com.samskivert.jdbc.depot.annotation.Id;
import com.samskivert.jdbc.depot.annotation.Index;
import com.samskivert.jdbc.depot.annotation.Table;
import com.samskivert.jdbc.depot.annotation.TableGenerator;
import com.samskivert.jdbc.depot.annotation.Transient;
import com.samskivert.jdbc.depot.annotation.UniqueConstraint;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.util.ArrayUtil;
import static com.samskivert.jdbc.depot.Log.log;
/**
* Handles the marshalling and unmarshalling of persistent instances to JDBC primitives ({@link
* PreparedStatement} and {@link ResultSet}).
*/
public class DepotMarshaller<T extends PersistentRecord>
{
/** 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;
Entity entity = pclass.getAnnotation(Entity.class);
// see if this is a computed entity
Computed computed = pclass.getAnnotation(Computed.class);
if (computed == null) {
// if not, this class has a corresponding SQL table
_tableName = _pclass.getName();
_tableName = _tableName.substring(_tableName.lastIndexOf(".")+1);
// 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);
}
// if there are FTS indexes in the Table, map those out here for future use
Table table = pclass.getAnnotation(Table.class);
if (table != null) {
for (FullTextIndex fts : table.fullTextIndexes()) {
_fullTextIndexes.put(fts.name(), fts);
}
}
// introspect on the class and create marshallers for persistent fields
ArrayList<String> fields = new ArrayList<String>();
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.log(Level.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);
fields.add(field.getName());
// check to see if this is our primary key
if (field.getAnnotation(Id.class) != null) {
if (_pkColumns == null) {
_pkColumns = new ArrayList<FieldMarshaller>();
}
_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);
}
}
}
// if the entity defines a single-columnar primary key, figure out if we will be generating
// values for it
if (_pkColumns != null) {
GeneratedValue gv = null;
FieldMarshaller keyField = null;
// loop over fields to see if there's a @GeneratedValue at all
for (FieldMarshaller field : _pkColumns) {
gv = field.getGeneratedValue();
if (gv != null) {
keyField = field;
break;
}
}
if (keyField != null) {
// and if there is, make sure we've a single-column id
if (_pkColumns.size() > 1) {
throw new IllegalArgumentException(
"Cannot use @GeneratedValue on multiple-column @Id's");
}
// the primary key must be numeric if we are to auto-assign it
Class<?> ftype = keyField.getField().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));
if (!isNumeric) {
throw new IllegalArgumentException(
"Cannot use @GeneratedValue on non-numeric column");
}
switch(gv.strategy()) {
case AUTO:
case IDENTITY:
_keyGenerator = new IdentityKeyGenerator(
gv, getTableName(), keyField.getColumnName());
break;
case TABLE:
String name = gv.generator();
generator = context.tableGenerators.get(name);
if (generator == null) {
throw new IllegalArgumentException(
"Unknown generator [generator=" + name + "]");
}
_keyGenerator = new TableKeyGenerator(
generator, gv, getTableName(), keyField.getColumnName());
break;
}
}
}
// generate our full list of fields/columns for use in queries
_allFields = fields.toArray(new String[fields.size()]);
}
/**
* Returns the persistent class this is object is a marshaller for.
*/
public Class<T> getPersistentClass ()
{
return _pclass;
}
/**
* 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;
}
/**
* Returns all the persistent fields of our class, in definition order.
*/
public String[] getFieldNames ()
{
return _allFields;
}
/**
* Returns all the persistent fields that correspond to concrete table columns.
*/
public String[] getColumnFieldNames ()
{
return _columnFields;
}
/**
* Return the {@link FullTextIndex} registered under the given name, or null if none.
*/
public FullTextIndex getFullTextIndex (String name)
{
return _fullTextIndexes.get(name);
}
/**
* 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 != null);
}
/**
* Returns the {@link KeyGenerator} used to generate primary keys for this persistent object,
* or null if it does not use a key generator.
*/
public KeyGenerator getKeyGenerator ()
{
return _keyGenerator;
}
/**
* Return the names of the columns that constitute the primary key of our associated persistent
* record.
*/
public String[] getPrimaryKeyFields ()
{
String[] pkcols = new String[_pkColumns.size()];
for (int ii = 0; ii < pkcols.length; ii ++) {
pkcols[ii] = _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 (!hasPrimaryKey()) {
if (requireKey) {
throw new UnsupportedOperationException(
_pclass.getName() + " does not define a primary key");
}
return null;
}
try {
Comparable[] values = new Comparable[_pkColumns.size()];
boolean hasNulls = false;
for (int ii = 0; ii < _pkColumns.size(); ii++) {
FieldMarshaller field = _pkColumns.get(ii);
values[ii] = (Comparable) field.getField().get(object);
if (values[ii] == null || Integer.valueOf(0).equals(values[ii])) {
// if this is the first null we see but not the first field, freak out
if (!hasNulls && ii > 0) {
throw new IllegalArgumentException(
"Persistent object's primary key fields are mixed null and non-null.");
}
hasNulls = true;
} else if (hasNulls) {
// if this is a non-null field and we've previously seen nulls, also freak
throw new IllegalArgumentException(
"Persistent object's primary key fields are mixed null and non-null.");
}
}
// if all the fields were null, return null, else build a key
return hasNulls ? null : makePrimaryKey(values);
} 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.
*/
public Key<T> makePrimaryKey (Comparable... values)
{
if (!hasPrimaryKey()) {
throw new UnsupportedOperationException(
getClass().getName() + " does not define a primary key");
}
String[] fields = new String[_pkColumns.size()];
for (int ii = 0; ii < _pkColumns.size(); ii++) {
fields[ii] = _pkColumns.get(ii).getField().getName();
}
return new Key<T>(_pclass, fields, values);
}
/**
* 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);
if (!(keyValue instanceof Comparable)) {
throw new IllegalArgumentException("Key field must be Comparable [field=" +
_pkColumns.get(ii).getColumnName() + "]");
}
values[ii] = (Comparable) keyValue;
}
return makePrimaryKey(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 _initialized;
}
/**
* Creates a persistent object from the supplied result set. The result set must have come from
* a query provided by {@link #createQuery}.
*/
public T createObject (ResultSet rs)
throws SQLException
{
try {
// first, build a set of the fields that we actually received
Set<String> fields = new HashSet<String>();
ResultSetMetaData metadata = rs.getMetaData();
for (int ii = 1; ii <= metadata.getColumnCount(); ii ++) {
fields.add(metadata.getColumnName(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());
}
fm.writeToObject(rs, po);
}
return po;
} catch (SQLException sqe) {
// pass this on through
throw sqe;
} catch (Exception e) {
String errmsg = "Failed to unmarshall persistent object [pclass=" +
_pclass.getName() + "]";
throw (SQLException)new SQLException(errmsg).initCause(e);
}
}
/**
* Fills in the primary key just assigned to the supplied persistence object by the execution
* of the results of {@link #createInsert}.
*
* @return the newly assigned primary key or null if the object does not use primary keys or
* this is not the right time to assign the key.
*/
public Key assignPrimaryKey (
Connection conn, DatabaseLiaison liaison, Object po, boolean postFactum)
throws SQLException
{
// if we have no primary key or no generator, then we're done
if (!hasPrimaryKey() || _keyGenerator == null) {
return null;
}
// run this generator either before or after the actual insertion
if (_keyGenerator.isPostFactum() != postFactum) {
return null;
}
try {
int nextValue = _keyGenerator.nextGeneratedValue(conn, liaison);
_pkColumns.get(0).getField().set(po, nextValue);
return makePrimaryKey(nextValue);
} catch (Exception e) {
String errmsg = "Failed to assign primary key [type=" + _pclass + "]";
throw (SQLException) new SQLException(errmsg).initCause(e);
}
}
/**
* 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 (final PersistenceContext ctx)
throws PersistenceException
{
if (_initialized) { // sanity check
throw new IllegalStateException(
"Cannot re-initialize marshaller [type=" + _pclass + "].");
}
_initialized = true;
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 String[_allFields.length];
String[] declarations = new String[_allFields.length];
int jj = 0;
for (int ii = 0; ii < _allFields.length; ii++) {
FieldMarshaller fm = _fields.get(_allFields[ii]);
// include all persistent non-computed fields
String colDef = fm.getColumnDefinition();
if (colDef != null) {
_columnFields[jj] = _allFields[ii];
declarations[jj] = colDef;
jj ++;
}
}
_columnFields = ArrayUtil.splice(_columnFields, jj);
declarations = ArrayUtil.splice(declarations, jj);
// if we did not find a schema version field, complain
if (_schemaVersion < 0) {
log.warning("Unable to read " + _pclass.getName() + "." + SCHEMA_VERSION_FIELD +
". Schema migration disabled.");
}
// check to see if our schema version table exists, create it if not
ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
liaison.createTableIfMissing(
conn, SCHEMA_VERSION_TABLE,
new String[] { "persistentClass", "version" },
new String[] { "VARCHAR(255) NOT NULL", "INTEGER NOT NULL" },
null,
new String[] { "persistentClass" });
return 0;
}
});
// fetch all relevant information regarding our table from the database
TableMetaData metaData = TableMetaData.load(ctx, getTableName());
// compute relevant information from our persistent record, too
RecordMetaData rMeta = new RecordMetaData(_pclass, _fields);
// if the table does not exist, create it
if (!metaData.tableExists) {
final String[] fDeclarations = declarations;
final String[][] uniqueConCols = new String[rMeta.uniqueConstraints.size()][];
int kk = 0;
for (Set<String> colSet : rMeta.uniqueConstraints) {
uniqueConCols[kk++] = colSet.toArray(new String[colSet.size()]);
}
final Iterable<Index> indexen = rMeta.indices.values();
ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
// 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,
uniqueConCols, primaryKeyColumns);
// add its indexen
for (Index idx : indexen) {
liaison.addIndexToTable(
conn, getTableName(), idx.columns(),
getTableName() + "_" + 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 the table exists, see if should attempt automatic schema migration
if (_schemaVersion < 0) {
// nope, versioning disabled
verifySchemasMatch(metaData, ctx, builder);
return;
}
// make sure the versions match
int currentVersion = ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
String query =
" select " + liaison.columnSQL("version") +
" from " + liaison.tableSQL(SCHEMA_VERSION_TABLE) +
" where " + liaison.columnSQL("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) {
verifySchemasMatch(metaData, ctx, builder);
return;
}
// otherwise try to migrate the schema
log.info("Migrating " + getTableName() + " from " + currentVersion + " to " +
_schemaVersion + "...");
if (_migrations.size() > 0) {
// run our pre-default-migrations
for (EntityMigration migration : _migrations) {
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());
}
// this is a little silly, but we need a copy for name disambiguation later
Set<String> indicesCopy = new HashSet<String>(metaData.indexColumns.keySet());
// add any missing columns
for (String fname : _columnFields) {
final FieldMarshaller fmarsh = _fields.get(fname);
if (metaData.tableColumns.remove(fmarsh.getColumnName())) {
continue;
}
// otherwise add the column
final String coldef = fmarsh.getColumnDefinition();
log.info("Adding column to " + getTableName() + ": " +
fmarsh.getColumnName() + " " + coldef);
ctx.invoke(new Modifier.Simple() {
protected String createQuery (DatabaseLiaison liaison) {
return "alter table " + liaison.tableSQL(getTableName()) +
" add column " + liaison.columnSQL(fmarsh.getColumnName()) + " " + 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.toLowerCase().indexOf(" timestamp") != -1 ||
coldef.toLowerCase().indexOf(" datetime") != -1) {
log.info("Assigning current time to " + fmarsh.getColumnName() + ".");
ctx.invoke(new Modifier.Simple() {
protected String createQuery (DatabaseLiaison liaison) {
// TODO: is NOW() standard SQL?
return "update " + liaison.tableSQL(getTableName()) +
" set " + liaison.columnSQL(fmarsh.getColumnName()) + " = NOW()";
}
});
}
}
// 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;
}
});
} else if (!hasPrimaryKey() && metaData.pkName != null) {
final String pkName = metaData.pkName;
log.info("Dropping primary key: " + pkName);
ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
liaison.dropPrimaryKey(conn, getTableName(), pkName);
return 0;
}
});
}
// add any missing indices
for (final Index index : rMeta.indices.values()) {
final String ixName = getTableName() + "_" + index.name();
if (metaData.indexColumns.containsKey(ixName)) {
metaData.indexColumns.remove(ixName);
continue;
}
ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
liaison.addIndexToTable(
conn, getTableName(), index.columns(), ixName, index.unique());
return 0;
}
});
}
// to get the @Table(uniqueIndices...) indices, we use our clever set of column name sets
Set<Set<String>> uniqueIndices = new HashSet<Set<String>>(metaData.indexColumns.values());
for (Set<String> colSet : rMeta.uniqueConstraints) {
// 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
// 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 (EntityMigration migration : _migrations) {
if (!migration.runBeforeDefault() &&
migration.shouldRunMigration(currentVersion, _schemaVersion)) {
migration.init(getTableName(), _fields);
ctx.invoke(migration);
}
}
// record our new version in the database
ctx.invoke(new Modifier() {
public int invoke (Connection conn, DatabaseLiaison liaison) throws SQLException {
updateVersion(conn, liaison, _schemaVersion);
return 0;
}
});
}
/**
* Checks that there are no database columns for which we no longer have Java fields.
*/
protected void verifySchemasMatch (
TableMetaData meta, PersistenceContext ctx, SQLBuilder builder)
throws PersistenceException
{
for (String fname : _columnFields) {
FieldMarshaller fmarsh = _fields.get(fname);
meta.tableColumns.remove(fmarsh.getColumnName());
}
for (String column : meta.tableColumns) {
if (builder.isPrivateColumn(column)) {
continue;
}
log.warning(getTableName() + " contains stale column '" + column + "'.");
}
}
protected void updateVersion (Connection conn, DatabaseLiaison liaison, int version)
throws SQLException
{
String update =
"update " + liaison.tableSQL(SCHEMA_VERSION_TABLE) +
" set " + liaison.columnSQL("version") + " = " + version +
" where " + liaison.columnSQL("persistentClass") + " = '" + getTableName() + "'";
Statement stmt = conn.createStatement();
try {
if (stmt.executeUpdate(update) == 0) {
String insert =
"insert into " + liaison.tableSQL(SCHEMA_VERSION_TABLE) +
" values('" + getTableName() + "', " + version + ")";
stmt.executeUpdate(insert);
}
} finally {
stmt.close();
}
}
protected static class TableMetaData
{
public boolean tableExists;
public Set<String> tableColumns = new HashSet<String>();
public Map<String, Set<String>> indexColumns = new HashMap<String, Set<String>>();
public String pkName;
public Set<String> pkColumns = new HashSet<String>();
public static TableMetaData load (PersistenceContext ctx, final String tableName)
throws PersistenceException
{
return ctx.invoke(new Query.TrivialQuery<TableMetaData>() {
public TableMetaData invoke (Connection conn, DatabaseLiaison dl)
throws SQLException {
return new TableMetaData(conn.getMetaData(), tableName);
}
});
}
public TableMetaData (DatabaseMetaData meta, String tableName)
throws SQLException
{
tableExists = meta.getTables("", "", 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 = new HashSet<String>();
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"));
}
}
}
protected static class RecordMetaData
{
public Map<String, Index> indices = new HashMap<String, Index>();
public Set<Set<String>> uniqueConstraints = new HashSet<Set<String>>();
public RecordMetaData (Class<?> pClass, Map<String, FieldMarshaller> fmap)
{
recurse(pClass, fmap);
}
protected void recurse (Class<?> pClass, Map<String, FieldMarshaller> fmap)
{
// recurse first so subclass definitions overwrite superclass ones
Class<?> superClass = pClass.getSuperclass();
if (PersistentRecord.class.isAssignableFrom(superClass) &&
!PersistentRecord.class.equals(superClass)) {
recurse(superClass, fmap);
}
Table table = pClass.getAnnotation(Table.class);
if (table != null) {
for (UniqueConstraint constraint : table.uniqueConstraints()) {
String[] fields = constraint.fieldNames();
Set<String> colSet = new HashSet<String>();
for (int ii = 0; ii < fields.length; ii ++) {
FieldMarshaller fm = fmap.get(fields[ii]);
if (fm == null) {
throw new IllegalArgumentException(
"Unknown unique constraint field: " + fields[ii]);
}
colSet.add(fm.getColumnName());
}
uniqueConstraints.add(colSet);
}
}
Entity entity = pClass.getAnnotation(Entity.class);
if (entity != null) {
for (Index index : entity.indices()) {
indices.put(index.name(), index);
}
}
}
}
/** The persistent object class that we manage. */
protected Class<T> _pclass;
/** The name of our persistent object table. */
protected String _tableName;
/** A field marshaller for each persistent field in our object. */
protected Map<String, FieldMarshaller> _fields = new HashMap<String, FieldMarshaller>();
/** The field marshallers for our persistent object's primary key columns or null if it did not
* define a primary key. */
protected ArrayList<FieldMarshaller> _pkColumns;
/** The generator to use for auto-generating primary key values, or null. */
protected KeyGenerator _keyGenerator;
/** The persisent fields of our object, in definition order. */
protected String[] _allFields;
/** The fields of our object with directly corresponding table columns. */
protected String[] _columnFields;
/** A mapping of all of the full text index annotations for our persistent record. */
protected Map<String, FullTextIndex> _fullTextIndexes = new HashMap<String, FullTextIndex>();
/** 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 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";
}