aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2014-10-29 17:12:14 -0400
committerJames Moger <james.moger@gitblit.com>2014-10-29 17:12:14 -0400
commit8d28bc740c9bcb76186e7572f74a144397e780ce (patch)
tree1815e1d21df77e352ba2e8106557f71cb5561a8e /src
parentbdb2899da4cbb27016d85c5e4fe268ddbccef546 (diff)
downloadiciql-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.java60
-rw-r--r--src/main/java/com/iciql/Define.java20
-rw-r--r--src/main/java/com/iciql/Iciql.java78
-rw-r--r--src/main/java/com/iciql/JavaSerializationTypeAdapter.java98
-rw-r--r--src/main/java/com/iciql/Query.java16
-rw-r--r--src/main/java/com/iciql/SQLDialect.java66
-rw-r--r--src/main/java/com/iciql/SQLDialectDefault.java50
-rw-r--r--src/main/java/com/iciql/SQLDialectH2.java5
-rw-r--r--src/main/java/com/iciql/SQLDialectHSQL.java5
-rw-r--r--src/main/java/com/iciql/SQLDialectMySQL.java7
-rw-r--r--src/main/java/com/iciql/SQLDialectPostgreSQL.java75
-rw-r--r--src/main/java/com/iciql/SQLStatement.java2
-rw-r--r--src/main/java/com/iciql/TableDefinition.java43
-rw-r--r--src/site/jaqu_comparison.mkd1
-rw-r--r--src/site/model_classes.mkd5
-rw-r--r--src/test/java/com/iciql/test/DataTypeAdapterTest.java94
-rw-r--r--src/test/java/com/iciql/test/IciqlSuite.java5
-rw-r--r--src/test/java/com/iciql/test/models/SupportedTypes.java5
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&lt;CustomObject&gt; {
+ *
+ * public Class&lt;CustomObject&gt; 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&lt;Custom&gt;</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();