From 8d28bc740c9bcb76186e7572f74a144397e780ce Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 29 Oct 2014 17:12:14 -0400 Subject: [PATCH] Support data type adapters This allows custom types to be (de)serialized into a standard JDBC type or to support db-specific data types, like the Postgres json and xml types. --- releases.moxie | 9 +- src/main/java/com/iciql/Db.java | 60 ++++++------ src/main/java/com/iciql/Define.java | 20 ++-- src/main/java/com/iciql/Iciql.java | 78 +++++++++++++++ .../iciql/JavaSerializationTypeAdapter.java | 98 +++++++++++++++++++ src/main/java/com/iciql/Query.java | 16 ++- src/main/java/com/iciql/SQLDialect.java | 66 +++++++++---- .../java/com/iciql/SQLDialectDefault.java | 50 +++++++++- src/main/java/com/iciql/SQLDialectH2.java | 5 +- src/main/java/com/iciql/SQLDialectHSQL.java | 5 +- src/main/java/com/iciql/SQLDialectMySQL.java | 7 +- .../java/com/iciql/SQLDialectPostgreSQL.java | 75 +++++++++++++- src/main/java/com/iciql/SQLStatement.java | 2 +- src/main/java/com/iciql/TableDefinition.java | 43 ++++++-- src/site/jaqu_comparison.mkd | 1 + src/site/model_classes.mkd | 5 + .../com/iciql/test/DataTypeAdapterTest.java | 94 ++++++++++++++++++ src/test/java/com/iciql/test/IciqlSuite.java | 5 +- .../com/iciql/test/models/SupportedTypes.java | 5 +- 19 files changed, 557 insertions(+), 87 deletions(-) create mode 100644 src/main/java/com/iciql/JavaSerializationTypeAdapter.java create mode 100644 src/test/java/com/iciql/test/DataTypeAdapterTest.java diff --git a/releases.moxie b/releases.moxie index c0e2c50..887eb08 100644 --- a/releases.moxie +++ b/releases.moxie @@ -11,9 +11,14 @@ r22: { security: ~ fixes: ~ changes: ~ - additions: ~ + additions: + - Support for specifying custom data type adapters in @IQColumn and Define.typeAdapter() + - Added com.iciql.SQLDialectPostgreSQL.JsonStringAdapter + - Added com.iciql.SQLDialectPostgreSQL.XmlStringAdapter + - Added com.iciql.JavaSerializationTypeAdapter to (de)serialize objects into a BLOB column dependencyChanges: ~ - contributors: ~ + contributors: + - James Moger } # diff --git a/src/main/java/com/iciql/Db.java b/src/main/java/com/iciql/Db.java index ecd373c..179b18d 100644 --- a/src/main/java/com/iciql/Db.java +++ b/src/main/java/com/iciql/Db.java @@ -62,7 +62,7 @@ public class Db { private static final Map TOKENS; private static final Map> DIALECTS; - + private final Connection conn; private final Map, TableDefinition> classMap = Collections .synchronizedMap(new HashMap, TableDefinition>()); @@ -72,7 +72,7 @@ public class Db { private boolean skipCreate; private boolean autoSavePoint = true; - + static { TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap()); DIALECTS = Collections.synchronizedMap(new HashMap>()); @@ -104,7 +104,7 @@ public class Db { /** * Register a new/custom dialect class. You can use this method to replace * any existing dialect or to add a new one. - * + * * @param token * the fully qualified name of the connection class or the * expected result of DatabaseMetaData.getDatabaseProductName() @@ -146,7 +146,7 @@ public class Db { throw new IciqlException(e); } } - + public static Db open(String url) { try { Connection conn = JdbcUtils.getConnection(null, url, null, null); @@ -164,7 +164,7 @@ public class Db { throw new IciqlException(e); } } - + public static Db open(String url, String user, char[] password) { try { Connection conn = JdbcUtils.getConnection(null, url, user, password == null ? null : new String(password)); @@ -177,7 +177,7 @@ public class Db { /** * Create a new database instance using a data source. This method is fast, * so that you can always call open() / close() on usage. - * + * * @param ds * the data source * @return the database instance. @@ -194,8 +194,8 @@ public class Db { return new Db(conn); } - - + + /** * Convenience function to avoid import statements in application code. */ @@ -227,7 +227,7 @@ public class Db { * Merge INSERTS if the record does not exist or UPDATES the record if it * does exist. Not all databases support MERGE and the syntax varies with * the database. - * + * * If the database does not support a MERGE syntax the dialect can try to * simulate a merge by implementing: *

@@ -239,7 +239,7 @@ public class Db { * See the Derby dialect for an implementation of this technique. *

* If the dialect does not support merge an IciqlException will be thrown. - * + * * @param t */ public void merge(T t) { @@ -324,7 +324,7 @@ public class Db { int[] columns = def.mapColumns(wildcardSelect, rs); while (rs.next()) { T item = Utils.newObject(modelClass); - def.readRow(item, rs, columns); + def.readRow(dialect, item, rs, columns); result.add(item); } } catch (SQLException e) { @@ -417,7 +417,7 @@ public class Db { if (def == null) { upgradeDb(); def = new TableDefinition(clazz); - def.mapFields(); + def.mapFields(this); classMap.put(clazz, def); if (Iciql.class.isAssignableFrom(clazz)) { T t = instance(clazz); @@ -437,7 +437,7 @@ public class Db { } return def; } - + boolean hasCreated(Class clazz) { return upgradeChecked.contains(clazz); } @@ -567,7 +567,7 @@ public class Db { throw IciqlException.fromSQL(sql, e); } } - + Savepoint prepareSavepoint() { // don't change auto-commit mode. // don't create save point. @@ -580,13 +580,13 @@ public class Db { conn.setAutoCommit(false); savepoint = conn.setSavepoint(); } catch (SQLFeatureNotSupportedException e) { - // jdbc driver does not support save points + // jdbc driver does not support save points } catch (SQLException e) { throw new IciqlException(e, "Could not create save point"); } return savepoint; } - + void commit(Savepoint savepoint) { if (savepoint != null) { try { @@ -597,7 +597,7 @@ public class Db { } } } - + void rollback(Savepoint savepoint) { if (savepoint != null) { try { @@ -616,13 +616,13 @@ public class Db { /** * Run a SQL query directly against the database. - * + * * Be sure to close the ResultSet with - * + * *

 	 * JdbcUtils.closeSilently(rs, true);
 	 * 
- * + * * @param sql * the SQL statement * @param args @@ -635,13 +635,13 @@ public class Db { /** * Run a SQL query directly against the database. - * + * * Be sure to close the ResultSet with - * + * *
 	 * JdbcUtils.closeSilently(rs, true);
 	 * 
- * + * * @param sql * the SQL statement * @param args @@ -668,7 +668,7 @@ public class Db { /** * Run a SQL query directly against the database and map the results to the * model class. - * + * * @param modelClass * the model class to bind the query ResultSet rows into. * @param sql @@ -682,7 +682,7 @@ public class Db { /** * Run a SQL query directly against the database and map the results to the * model class. - * + * * @param modelClass * the model class to bind the query ResultSet rows into. * @param sql @@ -714,7 +714,7 @@ public class Db { /** * Run a SQL statement directly against the database. - * + * * @param sql * the SQL statement * @return the update count @@ -727,14 +727,14 @@ public class Db { stat = conn.createStatement(); updateCount = stat.executeUpdate(sql); } else { - PreparedStatement ps = conn.prepareStatement(sql); + PreparedStatement ps = conn.prepareStatement(sql); int i = 1; for (Object arg : args) { ps.setObject(i++, arg); } updateCount = ps.executeUpdate(); stat = ps; - } + } return updateCount; } catch (SQLException e) { throw new IciqlException(e); @@ -756,7 +756,7 @@ public class Db { public boolean getSkipCreate() { return this.skipCreate; } - + /** * Allow to enable/disable usage of save point. * For advanced user wanting to gain full control of transactions. @@ -770,5 +770,5 @@ public class Db { public boolean getAutoSavePoint() { return this.autoSavePoint; } - + } diff --git a/src/main/java/com/iciql/Define.java b/src/main/java/com/iciql/Define.java index 1810a4b..b16ee6e 100644 --- a/src/main/java/com/iciql/Define.java +++ b/src/main/java/com/iciql/Define.java @@ -18,6 +18,7 @@ package com.iciql; +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.Iciql.IndexType; /** @@ -49,7 +50,7 @@ public class Define { checkInDefine(); currentTableDefinition.defineConstraintUnique(name, columns); } - + /* * The variable argument type Object can't be used twice :-) */ @@ -59,12 +60,12 @@ public class Define { // checkInDefine(); // currentTableDefinition.defineForeignKey(name, columns, refTableName, Columns, deleteType, updateType, deferrabilityType); // } - + public static void primaryKey(Object... columns) { checkInDefine(); currentTableDefinition.definePrimaryKey(columns); } - + public static void schemaName(String schemaName) { checkInDefine(); currentTableDefinition.defineSchemaName(schemaName); @@ -79,7 +80,7 @@ public class Define { checkInDefine(); currentTableDefinition.defineViewTableName(viewTableName); } - + public static void memoryTable() { checkInDefine(); currentTableDefinition.defineMemoryTable(); @@ -104,17 +105,17 @@ public class Define { checkInDefine(); currentTableDefinition.defineScale(column, scale); } - + public static void trim(Object column) { checkInDefine(); currentTableDefinition.defineTrim(column); } - + public static void nullable(Object column, boolean isNullable) { checkInDefine(); currentTableDefinition.defineNullable(column, isNullable); } - + public static void defaultValue(Object column, String defaultValue) { checkInDefine(); currentTableDefinition.defineDefaultValue(column, defaultValue); @@ -125,6 +126,11 @@ public class Define { currentTableDefinition.defineConstraint(column, constraint); } + public static void typeAdapter(Object column, Class> typeAdapter) { + checkInDefine(); + currentTableDefinition.defineTypeAdapter(column, typeAdapter); + } + static synchronized void define(TableDefinition tableDefinition, Iciql table) { currentTableDefinition = tableDefinition; currentTable = table; diff --git a/src/main/java/com/iciql/Iciql.java b/src/main/java/com/iciql/Iciql.java index 521e460..05cceeb 100644 --- a/src/main/java/com/iciql/Iciql.java +++ b/src/main/java/com/iciql/Iciql.java @@ -657,6 +657,14 @@ public interface Iciql { */ String defaultValue() default ""; + /** + * Allows specifying a custom data type adapter. + *

+ * For example, you might use this option to map a Postgres JSON column. + *

+ */ + Class> typeAdapter() default StandardJDBCTypeAdapter.class; + } /** @@ -731,4 +739,74 @@ public interface Iciql { * and the table name. */ void defineIQ(); + + /** + * Interface to allow implementations of custom data type adapters for supporting + * database-specific data types, like the Postgres 'json' or 'xml' types, + * or for supporting other object serialization schemes. + *

NOTE: Data type adapters are not thread-safe!

+ * + * @param + */ + public interface DataTypeAdapter { + + /** + * The SQL data type for this adapter. + * + * @return the SQL data type + */ + String getDataType(); + + /** + * The Java domain type for this adapter. + * + * @return the Java domain type + */ + Class getJavaType(); + + /** + * Serializes your Java object into a JDBC object. + * + * @param value + * @return a JDBC object + */ + Object serialize(T value); + + /** + * Deserializes a JDBC object into your Java object. + * + * @param value + * @return the Java object + */ + T deserialize(Object value); + + } + + /** + * The standard type adapter allows iciql to use it's internal utility convert functions + * to handle the standard JDBC types. + */ + public final static class StandardJDBCTypeAdapter implements DataTypeAdapter { + + @Override + public String getDataType() { + throw new RuntimeException("This adapter is for all standard JDBC types."); + } + + @Override + public Class getJavaType() { + throw new RuntimeException("This adapter is for all standard JDBC types."); + } + + @Override + public Object serialize(Object value) { + return value; + } + + @Override + public Object deserialize(Object value) { + return value; + } + + } } diff --git a/src/main/java/com/iciql/JavaSerializationTypeAdapter.java b/src/main/java/com/iciql/JavaSerializationTypeAdapter.java new file mode 100644 index 0000000..e38e0f8 --- /dev/null +++ b/src/main/java/com/iciql/JavaSerializationTypeAdapter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2014 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.sql.Blob; +import java.sql.SQLException; + +import com.iciql.Iciql.DataTypeAdapter; + +/** + * Base class for inserting/retrieving a Java Object as a BLOB field using Java Serialization. + *

You use this by creating a subclass which defines your object class.

+ *
+ * public class CustomObjectAdapter extends JavaSerializationTypeAdapter<CustomObject> {
+ *
+ *    public Class<CustomObject> getJavaType() {
+ *        return CustomObject.class;
+ *    }
+ * }
+ * 
+ * @param + */ +public abstract class JavaSerializationTypeAdapter implements DataTypeAdapter { + + @Override + public final String getDataType() { + return "BLOB"; + } + + @Override + public final Object serialize(T value) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + new ObjectOutputStream(os).writeObject(value); + return os.toByteArray(); + } catch (IOException e) { + throw new IciqlException(e); + } finally { + try { + os.close(); + } catch (IOException e) { + throw new IciqlException (e); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public final T deserialize(Object value) { + InputStream is = null; + if (value instanceof Blob) { + Blob blob = (Blob) value; + try { + is = blob.getBinaryStream(); + } catch (SQLException e) { + throw new IciqlException(e); + } + } else if (value instanceof byte[]) { + byte [] bytes = (byte []) value; + is = new ByteArrayInputStream(bytes); + } + + try { + T object = (T) new ObjectInputStream(is).readObject(); + return object; + } catch (Exception e) { + throw new IciqlException(e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + throw new IciqlException (e); + } + } + } + } +} diff --git a/src/main/java/com/iciql/Query.java b/src/main/java/com/iciql/Query.java index f22215c..7c5c985 100644 --- a/src/main/java/com/iciql/Query.java +++ b/src/main/java/com/iciql/Query.java @@ -28,9 +28,9 @@ import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; +import com.iciql.Iciql.EnumType; import com.iciql.NestedConditions.And; import com.iciql.NestedConditions.Or; -import com.iciql.Iciql.EnumType; import com.iciql.bytecode.ClassReader; import com.iciql.util.IciqlLogger; import com.iciql.util.JdbcUtils; @@ -253,7 +253,7 @@ public class Query { int[] columns = def.mapColumns(false, rs); while (rs.next()) { T item = from.newObject(); - def.readRow(item, rs, columns); + def.readRow(db.getDialect(), item, rs, columns); result.add(item); } } catch (SQLException e) { @@ -406,7 +406,7 @@ public class Query { int[] columns = def.mapColumns(false, rs); while (rs.next()) { X row = Utils.newObject(clazz); - def.readRow(row, rs, columns); + def.readRow(db.getDialect(), row, rs, columns); result.add(row); } } catch (SQLException e) { @@ -841,13 +841,19 @@ public class Query { } private void addParameter(SQLStatement stat, Object alias, Object value) { - if (alias != null && value.getClass().isEnum()) { - SelectColumn col = getColumnByReference(alias); + SelectColumn col = getColumnByReference(alias); + if (col != null && value.getClass().isEnum()) { + // enum EnumType type = col.getFieldDefinition().enumType; Enum anEnum = (Enum) value; Object y = Utils.convertEnum(anEnum, type); stat.addParameter(y); + } else if (col != null) { + // object + Object parameter = db.getDialect().serialize(value, col.getFieldDefinition().typeAdapter); + stat.addParameter(parameter); } else { + // primitive stat.addParameter(value); } } diff --git a/src/main/java/com/iciql/SQLDialect.java b/src/main/java/com/iciql/SQLDialect.java index f62168e..8937baf 100644 --- a/src/main/java/com/iciql/SQLDialect.java +++ b/src/main/java/com/iciql/SQLDialect.java @@ -20,6 +20,7 @@ package com.iciql; import java.sql.DatabaseMetaData; +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.TableDefinition.ConstraintForeignKeyDefinition; import com.iciql.TableDefinition.ConstraintUniqueDefinition; import com.iciql.TableDefinition.IndexDefinition; @@ -30,9 +31,35 @@ import com.iciql.TableDefinition.IndexDefinition; */ public interface SQLDialect { + /** + * Returns the registered instance of the type adapter. + * + * @param typeAdapter + * @return the type adapter instance + */ + DataTypeAdapter getTypeAdapter(Class> typeAdapter); + + /** + * Serialize the Java object into a type or format that the database will accept. + * + * @param value + * @param typeAdapter + * @return the serialized object + */ + Object serialize(T value, Class> typeAdapter); + + /** + * Deserialize the object received from the database into a Java type. + * + * @param value + * @param typeAdapter + * @return the deserialized object + */ + Object deserialize(Object value, Class> typeAdapter); + /** * Configure the dialect from the database metadata. - * + * * @param databaseName * @param data */ @@ -40,7 +67,7 @@ public interface SQLDialect { /** * Allows a dialect to substitute an SQL type. - * + * * @param sqlType * @return the dialect-safe type */ @@ -48,7 +75,7 @@ public interface SQLDialect { /** * Returns a properly formatted table name for the dialect. - * + * * @param schemaName * the schema name, or null for no schema * @param tableName @@ -59,7 +86,7 @@ public interface SQLDialect { /** * Returns a properly formatted column name for the dialect. - * + * * @param name * the column name * @return the properly formatted column name @@ -68,7 +95,7 @@ public interface SQLDialect { /** * Get the CREATE TABLE statement. - * + * * @param stat * @param def */ @@ -76,16 +103,16 @@ public interface SQLDialect { /** * Get the DROP TABLE statement. - * + * * @param stat * @param def */ void prepareDropTable(SQLStatement stat, TableDefinition def); - + /** * Get the CREATE VIEW statement. - * + * * @param stat * return the SQL statement * @param def @@ -95,7 +122,7 @@ public interface SQLDialect { /** * Get the CREATE VIEW statement. - * + * * @param stat * return the SQL statement * @param def @@ -106,17 +133,17 @@ public interface SQLDialect { /** * Get the DROP VIEW statement. - * + * * @param stat * return the SQL statement * @param def * table definition */ void prepareDropView(SQLStatement stat, TableDefinition def); - + /** * Get the CREATE INDEX statement. - * + * * @param stat * return the SQL statement * @param schemaName @@ -130,7 +157,7 @@ public interface SQLDialect { /** * Get the ALTER statement. - * + * * @param stat * return the SQL statement * @param schemaName @@ -144,7 +171,7 @@ public interface SQLDialect { /** * Get the ALTER statement. - * + * * @param stat * return the SQL statement * @param schemaName @@ -159,7 +186,7 @@ public interface SQLDialect { /** * Get a MERGE or REPLACE INTO statement. - * + * * @param stat * return the SQL statement * @param schemaName @@ -176,7 +203,7 @@ public interface SQLDialect { /** * Append "LIMIT limit OFFSET offset" to the SQL statement. - * + * * @param stat * the statement * @param limit @@ -190,7 +217,7 @@ public interface SQLDialect { * Returns the preferred DATETIME class for the database. *

* Either java.util.Date or java.sql.Timestamp - * + * * @return preferred DATETIME class */ Class getDateTimeClass(); @@ -198,9 +225,10 @@ public interface SQLDialect { /** * When building static string statements this method flattens an object to * a string representation suitable for a static string statement. - * + * * @param o * @return the string equivalent of this object */ - String prepareParameter(Object o); + String prepareStringParameter(Object o); + } diff --git a/src/main/java/com/iciql/SQLDialectDefault.java b/src/main/java/com/iciql/SQLDialectDefault.java index e29d7b8..7789412 100644 --- a/src/main/java/com/iciql/SQLDialectDefault.java +++ b/src/main/java/com/iciql/SQLDialectDefault.java @@ -22,9 +22,12 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.text.MessageFormat; import java.text.SimpleDateFormat; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import com.iciql.Iciql.ConstraintDeleteType; import com.iciql.Iciql.ConstraintUpdateType; +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.TableDefinition.ConstraintForeignKeyDefinition; import com.iciql.TableDefinition.ConstraintUniqueDefinition; import com.iciql.TableDefinition.FieldDefinition; @@ -32,6 +35,7 @@ import com.iciql.TableDefinition.IndexDefinition; import com.iciql.util.IciqlLogger; import com.iciql.util.StatementBuilder; import com.iciql.util.StringUtils; +import com.iciql.util.Utils; /** * Default implementation of an SQL dialect. @@ -45,6 +49,11 @@ public class SQLDialectDefault implements SQLDialect { int databaseMinorVersion; String databaseName; String productVersion; + Map>, DataTypeAdapter> typeAdapters; + + public SQLDialectDefault() { + typeAdapters = new ConcurrentHashMap>, DataTypeAdapter>(); + } @Override public String toString() { @@ -305,7 +314,8 @@ public class SQLDialectDefault implements SQLDialect { buff.appendExceptFirst(", "); buff.append('?'); Object value = def.getValue(obj, field); - stat.addParameter(value); + Object parameter = serialize(value, field.typeAdapter); + stat.addParameter(parameter); } buff.append(" FROM "); buff.append(prepareTableName(schemaName, tableName)); @@ -316,7 +326,8 @@ public class SQLDialectDefault implements SQLDialect { buff.appendExceptFirst(" AND "); buff.append(MessageFormat.format("{0} = ?", prepareColumnName(field.columnName))); Object value = def.getValue(obj, field); - stat.addParameter(value); + Object parameter = serialize(value, field.typeAdapter); + stat.addParameter(parameter); } } buff.append(" HAVING count(*)=0)"); @@ -334,7 +345,40 @@ public class SQLDialectDefault implements SQLDialect { } @Override - public String prepareParameter(Object o) { + public DataTypeAdapter getTypeAdapter(Class> typeAdapter) { + DataTypeAdapter dtt = typeAdapters.get(typeAdapter); + if (dtt == null) { + dtt = Utils.newObject(typeAdapter); + typeAdapters.put(typeAdapter, dtt); + } + return dtt; + } + + @SuppressWarnings("unchecked") + @Override + public Object serialize(T value, Class> typeAdapter) { + if (typeAdapter == null) { + // pass-through + return value; + } + + DataTypeAdapter dtt = (DataTypeAdapter) getTypeAdapter(typeAdapter); + return dtt.serialize(value); + } + + @Override + public Object deserialize(Object value, Class> typeAdapter) { + DataTypeAdapter dtt = typeAdapters.get(typeAdapter); + if (dtt == null) { + dtt = Utils.newObject(typeAdapter); + typeAdapters.put(typeAdapter, dtt); + } + + return dtt.deserialize(value); + } + + @Override + public String prepareStringParameter(Object o) { if (o instanceof String) { return LITERAL + o.toString().replace(LITERAL, "''") + LITERAL; } else if (o instanceof Character) { diff --git a/src/main/java/com/iciql/SQLDialectH2.java b/src/main/java/com/iciql/SQLDialectH2.java index 6b3bab1..2d7d0fd 100644 --- a/src/main/java/com/iciql/SQLDialectH2.java +++ b/src/main/java/com/iciql/SQLDialectH2.java @@ -37,7 +37,7 @@ public class SQLDialectH2 extends SQLDialectDefault { return "CREATE CACHED TABLE IF NOT EXISTS"; } } - + @Override protected String prepareCreateView(TableDefinition def) { return "CREATE VIEW IF NOT EXISTS"; @@ -127,7 +127,8 @@ public class SQLDialectH2 extends SQLDialectDefault { buff.appendExceptFirst(", "); buff.append('?'); Object value = def.getValue(obj, field); - stat.addParameter(value); + Object parameter = serialize(value, field.typeAdapter); + stat.addParameter(parameter); } buff.append(')'); stat.setSQL(buff.toString()); diff --git a/src/main/java/com/iciql/SQLDialectHSQL.java b/src/main/java/com/iciql/SQLDialectHSQL.java index 82e6833..8b05ca4 100644 --- a/src/main/java/com/iciql/SQLDialectHSQL.java +++ b/src/main/java/com/iciql/SQLDialectHSQL.java @@ -38,7 +38,7 @@ public class SQLDialectHSQL extends SQLDialectDefault { return "CREATE CACHED TABLE IF NOT EXISTS"; } } - + @Override public void prepareDropView(SQLStatement stat, TableDefinition def) { StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS " @@ -91,7 +91,8 @@ public class SQLDialectHSQL extends SQLDialectDefault { } buff.append(')'); Object value = def.getValue(obj, field); - stat.addParameter(value); + Object parameter = serialize(value, field.typeAdapter); + stat.addParameter(parameter); } // map to temporary table diff --git a/src/main/java/com/iciql/SQLDialectMySQL.java b/src/main/java/com/iciql/SQLDialectMySQL.java index 52676d4..ec5923f 100644 --- a/src/main/java/com/iciql/SQLDialectMySQL.java +++ b/src/main/java/com/iciql/SQLDialectMySQL.java @@ -36,7 +36,7 @@ public class SQLDialectMySQL extends SQLDialectDefault { protected String prepareCreateTable(TableDefinition def) { return "CREATE TABLE IF NOT EXISTS"; } - + @Override public void prepareDropView(SQLStatement stat, TableDefinition def) { StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS " @@ -44,7 +44,7 @@ public class SQLDialectMySQL extends SQLDialectDefault { stat.setSQL(buff.toString()); return; } - + @Override public String prepareColumnName(String name) { return "`" + name + "`"; @@ -77,7 +77,8 @@ public class SQLDialectMySQL extends SQLDialectDefault { buff.appendExceptFirst(", "); buff.append('?'); Object value = def.getValue(obj, field); - stat.addParameter(value); + Object parameter = serialize(value, field.typeAdapter); + stat.addParameter(parameter); } buff.append(") ON DUPLICATE KEY UPDATE "); buff.resetCount(); diff --git a/src/main/java/com/iciql/SQLDialectPostgreSQL.java b/src/main/java/com/iciql/SQLDialectPostgreSQL.java index fc115ab..b5ac5c3 100644 --- a/src/main/java/com/iciql/SQLDialectPostgreSQL.java +++ b/src/main/java/com/iciql/SQLDialectPostgreSQL.java @@ -16,6 +16,11 @@ package com.iciql; +import java.sql.SQLException; + +import org.postgresql.util.PGobject; + +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.TableDefinition.IndexDefinition; import com.iciql.util.StatementBuilder; @@ -46,7 +51,7 @@ public class SQLDialectPostgreSQL extends SQLDialectDefault { @Override protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType, - boolean isAutoIncrement, boolean isPrimaryKey) { + boolean isAutoIncrement, boolean isPrimaryKey) { String convertedType = convertSqlType(dataType); if (isIntegerType(dataType)) { if (isAutoIncrement) { @@ -63,7 +68,7 @@ public class SQLDialectPostgreSQL extends SQLDialectDefault { } return false; } - + @Override public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, IndexDefinition index) { @@ -100,4 +105,70 @@ public class SQLDialectPostgreSQL extends SQLDialectDefault { stat.setSQL(buff.toString().trim()); } + + /** + * Handles transforming raw strings to/from the Postgres JSON data type. + */ + public class JsonStringAdapter implements DataTypeAdapter { + + @Override + public String getDataType() { + return "json"; + } + + @Override + public Class getJavaType() { + return String.class; + } + + @Override + public Object serialize(String value) { + PGobject pg = new PGobject(); + pg.setType(getDataType()); + try { + pg.setValue(value); + } catch (SQLException e) { + // not thrown on base PGobject + } + return pg; + } + + @Override + public String deserialize(Object value) { + return value.toString(); + } + } + + /** + * Handles transforming raw strings to/from the Postgres XML data type. + */ + public class XmlStringAdapter implements DataTypeAdapter { + + @Override + public String getDataType() { + return "xml"; + } + + @Override + public Class getJavaType() { + return String.class; + } + + @Override + public Object serialize(String value) { + PGobject pg = new PGobject(); + pg.setType(getDataType()); + try { + pg.setValue(value); + } catch (SQLException e) { + // not thrown on base PGobject + } + return pg; + } + + @Override + public String deserialize(Object value) { + return value.toString(); + } + } } \ No newline at end of file diff --git a/src/main/java/com/iciql/SQLStatement.java b/src/main/java/com/iciql/SQLStatement.java index 394fc42..7eb0b04 100644 --- a/src/main/java/com/iciql/SQLStatement.java +++ b/src/main/java/com/iciql/SQLStatement.java @@ -97,7 +97,7 @@ public class SQLStatement { sb.append('?'); } else { // static parameter - sb.append(db.getDialect().prepareParameter(o)); + sb.append(db.getDialect().prepareStringParameter(o)); } i++; } diff --git a/src/main/java/com/iciql/TableDefinition.java b/src/main/java/com/iciql/TableDefinition.java index 0cb1ad2..4536695 100644 --- a/src/main/java/com/iciql/TableDefinition.java +++ b/src/main/java/com/iciql/TableDefinition.java @@ -33,6 +33,7 @@ import java.util.Set; import com.iciql.Iciql.ConstraintDeferrabilityType; import com.iciql.Iciql.ConstraintDeleteType; import com.iciql.Iciql.ConstraintUpdateType; +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.Iciql.EnumId; import com.iciql.Iciql.EnumType; import com.iciql.Iciql.IQColumn; @@ -50,6 +51,7 @@ import com.iciql.Iciql.IQTable; import com.iciql.Iciql.IQVersion; import com.iciql.Iciql.IQView; import com.iciql.Iciql.IndexType; +import com.iciql.Iciql.StandardJDBCTypeAdapter; import com.iciql.util.IciqlLogger; import com.iciql.util.StatementBuilder; import com.iciql.util.StringUtils; @@ -121,6 +123,7 @@ public class TableDefinition { Class enumTypeClass; boolean isPrimitive; String constraint; + Class> typeAdapter; Object getValue(Object obj) { try { @@ -132,11 +135,11 @@ public class TableDefinition { private Object initWithNewObject(Object obj) { Object o = Utils.newObject(field.getType()); - setValue(obj, o); + setValue(null, obj, o); return o; } - private void setValue(Object obj, Object o) { + private void setValue(SQLDialect dialect, Object obj, Object o) { try { if (!field.isAccessible()) { field.setAccessible(true); @@ -144,6 +147,8 @@ public class TableDefinition { Class targetType = field.getType(); if (targetType.isEnum()) { o = Utils.convertEnum(o, targetType, enumType); + } else if (dialect != null && typeAdapter != null) { + o = dialect.deserialize(o, typeAdapter); } else { o = Utils.convert(o, targetType); } @@ -407,7 +412,14 @@ public class TableDefinition { } } - void mapFields() { + void defineTypeAdapter(Object column, Class> typeAdapter) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.typeAdapter = typeAdapter; + } + } + + void mapFields(Db db) { boolean byAnnotationsOnly = false; boolean inheritColumns = false; if (clazz.isAnnotationPresent(IQTable.class)) { @@ -465,6 +477,9 @@ public class TableDefinition { Class enumTypeClass = null; String defaultValue = ""; String constraint = ""; + String dataType = null; + Class> typeAdapter = null; + // configure Java -> SQL enum mapping if (f.getType().isEnum()) { enumType = EnumType.DEFAULT_TYPE; @@ -517,6 +532,12 @@ public class TableDefinition { trim = col.trim(); nullable = col.nullable(); + if (col.typeAdapter() != null && col.typeAdapter() != StandardJDBCTypeAdapter.class) { + typeAdapter = col.typeAdapter(); + DataTypeAdapter dtt = db.getDialect().getTypeAdapter(col.typeAdapter()); + dataType = dtt.getDataType(); + } + // annotation overrides if (!StringUtils.isNullOrEmpty(col.defaultValue())) { defaultValue = col.defaultValue(); @@ -547,7 +568,8 @@ public class TableDefinition { fieldDef.defaultValue = defaultValue; fieldDef.enumType = enumType; fieldDef.enumTypeClass = enumTypeClass; - fieldDef.dataType = ModelUtils.getDataType(fieldDef); + fieldDef.dataType = StringUtils.isNullOrEmpty(dataType) ? ModelUtils.getDataType(fieldDef) : dataType; + fieldDef.typeAdapter = typeAdapter; fieldDef.constraint = constraint; uniqueFields.add(fieldDef); } @@ -676,7 +698,8 @@ public class TableDefinition { // try to interpret and instantiate a default value value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass()); } - stat.addParameter(value); + Object parameter = db.getDialect().serialize(value, field.typeAdapter); + stat.addParameter(parameter); } buff.append(')'); stat.setSQL(buff.toString()); @@ -711,7 +734,8 @@ public class TableDefinition { // try to interpret and instantiate a default value value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass()); } - stat.addParameter(value); + Object parameter = db.getDialect().serialize(value, field.typeAdapter); + stat.addParameter(parameter); } buff.append(')'); stat.setSQL(buff.toString()); @@ -785,7 +809,8 @@ public class TableDefinition { buff.appendExceptFirst(", "); buff.append(db.getDialect().prepareColumnName(field.columnName)); buff.append(" = ?"); - stat.addParameter(value); + Object parameter = db.getDialect().serialize(value, field.typeAdapter); + stat.addParameter(parameter); } } Object alias = Utils.newObject(obj.getClass()); @@ -1193,12 +1218,12 @@ public class TableDefinition { return columns; } - void readRow(Object item, ResultSet rs, int[] columns) { + void readRow(SQLDialect dialect, Object item, ResultSet rs, int[] columns) { for (int i = 0; i < fields.size(); i++) { FieldDefinition def = fields.get(i); int index = columns[i]; Object o = def.read(rs, index); - def.setValue(item, o); + def.setValue(dialect, item, o); } } diff --git a/src/site/jaqu_comparison.mkd b/src/site/jaqu_comparison.mkd index b37addb..3a00e6d 100644 --- a/src/site/jaqu_comparison.mkd +++ b/src/site/jaqu_comparison.mkd @@ -26,6 +26,7 @@ This is an overview of the fundamental differences between the original JaQu pro DECIMAL(length,scale)can specify length/precision and scale-- BOOLEANflexible mapping of boolean as bool, varchar, or int-- BLOBpartially supported (can not be used in a WHERE clause)-- +Custompartially supported uses custom-defined data type adapter (can not be used in a WHERE clause)-- UUIDfully supported (H2 only) -- configuration DEFAULT valuesset from annotation, default object values, or Define.defaultValue()set from annotations diff --git a/src/site/model_classes.mkd b/src/site/model_classes.mkd index cb21dcd..747c094 100644 --- a/src/site/model_classes.mkd +++ b/src/site/model_classes.mkd @@ -73,6 +73,8 @@ can be used for all iciql expressions can not be directly referenced in an expression byte [] BLOB +Custom create a DataTypeAdapter<Custom> +Custom H2 Database Types
fully supported when paired with an H2 database @@ -180,6 +182,9 @@ public class Product { @IQColumn private Availability availability; + + @IQColumn(typeAdapter = MyCustomClassAdapter.class) + private MyCustomClass; // ignored because it is not annotated AND the class is @IQTable annotated private Integer ignoredField; diff --git a/src/test/java/com/iciql/test/DataTypeAdapterTest.java b/src/test/java/com/iciql/test/DataTypeAdapterTest.java new file mode 100644 index 0000000..f10d298 --- /dev/null +++ b/src/test/java/com/iciql/test/DataTypeAdapterTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2014 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import java.util.Date; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQTable; +import com.iciql.JavaSerializationTypeAdapter; +import com.iciql.test.models.SupportedTypes; + +/** + * Tests insertion and retrieval of a custom data type that is automatically transformed + * by a Java Object Serialization-based type adapter. + */ +public class DataTypeAdapterTest extends Assert { + + private Db db; + + + @Before + public void setUp() { + db = IciqlSuite.openNewDb(); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testSerializedObjectDataType() { + + SerializedObjectTypeAdapterTest row = new SerializedObjectTypeAdapterTest(); + row.received = new Date(); + row.obj = SupportedTypes.createList().get(1); + db.insert(row); + + SerializedObjectTypeAdapterTest table = new SerializedObjectTypeAdapterTest(); + SerializedObjectTypeAdapterTest q1 = db.from(table).selectFirst(); + + assertNotNull(q1); + assertTrue(row.obj.equivalentTo(q1.obj)); + + } + + @IQTable + public static class SerializedObjectTypeAdapterTest { + + @IQColumn(autoIncrement = true, primaryKey = true) + private long id; + + @IQColumn + private java.util.Date received; + + @IQColumn(typeAdapter = SupportedTypesAdapter.class) + private SupportedTypes obj; + + } + + /** + * Maps a SupportedType instance to a BLOB using Java Object serialization. + * + */ + public static class SupportedTypesAdapter extends JavaSerializationTypeAdapter { + + @Override + public Class getJavaType() { + return SupportedTypes.class; + } + + } + +} diff --git a/src/test/java/com/iciql/test/IciqlSuite.java b/src/test/java/com/iciql/test/IciqlSuite.java index c5d7ce8..3829501 100644 --- a/src/test/java/com/iciql/test/IciqlSuite.java +++ b/src/test/java/com/iciql/test/IciqlSuite.java @@ -48,6 +48,7 @@ import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameters; import com.iciql.Constants; import com.iciql.Db; +import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest; import com.iciql.test.models.BooleanModel; import com.iciql.test.models.CategoryAnnotationOnly; import com.iciql.test.models.ComplexObject; @@ -93,7 +94,8 @@ import com.iciql.util.Utils; @SuiteClasses({ AliasMapTest.class, AnnotationsTest.class, BooleanModelTest.class, ClobTest.class, ConcurrencyTest.class, EnumsTest.class, ModelsTest.class, PrimitivesTest.class, OneOfTest.class, RuntimeQueryTest.class, SamplesTest.class, UpdateTest.class, UpgradesTest.class, JoinTest.class, - UUIDTest.class, ViewsTest.class, ForeignKeyTest.class, TransactionTest.class, NestedConditionsTest.class }) + UUIDTest.class, ViewsTest.class, ForeignKeyTest.class, TransactionTest.class, NestedConditionsTest.class, + DataTypeAdapterTest.class }) public class IciqlSuite { private static final TestDb[] TEST_DBS = { @@ -191,6 +193,7 @@ public class IciqlSuite { db.dropTable(MultipleBoolsModel.class); db.dropTable(ProductAnnotationOnlyWithForeignKey.class); db.dropTable(CategoryAnnotationOnly.class); + db.dropTable(SerializedObjectTypeAdapterTest.class); return db; } diff --git a/src/test/java/com/iciql/test/models/SupportedTypes.java b/src/test/java/com/iciql/test/models/SupportedTypes.java index 9fa4fbc..489650e 100644 --- a/src/test/java/com/iciql/test/models/SupportedTypes.java +++ b/src/test/java/com/iciql/test/models/SupportedTypes.java @@ -17,6 +17,7 @@ package com.iciql.test.models; +import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; @@ -44,7 +45,9 @@ import com.iciql.util.Utils; @IQTable @IQIndexes({ @IQIndex({ "myLong", "myInteger" }), @IQIndex(type = IndexType.HASH, value = "myString") }) @IQVersion(1) -public class SupportedTypes { +public class SupportedTypes implements Serializable { + + private static final long serialVersionUID = 1L; public static final SupportedTypes SAMPLE = new SupportedTypes(); -- 2.39.5