@@ -138,6 +138,7 @@ | |||
<menu name="getting started" pager="true" pagerPlacement="bottom" pagerLayout="justified"> | |||
<page name="models" src="model_classes.mkd" out="model_classes.html" headerLinks="true" /> | |||
<page name="versioning" src="table_versioning.mkd" out="table_versioning.html" headerLinks="true" /> | |||
<page name="data type adapters" src="dta.mkd" out="dta.html" headerLinks="true" /> | |||
<page name="usage" src="usage.mkd" out="usage.html" headerLinks="true" /> | |||
<page name="examples" src="examples.mkd" out="examples.html" headerLinks="true" /> | |||
<page name="tools" src="tools.mkd" out="tools.html" headerLinks="true" /> |
@@ -657,14 +657,6 @@ 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; | |||
} | |||
/** | |||
@@ -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<? extends DataTypeAdapter<?>> 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<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; | |||
} | |||
} | |||
} |
@@ -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<String> { | |||
@Override | |||
public String getDataType() { | |||
return "jsonb"; | |||
} | |||
@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. | |||
*/ |
@@ -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<T> { | |||
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<T> { | |||
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(); |
@@ -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<? extends DataTypeAdapter<?>> getDataTypeAdapter(Annotation [] annotations) { | |||
Class<? extends DataTypeAdapter<?>> 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; | |||
} | |||
} |
@@ -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<Invoice> { | |||
@Override | |||
public String getDataType() { | |||
return "jsonb"; | |||
} | |||
@Override | |||
public Class<Invoice> 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. | |||
@@ -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<SupportedTypes> { | |||
public static class SupportedTypesAdapterImpl extends JavaSerializationTypeAdapter<SupportedTypes> { | |||
@Override | |||
public Class<SupportedTypes> 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 { } | |||
} |