diff --git a/PostgresqlExtensionsGrailsPlugin.groovy b/PostgresqlExtensionsGrailsPlugin.groovy index 2e07b69..65148a1 100644 --- a/PostgresqlExtensionsGrailsPlugin.groovy +++ b/PostgresqlExtensionsGrailsPlugin.groovy @@ -3,7 +3,7 @@ import net.kaleidos.hibernate.postgresql.criteria.HstoreCriterias class PostgresqlExtensionsGrailsPlugin { // the plugin version - def version = "4.0.0" + def version = "4.1.0" // the version or versions of Grails the plugin is designed for def grailsVersion = "2.0 > *" // the other plugins this plugin depends on @@ -25,7 +25,7 @@ class PostgresqlExtensionsGrailsPlugin { def author = "Iván López" def authorEmail = "lopez.ivan@gmail.com" def description = '''\ -This plugin provides hibernate user types to support for Postgresql Native Types like Arrays, HStores, JSON,... as well as new criterias to query this native types +This plugin provides hibernate user types to support for Postgresql Native Types like Array, HStore, JSON,... as well as new criterias to query this native types ''' // URL to the plugin's documentation diff --git a/README.md b/README.md index e4d7ed8..bcc1239 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Grails Postgresql Extensions [![Build Status](https://travis-ci.org/kaleidos/grails-postgresql-extensions.svg?branch=master)](https://travis-ci.org/kaleidos/grails-postgresql-extensions) [![Coverage Status](https://coveralls.io/repos/kaleidos/grails-postgresql-extensions/badge.png?branch=master)](https://coveralls.io/r/kaleidos/grails-postgresql-extensions?branch=master) -This is a grails plugin that provides hibernate user types to use postgresql native types such as arrays, hstores, json,... from a Grails application. It also provides new criterias to query this new native types. +This is a grails plugin that provides hibernate user types to use Postgresql native types such as Array, Hstore, Json,... from a Grails application. It also provides new criterias to query this new native types. -Currently the plugin supports arrays and hstore and some query methods has been implemented. More native types and query methods will be added in the future. +Currently the plugin supports array, hstore and json fields as well as some query methods. More native types and query methods will be added in the future. * [Installation](#installation) * [Configuration](#configuration) @@ -30,6 +30,7 @@ Currently the plugin supports arrays and hstore and some query methods has been * [Contains Key](#contains-key) * [Contains](#contains-1) * [Is Contained](#is-contained-1) + * [JSON](#json) * [Authors](#authors) * [Release Notes](#release-notes) @@ -409,6 +410,47 @@ testAttributes = ["1" : "a", "2" : "b"] ``` This criteria can also be used to look for exact matches + +### JSON + +Currently the Json support is only available in Grails 2.2.5 and 2.3.1+. Just like with the Hstore support, it's not possible to use a Map in domain classes with old Grails versions and be able to set the database type for the Map. + +To define a json field you only have to define a `Map` field and use the `JsonMapType` hibernate user type. + +```groovy +import net.kaleidos.hibernate.usertype.JsonMapType + +class TestMapJson { + + Map data + + static constraints = { + } + static mapping = { + data type: JsonMapType + } +} +``` + +#### Using Json + +Now you can create and instance of the domain class: + +```groovy +def instance = new TestMapJson(data: [name: "Iván", age: 34, hasChilds: true, childs: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]]) +instance.save() +``` + +``` +=# select * from test_map_json; + + id | version | data +----+---------+------------------------------------------------------------------------------------------------------------- + 1 | 0 | {"hasChilds":true,"age":34,"name":"Iván","childs":[{"name":"Judith","age":7},{"name":"Adriana","age":4}]} + +As you can see the plugin converts to Json automatically the attributes and the lists in the map type. + + Authors ------- @@ -423,9 +465,10 @@ Collaborations are appreciated :-) Release Notes ------------- -* [4.0.0] - 18/Jul/2014 - Version compatible with Hibernate 4.x -* [3.0.0] - 18/Jul/2014 - Version compatible with Hibernate 3.x -* [0.9](https://github.com/kaleidos/grails-postgresql-extensions/issues?milestone=1) - 16/Jun/2014 - Add new array criterias: pgArrayEquals, pgArrayNotEquals +* 4.1.0 - 23/Jul/2014 - Add JSON support. It's now possible to store and read domain classes with map types persisted to json. +* 4.0.0 - 18/Jul/2014 - Version compatible with Hibernate 4.x. +* 3.0.0 - 18/Jul/2014 - Version compatible with Hibernate 3.x. +* [0.9](https://github.com/kaleidos/grails-postgresql-extensions/issues?milestone=1) - 16/Jun/2014 - Add new array criterias: pgArrayEquals, pgArrayNotEquals. * 0.8.1 - 24/Apr/2014 - Fix NPE when array is null. * 0.8 - 24/Apr/2014 - Added support for Double and Float arrays. Refactored the ArrayType to be used as a parametrized type. * 0.7 - Unreleased - New HstoreMapType and update plugin to Grails 2.2.5. diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 5a9b1e0..ae87eba 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -37,6 +37,8 @@ grails.project.dependency.resolution = { test "org.springframework:spring-expression:$springVersion" test "org.springframework:spring-aop:$springVersion" + compile 'com.google.code.gson:gson:2.2.4' + // Coveralls plugin build 'org.apache.httpcomponents:httpcore:4.3.2' build 'org.apache.httpcomponents:httpclient:4.3.2' diff --git a/grails-app/domain/test/json/TestMapJson.groovy b/grails-app/domain/test/json/TestMapJson.groovy new file mode 100644 index 0000000..6aa1eee --- /dev/null +++ b/grails-app/domain/test/json/TestMapJson.groovy @@ -0,0 +1,14 @@ +package test.json + +import net.kaleidos.hibernate.usertype.JsonMapType + +class TestMapJson { + + Map data + + static constraints = { + } + static mapping = { + data type: JsonMapType + } +} \ No newline at end of file diff --git a/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java b/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java index 7160f1a..aea299d 100644 --- a/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java +++ b/src/java/net/kaleidos/hibernate/PostgresqlExtensionsDialect.java @@ -2,6 +2,7 @@ import net.kaleidos.hibernate.usertype.ArrayType; import net.kaleidos.hibernate.usertype.HstoreType; +import net.kaleidos.hibernate.usertype.JsonMapType; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.PostgreSQL81Dialect; import org.hibernate.id.PersistentIdentifierGenerator; @@ -26,6 +27,7 @@ public PostgresqlExtensionsDialect() { registerColumnType(ArrayType.DOUBLE_ARRAY, "float8[]"); registerColumnType(ArrayType.FLOAT_ARRAY, "float[]"); registerColumnType(HstoreType.SQLTYPE, "hstore"); + registerColumnType(JsonMapType.SQLTYPE, "json"); } /** diff --git a/src/java/net/kaleidos/hibernate/usertype/JsonMapType.java b/src/java/net/kaleidos/hibernate/usertype/JsonMapType.java new file mode 100644 index 0000000..df71754 --- /dev/null +++ b/src/java/net/kaleidos/hibernate/usertype/JsonMapType.java @@ -0,0 +1,95 @@ +package net.kaleidos.hibernate.usertype; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.lang.ObjectUtils; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.lang.reflect.Type; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +public class JsonMapType implements UserType { + + public static int SQLTYPE = 90021; + + private final Type userType = Map.class; + + private final Gson gson = new GsonBuilder().create(); + + @Override + public int[] sqlTypes() { + return new int[]{SQLTYPE}; + } + + @Override + public Class returnedClass() { + return userType.getClass(); + } + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + return ObjectUtils.equals(x, y); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return x == null ? 0 : x.hashCode(); + } + + @Override + public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { + String jsonString = rs.getString(names[0]); + return gson.fromJson(jsonString, userType); + } + + @Override + public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { + if (value == null) { + st.setNull(index, Types.OTHER); + } else { + st.setObject(index, gson.toJson(value, userType), Types.OTHER); + } + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + if (value != null) { + Map m = (Map) value; + + if (m == null) { + m = new HashMap(); + } + return new HashMap(m); + } else { + return null; + } + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return gson.toJson(value, userType); + } + + @Override + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return gson.fromJson((String) cached, userType); + } + + @Override + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return original; + } +} \ No newline at end of file diff --git a/test/integration/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy new file mode 100644 index 0000000..7fbf9d5 --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PostgresqlJsonMapDomainIntegrationSpec.groovy @@ -0,0 +1,41 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJson + +class PostgresqlJsonMapDomainIntegrationSpec extends Specification { + + @Unroll + void 'save a domain class with a map #map to json'() { + setup: + def testMapJson = new TestMapJson(data: map) + + when: + testMapJson.save(flush: true) + + then: + testMapJson.hasErrors() == false + testMapJson.data == map + + where: + map << [null, [:], [name: 'Ivan', age: 34]] + } + + void 'save and read a domain class with json'() { + setup: + def value = [name: 'Ivan', age: 34, hasChilds: true, childs: [[name: 'Judith', age: 7], [name: 'Adriana', age: 4]]] + def testMapJson = new TestMapJson(data: value) + + when: + testMapJson.save(flush: true) + + then: + testMapJson.hasErrors() == false + + and: + def obj = testMapJson.get(testMapJson.id) + obj.data.keySet().collect { it.toString() }.equals(['name', 'age', 'hasChilds', 'childs']) + obj.data.childs.size() == 2 + } +} \ No newline at end of file