diff options
author | James Moger <james.moger@gitblit.com> | 2014-10-29 17:12:14 -0400 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2014-10-29 17:12:14 -0400 |
commit | 8d28bc740c9bcb76186e7572f74a144397e780ce (patch) | |
tree | 1815e1d21df77e352ba2e8106557f71cb5561a8e /src | |
parent | bdb2899da4cbb27016d85c5e4fe268ddbccef546 (diff) | |
download | iciql-8d28bc740c9bcb76186e7572f74a144397e780ce.tar.gz iciql-8d28bc740c9bcb76186e7572f74a144397e780ce.zip |
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.
Diffstat (limited to 'src')
-rw-r--r-- | src/main/java/com/iciql/Db.java | 60 | ||||
-rw-r--r-- | src/main/java/com/iciql/Define.java | 20 | ||||
-rw-r--r-- | src/main/java/com/iciql/Iciql.java | 78 | ||||
-rw-r--r-- | src/main/java/com/iciql/JavaSerializationTypeAdapter.java | 98 | ||||
-rw-r--r-- | src/main/java/com/iciql/Query.java | 16 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialect.java | 66 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialectDefault.java | 50 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialectH2.java | 5 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialectHSQL.java | 5 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialectMySQL.java | 7 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLDialectPostgreSQL.java | 75 | ||||
-rw-r--r-- | src/main/java/com/iciql/SQLStatement.java | 2 | ||||
-rw-r--r-- | src/main/java/com/iciql/TableDefinition.java | 43 | ||||
-rw-r--r-- | src/site/jaqu_comparison.mkd | 1 | ||||
-rw-r--r-- | src/site/model_classes.mkd | 5 | ||||
-rw-r--r-- | src/test/java/com/iciql/test/DataTypeAdapterTest.java | 94 | ||||
-rw-r--r-- | src/test/java/com/iciql/test/IciqlSuite.java | 5 | ||||
-rw-r--r-- | src/test/java/com/iciql/test/models/SupportedTypes.java | 5 |
18 files changed, 550 insertions, 85 deletions
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<Object, Token> TOKENS;
private static final Map<String, Class<? extends SQLDialect>> DIALECTS;
-
+
private final Connection conn;
private final Map<Class<?>, TableDefinition<?>> classMap = Collections
.synchronizedMap(new HashMap<Class<?>, TableDefinition<?>>());
@@ -72,7 +72,7 @@ public class Db { private boolean skipCreate;
private boolean autoSavePoint = true;
-
+
static {
TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());
DIALECTS = Collections.synchronizedMap(new HashMap<String, Class<? extends SQLDialect>>());
@@ -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:
* <p>
@@ -239,7 +239,7 @@ public class Db { * See the Derby dialect for an implementation of this technique.
* <p>
* If the dialect does not support merge an IciqlException will be thrown.
- *
+ *
* @param t
*/
public <T> 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<T>(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;
}
-
+
<T> boolean hasCreated(Class<T> 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
- *
+ *
* <pre>
* JdbcUtils.closeSilently(rs, true);
* </pre>
- *
+ *
* @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
- *
+ *
* <pre>
* JdbcUtils.closeSilently(rs, true);
* </pre>
- *
+ *
* @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<? extends DataTypeAdapter<?>> typeAdapter) {
+ checkInDefine();
+ currentTableDefinition.defineTypeAdapter(column, typeAdapter);
+ }
+
static synchronized <T> void define(TableDefinition<T> 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.
+ * <p>
+ * For example, you might use this option to map a Postgres JSON column.
+ * </p>
+ */
+ Class<? extends DataTypeAdapter<?>> 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.
+ * <p><b>NOTE:</b> Data type adapters are not thread-safe!</p>
+ *
+ * @param <T>
+ */
+ public interface DataTypeAdapter<T> {
+
+ /**
+ * 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<T> 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<Object> {
+
+ @Override
+ public String getDataType() {
+ throw new RuntimeException("This adapter is for all standard JDBC types.");
+ }
+
+ @Override
+ public Class<Object> 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. + * <p>You use this by creating a subclass which defines your object class.</p> + * <pre> + * public class CustomObjectAdapter extends JavaSerializationTypeAdapter<CustomObject> { + * + * public Class<CustomObject> getJavaType() { + * return CustomObject.class; + * } + * } + * </pre> + * @param <T> + */ +public abstract class JavaSerializationTypeAdapter<T> implements DataTypeAdapter<T> { + + @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<T> { 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<T> { 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<T> { }
private void addParameter(SQLStatement stat, Object alias, Object value) {
- if (alias != null && value.getClass().isEnum()) {
- SelectColumn<T> col = getColumnByReference(alias);
+ SelectColumn<T> 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; @@ -31,8 +32,34 @@ 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<? extends DataTypeAdapter<?>> typeAdapter); + + /** + * Serialize the Java object into a type or format that the database will accept. + * + * @param value + * @param typeAdapter + * @return the serialized object + */ + <T> Object serialize(T value, Class<? extends DataTypeAdapter<?>> 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<? extends DataTypeAdapter<?>> 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 */ <T> void prepareDropTable(SQLStatement stat, TableDefinition<T> 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 */ <T> void prepareDropView(SQLStatement stat, TableDefinition<T> 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. * <p> * Either java.util.Date or java.sql.Timestamp - * + * * @return preferred DATETIME class */ Class<? extends java.util.Date> 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<Class<? extends DataTypeAdapter<?>>, DataTypeAdapter<?>> typeAdapters;
+
+ public SQLDialectDefault() {
+ typeAdapters = new ConcurrentHashMap<Class<? extends DataTypeAdapter<?>>, 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<? extends DataTypeAdapter<?>> typeAdapter) {
+ DataTypeAdapter<?> dtt = typeAdapters.get(typeAdapter);
+ if (dtt == null) {
+ dtt = Utils.newObject(typeAdapter);
+ typeAdapters.put(typeAdapter, dtt);
+ }
+ return dtt;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T> Object serialize(T value, Class<? extends DataTypeAdapter<?>> typeAdapter) {
+ if (typeAdapter == null) {
+ // pass-through
+ return value;
+ }
+
+ DataTypeAdapter<T> dtt = (DataTypeAdapter<T>) getTypeAdapter(typeAdapter);
+ return dtt.serialize(value);
+ }
+
+ @Override
+ public Object deserialize(Object value, Class<? extends DataTypeAdapter<?>> 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 <T> String prepareCreateView(TableDefinition<T> 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 <T> void prepareDropView(SQLStatement stat, TableDefinition<T> 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 <T> String prepareCreateTable(TableDefinition<T> def) {
return "CREATE TABLE IF NOT EXISTS";
}
-
+
@Override
public <T> void prepareDropView(SQLStatement stat, TableDefinition<T> 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<String> {
+
+ @Override
+ public String getDataType() {
+ return "json";
+ }
+
+ @Override
+ public Class<String> 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<String> {
+
+ @Override
+ public String getDataType() {
+ return "xml";
+ }
+
+ @Override
+ public Class<String> 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<T> { Class<?> enumTypeClass;
boolean isPrimitive;
String constraint;
+ Class<? extends DataTypeAdapter<?>> typeAdapter;
Object getValue(Object obj) {
try {
@@ -132,11 +135,11 @@ public class TableDefinition<T> { 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<T> { 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<T> { }
}
- void mapFields() {
+ void defineTypeAdapter(Object column, Class<? extends DataTypeAdapter<?>> 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<T> { Class<?> enumTypeClass = null;
String defaultValue = "";
String constraint = "";
+ String dataType = null;
+ Class<? extends DataTypeAdapter<?>> typeAdapter = null;
+
// configure Java -> SQL enum mapping
if (f.getType().isEnum()) {
enumType = EnumType.DEFAULT_TYPE;
@@ -517,6 +532,12 @@ public class TableDefinition<T> { 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<T> { 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<T> { // 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<T> { // 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<T> { 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<T> { 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 <tr><td>DECIMAL(length,scale)</td><td>can specify length/precision and scale</td><td>--</td></tr>
<tr><td>BOOLEAN</td><td>flexible mapping of boolean as bool, varchar, or int</td><td>--</td></tr>
<tr><td>BLOB</td><td>partially supported <em>(can not be used in a WHERE clause)</em></td><td>--</td></tr>
+<tr><td>Custom</td><td>partially supported <em>uses custom-defined data type adapter (can not be used in a WHERE clause)</em></td><td>--</td></tr>
<tr><td>UUID</td><td>fully supported <em>(H2 only)</em> </td><td>--</td></tr>
<tr><th colspan="3">configuration</th></tr>
<tr><td>DEFAULT values</td><td>set from annotation, <em>default object values</em>, or Define.defaultValue()</td><td>set from annotations</td></tr>
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</td></tr>
<tr><td>byte []</td> <td></td>
<td>BLOB</td><tr/>
+<tr><td>Custom</td> <td>create a DataTypeAdapter<Custom></td>
+<td>Custom</td><tr/>
<tr><td colspan="3"><b>H2 Database Types</b><br/>
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<SupportedTypes> { + + @Override + public Class<SupportedTypes> 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(); |