diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 150c93d..0c80b4a 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -26,20 +26,32 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; +import java.util.logging.Level; import javax.persistence.Id; +import javax.persistence.Transient; +import com.samskivert.jdbc.JDBCUtil; import com.samskivert.util.StringUtil; +import static com.samskivert.Log.log; + /** * Handles the marshalling and unmarshalling of persistent instances to JDBC * primitives ({@link PreparedStatement} and {@link ResultSet}). */ public class DepotMarshaller { + /** The name of the private static field that must be defined for all + * persistent object classes which 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. */ @@ -54,11 +66,23 @@ public class DepotMarshaller // introspect on the class and create marshallers for persistent fields ArrayList fields = new ArrayList(); for (Field field : _pclass.getFields()) { - // the field must be public, non-static and non-transient int mods = field.getModifiers(); + + // check for a static constant schema version + if ((mods & Modifier.STATIC) != 0 && + 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 (((mods & Modifier.PUBLIC) == 0) || ((mods & Modifier.STATIC) != 0) || - ((mods & Modifier.TRANSIENT) != 0)) { + field.getAnnotation(Transient.class) != null) { continue; } @@ -76,6 +100,20 @@ public class DepotMarshaller // generate our full list of fields/columns for use in queries _allFields = fields.toArray(new String[fields.size()]); _fullColumnList = StringUtil.join(_allFields, ","); + + // create the SQL used to create and migrate our table + _columnDefinitions = new String[_allFields.length]; + for (int ii = 0; ii < _allFields.length; ii++) { + _columnDefinitions[ii] = + _fields.get(_allFields[ii]).getColumnDefinition(); + } + _postamble = ""; // TODO: add annotations for the postamble + + // 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."); + } } /** @@ -116,6 +154,44 @@ public class DepotMarshaller return new DepotRepository.Key(_primaryKey.getColumnName(), value); } + /** + * Initializes the table used by this marshaller. 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 (Connection conn) + throws SQLException + { + // check to see if our schema version table exists, create it if not + JDBCUtil.createTableIfMissing(conn, SCHEMA_VERSION_TABLE, + new String[] { "persistentClass VARCHAR(255) NOT NULL", + "version INTEGER NOT NULL" }, + ""); + + // now create the table for our persistent class if it does not exist + if (!JDBCUtil.tableExists(conn, getTableName())) { + JDBCUtil.createTableIfMissing( + conn, getTableName(), _columnDefinitions, _postamble); + // TODO: insert current version into version table + return; + } + + // if schema versioning is disabled, stop now + if (_schemaVersion < 0) { + return; + } + + // otherwise, then make sure the versions match and do some migration + // if they don't + Statement stmt = conn.createStatement(); + try { + // TODO: magical migration; execute registered migration actions + } finally { + stmt.close(); + } + } + /** * Creates a query for instances of this persistent object type using the * supplied key. @@ -195,6 +271,49 @@ public class DepotMarshaller } } + /** + * Fills in the primary key just assigned to the supplied persistence + * object by the execution of the results of {@link #createInsert}. The + * structure of primary key assignment will probably have to change when we + * support other databases. + */ + public void assignPrimaryKey (Connection conn, Object po) + throws SQLException + { + // no primary key, no problem! + if (_primaryKey == null) { + return; + } + + // if the primary key is non-numeric, we can't auto-assign it + Class ftype = _primaryKey.getField().getType(); + if (!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)) { + return; + } + + // load up the last inserted ID mysql style; eventually this will be + // fancier and pluggable + Statement stmt = null; + try { + stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()"); + if (rs.next()) { + _primaryKey.getField().set(po, rs.getInt(1)); + } + + } catch (IllegalAccessException iae) { + String errmsg = "Failed to assign primary key " + + "[type=" + _pclass + "]"; + throw (SQLException)new SQLException(errmsg).initCause(iae); + + } finally { + JDBCUtil.close(stmt); + } + } + /** * Creates a statement that will update the supplied persistent object * using the supplied key. @@ -320,12 +439,37 @@ public class DepotMarshaller return pstmt; } + /** The persistent object class that we manage. */ protected Class _pclass; + + /** A field marshaller for each persistent field in our object. */ protected HashMap _fields = new HashMap(); - protected String _tableName; + /** The field marshaller for our persistent object's primary key or null if + * it did not define a primary key. */ protected FieldMarshaller _primaryKey; + + /** The name of our persistent object table. */ + protected String _tableName; + + /** The persisent fields of our object, in definition order, separated by + * commas for easy use in a select statement. */ protected String _fullColumnList; + + /** The persisent fields of our object, in definition order. */ protected String[] _allFields; + + /** The version of our persistent object schema as specified in the class + * definition. */ + protected int _schemaVersion = -1; + + /** Used when creating and migrating our table schema. */ + protected String[] _columnDefinitions; + + /** Used when creating and migrating our table schema. */ + protected String _postamble; + + /** The name of the table we use to track schema versions. */ + protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion"; } diff --git a/src/java/com/samskivert/jdbc/depot/DepotRepository.java b/src/java/com/samskivert/jdbc/depot/DepotRepository.java index d326869..bb7f520 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java +++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java @@ -59,9 +59,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(type); return invoke(new Query(key) { - public T invoke (Connection conn) - throws SQLException, PersistenceException - { + public T invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createQuery(conn, _key); try { T result = null; @@ -89,9 +87,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(type); return invoke(new Query>(key) { - public ArrayList invoke (Connection conn) - throws SQLException, PersistenceException - { + public ArrayList invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createQuery(conn, _key); try { ArrayList results = new ArrayList(); @@ -131,13 +127,11 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(record.getClass()); return invoke(new Modifier(marsh.getPrimaryKey(record)) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createInsert(conn, record); try { int mods = stmt.executeUpdate(); - // TODO: assign primary key + marsh.assignPrimaryKey(conn, record); return mods; } finally { stmt.close(); @@ -157,9 +151,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(record.getClass()); return invoke(new Modifier(marsh.getPrimaryKey(record)) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createUpdate(conn, record, _key); try { return stmt.executeUpdate(); @@ -181,9 +173,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(record.getClass()); return invoke(new Modifier(marsh.getPrimaryKey(record)) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createUpdate( conn, record, _key, modifiedFields); try { @@ -239,9 +229,7 @@ public class DepotRepository final DepotMarshaller marsh = getMarshaller(type); return invoke(new Modifier(key) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createPartialUpdate( conn, _key, fields, values); try { @@ -313,9 +301,7 @@ public class DepotRepository final DepotMarshaller marsh = getMarshaller(type); return invoke(new Modifier(key) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createLiteralUpdate( conn, _key, fields, values); try { @@ -341,9 +327,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(record.getClass()); return invoke(new Modifier(marsh.getPrimaryKey(record)) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = null; try { // if our primary key is null or is the integer 0, assume @@ -360,7 +344,9 @@ public class DepotRepository // if the update modified zero rows or the primary key was // obviously unset, do an insertion stmt = marsh.createInsert(conn, record); - return stmt.executeUpdate(); + int mods = stmt.executeUpdate(); + marsh.assignPrimaryKey(conn, record); + return mods; } finally { stmt.close(); @@ -407,9 +393,7 @@ public class DepotRepository { final DepotMarshaller marsh = getMarshaller(type); return invoke(new Modifier(key) { - public int invoke (Connection conn) - throws SQLException - { + public int invoke (Connection conn) throws SQLException { PreparedStatement stmt = marsh.createDelete(conn, _key); try { return stmt.executeUpdate(); @@ -421,11 +405,21 @@ public class DepotRepository } protected DepotMarshaller getMarshaller (Class type) + throws PersistenceException { @SuppressWarnings("unchecked")DepotMarshaller marshaller = (DepotMarshaller)_marshallers.get(type); if (marshaller == null) { _marshallers.put(type, marshaller = new DepotMarshaller(type)); + // initialize the marshaller which may create or migrate the table + // for its underlying persistent object + final DepotMarshaller fm = marshaller; + invoke(new Modifier(null) { + public int invoke (Connection conn) throws SQLException { + fm.init(conn); + return 0; + } + }); } return marshaller; } @@ -530,8 +524,7 @@ public class DepotRepository return _key; } - public abstract T invoke (Connection conn) - throws SQLException, PersistenceException; + public abstract T invoke (Connection conn) throws SQLException; protected Query (Key key) { @@ -543,8 +536,7 @@ public class DepotRepository protected static abstract class CollectionQuery { - public abstract T invoke (Connection conn) - throws SQLException, PersistenceException; + public abstract T invoke (Connection conn) throws SQLException; } protected static abstract class Modifier @@ -554,8 +546,7 @@ public class DepotRepository return _key; } - public abstract int invoke (Connection conn) - throws SQLException, PersistenceException; + public abstract int invoke (Connection conn) throws SQLException; protected Modifier (Key key) { diff --git a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java index b34c8d0..17fa10a 100644 --- a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java @@ -29,6 +29,13 @@ import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import com.samskivert.util.StringUtil; + /** * Handles the marshalling and unmarshalling of a particular field of a * persistent object. @@ -91,6 +98,30 @@ public abstract class FieldMarshaller return marshaller; } + /** + * Returns the {@link Field} handled by this marshaller. + */ + public Field getField () + { + return _field; + } + + /** + * Returns the name of the table column used to store this field. + */ + public String getColumnName () + { + return _columnName; + } + + /** + * Returns the SQL used to define this field's column. + */ + public String getColumnDefinition () + { + return _columnDefinition; + } + /** * Reads the value of our field from the persistent object and sets that * value into the specified column of the supplied prepared statement. @@ -106,24 +137,82 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException; /** - * Returns the name of the table column used to store this field. + * Returns the type used in the SQL column definition for this field. */ - public String getColumnName () - { - return _field.getName(); - } - - /** - * Returns the {@link Field} handled by this marshaller. - */ - public Field getField () - { - return _field; - } + public abstract String getColumnType (); protected void init (Field field) { _field = field; + _columnName = field.getName(); + + // read our column metadata from the annotation (if it exists); + // annoyingly we can't create a Column instance to read the defaults so + // we have to duplicate them here + int length = 255; + boolean nullable = true; + boolean unique = false; + Column column = _field.getAnnotation(Column.class); + if (column != null) { + nullable = column.nullable(); + unique = column.unique(); + length = column.length(); + if (!StringUtil.isBlank(column.name())) { + _columnName = column.name(); + } + } + + // create our SQL column definition + StringBuilder builder = new StringBuilder(); + if (column != null && !StringUtil.isBlank(column.columnDefinition())) { + builder.append(column.columnDefinition()); + + } else { + builder.append(getColumnName()); + String type = getColumnType(); + builder.append(" ").append(type); + + // if this is a VARCHAR field, add the length + if (type.equals("VARCHAR")) { + builder.append("(").append(length).append(")"); + } + + // TODO: handle precision and scale + + // handle nullability and uniqueness + if (!nullable) { + builder.append(" NOT NULL"); + } + if (unique) { + builder.append(" UNIQUE"); + } + } + + // handle primary keyness + if (field.getAnnotation(Id.class) != null) { + builder.append(" PRIMARY KEY"); + + // figure out how we're going to generate our primary key values + GeneratedValue gv = field.getAnnotation(GeneratedValue.class); + GenerationType strat = GenerationType.AUTO; + if (gv != null) { + strat = gv.strategy(); + } + switch (strat) { + case AUTO: + case IDENTITY: + builder.append(" AUTO_INCREMENT"); + break; + case SEQUENCE: // TODO + throw new IllegalArgumentException( + "TABLE key generation strategy not yet supported."); + case TABLE: // TODO + throw new IllegalArgumentException( + "TABLE key generation strategy not yet supported."); + } + } + + _columnDefinition = builder.toString(); } protected static class PBooleanMarshaller extends FieldMarshaller { @@ -135,6 +224,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setBoolean(po, rs.getBoolean(getColumnName())); } + public String getColumnType () { + return "TINYINT"; + } } protected static class PByteMarshaller extends FieldMarshaller { @@ -146,6 +238,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setByte(po, rs.getByte(getColumnName())); } + public String getColumnType () { + return "TINYINT"; + } } protected static class PShortMarshaller extends FieldMarshaller { @@ -157,6 +252,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setShort(po, rs.getShort(getColumnName())); } + public String getColumnType () { + return "SMALLINT"; + } } protected static class PIntMarshaller extends FieldMarshaller { @@ -168,6 +266,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setInt(po, rs.getInt(getColumnName())); } + public String getColumnType () { + return "INTEGER"; + } } protected static class PLongMarshaller extends FieldMarshaller { @@ -179,6 +280,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setLong(po, rs.getLong(getColumnName())); } + public String getColumnType () { + return "BIGINT"; + } } protected static class PFloatMarshaller extends FieldMarshaller { @@ -190,6 +294,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setFloat(po, rs.getFloat(getColumnName())); } + public String getColumnType () { + return "FLOAT"; + } } protected static class PDoubleMarshaller extends FieldMarshaller { @@ -201,6 +308,9 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.setDouble(po, rs.getDouble(getColumnName())); } + public String getColumnType () { + return "DOUBLE"; + } } protected static class ObjectMarshaller extends FieldMarshaller { @@ -212,7 +322,39 @@ public abstract class FieldMarshaller throws SQLException, IllegalAccessException { _field.set(po, rs.getObject(getColumnName())); } + public String getColumnType () { + Class ftype = _field.getType(); + if (ftype.equals(Byte.class)) { + return "TINYINT"; + } else if (ftype.equals(Short.class)) { + return "SMALLINT"; + } else if (ftype.equals(Integer.class)) { + return "INTEGER"; + } else if (ftype.equals(Long.class)) { + return "BIGINT"; + } else if (ftype.equals(Float.class)) { + return "FLOAT"; + } else if (ftype.equals(Double.class)) { + return "DOUBLE"; + } else if (ftype.equals(String.class)) { + return "VARCHAR"; + } else if (ftype.equals(Date.class)) { + return "DATE"; + } else if (ftype.equals(Time.class)) { + return "DATETIME"; + } else if (ftype.equals(Timestamp.class)) { + return "TIMESTAMP"; + } else if (ftype.equals(Blob.class)) { + return "BLOB"; + } else if (ftype.equals(Clob.class)) { + return "CLOB"; + } else { + throw new IllegalArgumentException( + "Don't know how to create SQL for " + ftype + "."); + } + } } protected Field _field; + protected String _columnName, _columnDefinition; }