From db0d58c22a0bd4fa2baf023428599757aa4db381 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 7 Nov 2014 09:28:57 -0500 Subject: [PATCH] Revise type adapter definition to be a separate annotation --- build.xml | 1 + src/main/java/com/iciql/Iciql.java | 46 ++------ .../java/com/iciql/SQLDialectPostgreSQL.java | 33 ++++++ src/main/java/com/iciql/TableDefinition.java | 18 +-- src/main/java/com/iciql/util/Utils.java | 23 ++++ src/site/dta.mkd | 104 ++++++++++++++++++ .../com/iciql/test/DataTypeAdapterTest.java | 21 +++- 7 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 src/site/dta.mkd diff --git a/build.xml b/build.xml index d53c820..bcbcd7f 100644 --- a/build.xml +++ b/build.xml @@ -138,6 +138,7 @@ + diff --git a/src/main/java/com/iciql/Iciql.java b/src/main/java/com/iciql/Iciql.java index 05cceeb..524faa8 100644 --- a/src/main/java/com/iciql/Iciql.java +++ b/src/main/java/com/iciql/Iciql.java @@ -657,14 +657,6 @@ 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; - } /** @@ -740,6 +732,17 @@ public interface Iciql { */ void defineIQ(); + /** + * Specify a custom type adapter for a method return type, a class field, or a method + * parameter. Type adapters allow you to transform content received from or inserted into + * a database field. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) + public @interface TypeAdapter { + Class> value(); + } + /** * Interface to allow implementations of custom data type adapters for supporting * database-specific data types, like the Postgres 'json' or 'xml' types, @@ -782,31 +785,4 @@ public interface Iciql { } - /** - * 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/SQLDialectPostgreSQL.java b/src/main/java/com/iciql/SQLDialectPostgreSQL.java index b5ac5c3..f10017c 100644 --- a/src/main/java/com/iciql/SQLDialectPostgreSQL.java +++ b/src/main/java/com/iciql/SQLDialectPostgreSQL.java @@ -139,6 +139,39 @@ public class SQLDialectPostgreSQL extends SQLDialectDefault { } } + /** + * Handles transforming raw strings to/from the Postgres JSONB data type. + */ + public class JsonbStringAdapter implements DataTypeAdapter { + + @Override + public String getDataType() { + return "jsonb"; + } + + @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. */ diff --git a/src/main/java/com/iciql/TableDefinition.java b/src/main/java/com/iciql/TableDefinition.java index fc83d29..e4fff87 100644 --- a/src/main/java/com/iciql/TableDefinition.java +++ b/src/main/java/com/iciql/TableDefinition.java @@ -51,7 +51,6 @@ 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; @@ -525,6 +524,17 @@ public class TableDefinition { throw new IciqlException(e, "failed to get default object for {0}", columnName); } + // identify the type adapter + typeAdapter = Utils.getDataTypeAdapter(f.getAnnotations()); + if (typeAdapter == null) { + typeAdapter = Utils.getDataTypeAdapter(f.getType().getAnnotations()); + } + + if (typeAdapter != null) { + DataTypeAdapter dtt = db.getDialect().getAdapter(typeAdapter); + dataType = dtt.getDataType(); + } + boolean hasAnnotation = f.isAnnotationPresent(IQColumn.class); if (hasAnnotation) { IQColumn col = f.getAnnotation(IQColumn.class); @@ -538,12 +548,6 @@ public class TableDefinition { trim = col.trim(); nullable = col.nullable(); - if (col.typeAdapter() != null && col.typeAdapter() != StandardJDBCTypeAdapter.class) { - typeAdapter = col.typeAdapter(); - DataTypeAdapter dtt = db.getDialect().getAdapter(col.typeAdapter()); - dataType = dtt.getDataType(); - } - // annotation overrides if (!StringUtils.isNullOrEmpty(col.defaultValue())) { defaultValue = col.defaultValue(); diff --git a/src/main/java/com/iciql/util/Utils.java b/src/main/java/com/iciql/util/Utils.java index 346c6d7..bf66092 100644 --- a/src/main/java/com/iciql/util/Utils.java +++ b/src/main/java/com/iciql/util/Utils.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringWriter; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.math.BigDecimal; import java.math.BigInteger; @@ -40,8 +41,10 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import com.iciql.Iciql.DataTypeAdapter; import com.iciql.Iciql.EnumId; import com.iciql.Iciql.EnumType; +import com.iciql.Iciql.TypeAdapter; import com.iciql.IciqlException; /** @@ -532,4 +535,24 @@ public class Utils { in.close(); } } + + /** + * Identify the data type adapter class in the annotations. + * + * @param annotations + * @return null or the dtaa type adapter class + */ + public static Class> getDataTypeAdapter(Annotation [] annotations) { + Class> typeAdapter = null; + if (annotations != null) { + for (Annotation annotation : annotations) { + if (annotation instanceof TypeAdapter) { + typeAdapter = ((TypeAdapter) annotation).value(); + } else if (annotation.annotationType().isAnnotationPresent(TypeAdapter.class)) { + typeAdapter = annotation.annotationType().getAnnotation(TypeAdapter.class).value(); + } + } + } + return typeAdapter; + } } diff --git a/src/site/dta.mkd b/src/site/dta.mkd new file mode 100644 index 0000000..61c5f62 --- /dev/null +++ b/src/site/dta.mkd @@ -0,0 +1,104 @@ +## Data Type Adapters + +Data type adapters allow you to extend Iciql's support for field data types. + +For example, you might want to take advantage of the [Postgres JSON/JSONB support](http://www.postgresql.org/docs/9.4/static/datatype-json.html) in 9.3/9.4 but instead of directly handling JSON text documents you might want to represent that JSON as a domain object and serialize/deserialize to JSON only when executing an SQL operation. + +Data type adapters give you this flexibility. + +**NOTE:** Data type adapters are reused within a Db instance and are not inherently thread-safe. You must handle thread-safety on your own, if it is an issue. + +### An example + +Consider the following model class. + +---JAVA--- +@IQTable +public class Invoices { + + @IQColumn(primaryKey = true, autoIncrement = true) + public long _id; + + @IQColumn + Date received; + + @IQColumn + @TypeAdapter(InvoiceAdapterImpl.class) + Invoice invoice; +} +---JAVA--- + +This is a really simple table with three columns, but the third column uses a type adapter to map our *invoice* object field to an SQL type. You can use the `@TypeAdapter` annotation either on the field definition or on the class definition of your domain model. + +Let's take a look at *InvoiceAdapterImpl*. + +---JAVA--- +public class InvoiceAdapterImpl implements DataTypeAdapter { + + @Override + public String getDataType() { + return "jsonb"; + } + + @Override + public Class getJavaType() { + return Invoice.class; + } + + Gson gson() { + return new GsonBuilder().create(); + } + + @Override + public Object serialize(DcKey value) { + String json = gson().toJson(value); + PGobject pg = new PGobject(); + pg.setType(getDataType()); + try { + pg.setValue(json); + } catch (SQLException e) { + // ignore, never thrown + } + return pg; + } + + @Override + public Invoice deserialize(Object value) { + + // the incoming object is always represented as a string + final String json = value.toString(); + final Invoice invoice = gson().fromJson(json, getJavaType()); + + return invoice; + } +---JAVA--- + +Here you can see how the *InvoiceTypeAdapter* defines a [Postgres JSONB data type](http://www.postgresql.org/docs/9.4/static/datatype-json.html) and automatically handles JSON (de)serialization with [Google Gson](https://code.google.com/p/google-gson) so that the database gets the content in a form that it requires but we can continue to work with objects in Java. + +### Custom annotations + +It is a little verbose to repeat `@TypeAdapter(InvoiceAdapterImpl.class)` everywhere you want to use your adapter. + +To simplify this, you can implement your own annotation which specifies your type adapter. + +---JAVA--- +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@TypeAdapter(InvoiceAdapterImpl.class) +public @interface InvoiceAdapter { } +---JAVA--- + +### Included DataTypeAdapters + +Not every DataTypeAdapter has to be a custom implementation. +The following adapters are included in Iciql for general purpose use. + +- `com.iciql.JavaSerializationTypeAdapter` +Uses Java serialization to store/retrieve your objects as BLOBs. +- `com.iciql.SQLDialectPostgreSQL.JsonStringAdapter` +Allows you to use the Postgres JSON data type with a standard String. +- `com.iciql.SQLDialectPostgreSQL.JsonbStringAdapter` +Allows you to use the Postgres JSONB data type with a standard String. +- `com.iciql.SQLDialectPostgreSQL.XmlStringAdapter` +Allows you to use the Postgres XML data type with a standard String. + diff --git a/src/test/java/com/iciql/test/DataTypeAdapterTest.java b/src/test/java/com/iciql/test/DataTypeAdapterTest.java index f10d298..d1ccb53 100644 --- a/src/test/java/com/iciql/test/DataTypeAdapterTest.java +++ b/src/test/java/com/iciql/test/DataTypeAdapterTest.java @@ -16,6 +16,10 @@ package com.iciql.test; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Date; import org.junit.After; @@ -26,6 +30,7 @@ import org.junit.Test; import com.iciql.Db; import com.iciql.Iciql.IQColumn; import com.iciql.Iciql.IQTable; +import com.iciql.Iciql.TypeAdapter; import com.iciql.JavaSerializationTypeAdapter; import com.iciql.test.models.SupportedTypes; @@ -64,17 +69,18 @@ public class DataTypeAdapterTest extends Assert { } - @IQTable + @IQTable(name="dataTypeAdapters") public static class SerializedObjectTypeAdapterTest { @IQColumn(autoIncrement = true, primaryKey = true) private long id; @IQColumn - private java.util.Date received; + public java.util.Date received; - @IQColumn(typeAdapter = SupportedTypesAdapter.class) - private SupportedTypes obj; + @IQColumn + @SupportedTypesAdapter + public SupportedTypes obj; } @@ -82,7 +88,7 @@ public class DataTypeAdapterTest extends Assert { * Maps a SupportedType instance to a BLOB using Java Object serialization. * */ - public static class SupportedTypesAdapter extends JavaSerializationTypeAdapter { + public static class SupportedTypesAdapterImpl extends JavaSerializationTypeAdapter { @Override public Class getJavaType() { @@ -91,4 +97,9 @@ public class DataTypeAdapterTest extends Assert { } + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) + @TypeAdapter(SupportedTypesAdapterImpl.class) + public @interface SupportedTypesAdapter { } + } -- 2.39.5