From 92cf99b9dbab092230370a351d0c61dcdddf1cfe Mon Sep 17 00:00:00 2001 From: Sabst Date: Sun, 21 Aug 2016 11:44:52 +0200 Subject: [PATCH] Add support for more generic json/jsonb criteria --- README.md | 24 +++++++ grails-app/conf/Config.groovy | 4 ++ .../json/PgJsonTestSearchService.groovy | 11 ++- .../json/PgJsonbTestSearchService.groovy | 20 ++++++ .../postgresql/criteria/JsonCriterias.groovy | 27 ++++++- .../criterion/json/PgJsonExpression.java | 12 ++-- .../json/PgJsonEqualsIntegrationSpec.groovy | 4 +- .../json/PgJsonPathsIntegrationSpec.groovy | 53 ++++++++++++++ .../json/PgJsonValuesIntegrationSpec.groovy | 71 +++++++++++++++++++ .../json/PgJsonbEqualsIntegrationSpec.groovy | 30 ++++++++ .../json/PgJsonbPathsIntegrationSpec.groovy | 53 ++++++++++++++ .../json/PgJsonbValuesIntegrationSpec.groovy | 71 +++++++++++++++++++ 12 files changed, 370 insertions(+), 10 deletions(-) create mode 100644 grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy create mode 100644 test/integration/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy create mode 100644 test/integration/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy create mode 100644 test/integration/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy create mode 100644 test/integration/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy create mode 100644 test/integration/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy diff --git a/README.md b/README.md index bf4ad05..ee67e1e 100644 --- a/README.md +++ b/README.md @@ -536,7 +536,31 @@ def result = TestMapJson.withCriteria { The previous criteria will return all the rows that have a `name` attribute in the json field `data` with the value `Iván`. In this example `obj1` and `obj3`. +##### Generic criterion +With this criterion you can use more operators using a syntax close to the one described in Postgresql documentation. To use it just use `pgJson`: + + +```groovy +def obj1 = new TestMapJson(data: [name: 'Iván', lastName: 'López', other: [followersCount: 150]]).save(flush: true) +def obj2 = new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', other: [followersCount: 148]]).save(flush: true) +def obj3 = new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', other: [followersCount: 149]]).save(flush: true) + +def result1 = TestMapJson.withCriteria { + pgJson 'data', '->>', 'name', 'ilike', '%iv%' +} +``` + +The previous query will return all the rows that have a `name` attribute in the json field `data` containing `iv` (case insensitive). In this example `obj1` and `obj3`. + + +```groovy +def result2 = TestMapJson.withCriteria { + pgJson 'data', '#>>', '{other, followersCount}', '>', 149 +} +``` + +The previous query will return all the rows that have an `other` value whose `followersCount` value is greater than `149`. In this example `obj1`. #### JSONB diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy index 2f1168d..f939b68 100644 --- a/grails-app/conf/Config.groovy +++ b/grails-app/conf/Config.groovy @@ -3,4 +3,8 @@ log4j = { 'org.springframework', 'org.hibernate', 'net.sf.ehcache.hibernate' + + /* Uncomment this to learn more about the SQL actually executed + debug 'org.hibernate' + */ } diff --git a/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy b/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy index ed9eb28..d3c7726 100644 --- a/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy +++ b/grails-app/services/test/criteria/json/PgJsonTestSearchService.groovy @@ -7,7 +7,14 @@ class PgJsonTestSearchService { List search(String criteriaName, String field, String jsonAttribute, value) { TestMapJson.withCriteria { - "${criteriaName}" field, jsonAttribute, value + "${criteriaName}" field, jsonAttribute, value.toString() } } -} \ No newline at end of file + + List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) { + TestMapJson.withCriteria { + "${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString() + } + } + +} diff --git a/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy b/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy new file mode 100644 index 0000000..3a8c058 --- /dev/null +++ b/grails-app/services/test/criteria/json/PgJsonbTestSearchService.groovy @@ -0,0 +1,20 @@ +package test.criteria.json + +import test.json.TestMapJsonb + +class PgJsonbTestSearchService { + static transactional = false + + List search(String criteriaName, String field, String jsonAttribute, value) { + TestMapJsonb.withCriteria { + "${criteriaName}" field, jsonAttribute, value.toString() + } + } + + List search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) { + TestMapJsonb.withCriteria { + "${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString() + } + } + +} diff --git a/src/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy b/src/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy index 9122c4a..fdde6f5 100644 --- a/src/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy +++ b/src/groovy/net/kaleidos/hibernate/postgresql/criteria/JsonCriterias.groovy @@ -7,6 +7,7 @@ class JsonCriterias { JsonCriterias() { addHasFieldValueOperator() + addGenericFieldValueOperator() } private void addHasFieldValueOperator() { @@ -26,7 +27,31 @@ class JsonCriterias { propertyName = calculatePropertyName(propertyName) propertyValue = calculatePropertyValue(propertyValue) - return addToCriteria(new PgJsonExpression(propertyName, jsonAttribute, propertyValue, "=")) + return addToCriteria(new PgJsonExpression(propertyName, '->>', jsonAttribute, "=", propertyValue as String)) } } + + private void addGenericFieldValueOperator() { + /** + * Creates a "json on field value" Criterion based on the specified property name and value + * @param propertyName The property name (json field) + * @param jsonAttribute The json attribute + * @param jsonOp The json operator (->>, #>, ...) + * @param propertyValue The property value + * @param sqlOp The sql operator (=, <>, ilike, ...) + * @return A Criterion instance + */ + HibernateCriteriaBuilder.metaClass.pgJson = { String propertyName, String jsonOp, String jsonAttribute, String sqlOp, propertyValue-> + if (!validateSimpleExpression()) { + throwRuntimeException(new IllegalArgumentException("Call to [pgJson] with propertyName [" + + propertyName + "], json operator [" + jsonOp + "], jsonAttribute [" + jsonAttribute + "], sql operator [" + sqlOp + "] and value [" + propertyValue + "] not allowed here.")) + } + + propertyName = calculatePropertyName(propertyName) + propertyValue = calculatePropertyValue(propertyValue) + + return addToCriteria(new PgJsonExpression(propertyName, jsonOp, jsonAttribute, sqlOp, propertyValue as String)) + } + } + } diff --git a/src/java/net/kaleidos/hibernate/criterion/json/PgJsonExpression.java b/src/java/net/kaleidos/hibernate/criterion/json/PgJsonExpression.java index 74eaa55..7f8fa42 100644 --- a/src/java/net/kaleidos/hibernate/criterion/json/PgJsonExpression.java +++ b/src/java/net/kaleidos/hibernate/criterion/json/PgJsonExpression.java @@ -13,22 +13,24 @@ public class PgJsonExpression implements Criterion { private static final long serialVersionUID = 8372629374639273L; private final String propertyName; + private final String jsonOp; private final String jsonAttribute; + private final String sqlOp; private final Object value; - private final String op; - protected PgJsonExpression(String propertyName, String jsonAttribute, Object value, String op) { + protected PgJsonExpression(String propertyName, String jsonOp, String jsonAttribute, String sqlOp, Object value) { this.propertyName = propertyName; + this.jsonOp = jsonOp; this.jsonAttribute = jsonAttribute; + this.sqlOp = sqlOp; this.value = value; - this.op = op; } @Override public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { return StringHelper.join( " and ", - StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), "->>'" + jsonAttribute + "' " + op + " ?") + StringHelper.suffix(criteriaQuery.findColumns(propertyName, criteria), jsonOp + "'" + jsonAttribute + "' " + sqlOp + " ?") ); } @@ -38,4 +40,4 @@ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuer new TypedValue(new StringType(), value) }; } -} \ No newline at end of file +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy index ec2f951..f3516ea 100644 --- a/test/integration/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonEqualsIntegrationSpec.groovy @@ -9,7 +9,7 @@ class PgJsonEqualsIntegrationSpec extends Specification { def pgJsonTestSearchService @Unroll - void 'Test equals finding value: #value'() { + void 'Test equals finding value: #value (json)'() { setup: new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) @@ -27,4 +27,4 @@ class PgJsonEqualsIntegrationSpec extends Specification { 'Iván' || 2 'John' || 0 } -} \ No newline at end of file +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy new file mode 100644 index 0000000..4e3dfb8 --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonPathsIntegrationSpec.groovy @@ -0,0 +1,53 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJson + +class PgJsonPathsIntegrationSpec extends Specification { + + def pgJsonTestSearchService + + @Unroll + void 'Test equals finding nested values (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) + + then: + result.size() == size + result.every { it.data.nested.a == value } + + where: + value || size + 1 || 2 // there are 2 items with nested.a equal to 1 + 2 || 1 + 3 || 0 + } + + @Unroll + void 'Test equals finding nested values (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) + + then: + result.size() == size + result.every { it.data.nested.b > value.toInteger() } + + where: + value || size + 1 || 3 // There are 3 items with nested.b > 1 + 3 || 1 + 6 || 0 + } + +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy new file mode 100644 index 0000000..32bce4d --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonValuesIntegrationSpec.groovy @@ -0,0 +1,71 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJson + +class PgJsonValuesIntegrationSpec extends Specification { + + def pgJsonTestSearchService + + @Unroll + void 'Test equals finding value: #value with condition is ilike (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) + + then: + result.size() == size + result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } + + where: + value || size + '%iv%' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition equals (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition does not equal (json)'() { + setup: + new TestMapJson(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJson(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJson(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) + + then: + result.size() == size + result.every { it.data.name != value } + + where: + value || size + 'Iván' || 1 + 'John' || 3 + } + +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy new file mode 100644 index 0000000..1afd5a2 --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonbEqualsIntegrationSpec.groovy @@ -0,0 +1,30 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJsonb + +class PgJsonbEqualsIntegrationSpec extends Specification { + + def pgJsonbTestSearchService + + @Unroll + void 'Test equals finding value: #value (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJsonHasFieldValue', 'data', 'name', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy new file mode 100644 index 0000000..6393c7a --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonbPathsIntegrationSpec.groovy @@ -0,0 +1,53 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJsonb + +class PgJsonbPathsIntegrationSpec extends Specification { + + def pgJsonbTestSearchService + + @Unroll + void 'Test equals finding nested values (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, a}', '=', value) + + then: + result.size() == size + result.every { it.data.nested.a == value } + + where: + value || size + 1 || 2 // there are 2 items with nested.a equal to 1 + 2 || 1 + 3 || 0 + } + + @Unroll + void 'Test equals finding nested values (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López', nested: [a: 1, b: 2]]).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres', nested: [a: 2, b: 3]]).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez', nested: [a: 1, b: 5]]).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '#>>', '{nested, b}', '>', value) + + then: + result.size() == size + result.every { it.data.nested.b > value.toInteger() } + + where: + value || size + 1 || 3 // There are 3 items with nested.b > 1 + 3 || 1 + 6 || 0 + } + +} diff --git a/test/integration/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy b/test/integration/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy new file mode 100644 index 0000000..3918a0f --- /dev/null +++ b/test/integration/net/kaleidos/hibernate/json/PgJsonbValuesIntegrationSpec.groovy @@ -0,0 +1,71 @@ +package net.kaleidos.hibernate.json + +import spock.lang.Specification +import spock.lang.Unroll +import test.json.TestMapJsonb + +class PgJsonbValuesIntegrationSpec extends Specification { + + def pgJsonbTestSearchService + + @Unroll + void 'Test equals finding value: #value with condition is ilike (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', 'ilike', value) + + then: + result.size() == size + result.every { it.data.name.matches "^(?i)${value.replace('%', '.*')}\$" } + + where: + value || size + '%iv%' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition equals (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '=', value) + + then: + result.size() == size + result.every { it.data.name == value } + + where: + value || size + 'Iván' || 2 + 'John' || 0 + } + + @Unroll + void 'Test equals finding value: #value with condition does not equal (jsonb)'() { + setup: + new TestMapJsonb(data: [name: 'Iván', lastName: 'López']).save(flush: true) + new TestMapJsonb(data: [name: 'Alonso', lastName: 'Torres']).save(flush: true) + new TestMapJsonb(data: [name: 'Iván', lastName: 'Pérez']).save(flush: true) + + when: + def result = pgJsonbTestSearchService.search('pgJson', 'data', '->>', 'name', '<>', value) + + then: + result.size() == size + result.every { it.data.name != value } + + where: + value || size + 'Iván' || 1 + 'John' || 3 + } + +}