Skip to content

Commit

Permalink
Add support for more generic json/jsonb criteria
Browse files Browse the repository at this point in the history
  • Loading branch information
Sabst committed Aug 21, 2016
1 parent 5db768a commit 92cf99b
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 10 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions grails-app/conf/Config.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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'
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ class PgJsonTestSearchService {

List<TestMapJson> search(String criteriaName, String field, String jsonAttribute, value) {
TestMapJson.withCriteria {
"${criteriaName}" field, jsonAttribute, value
"${criteriaName}" field, jsonAttribute, value.toString()
}
}
}

List<TestMapJson> search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) {
TestMapJson.withCriteria {
"${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString()
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package test.criteria.json

import test.json.TestMapJsonb

class PgJsonbTestSearchService {
static transactional = false

List<TestMapJsonb> search(String criteriaName, String field, String jsonAttribute, value) {
TestMapJsonb.withCriteria {
"${criteriaName}" field, jsonAttribute, value.toString()
}
}

List<TestMapJsonb> search(String criteriaName, String field, String jsonOp, String jsonAttribute, String sqlOp, value) {
TestMapJsonb.withCriteria {
"${criteriaName}" field, jsonOp, jsonAttribute, sqlOp, value.toString()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class JsonCriterias {

JsonCriterias() {
addHasFieldValueOperator()
addGenericFieldValueOperator()
}

private void addHasFieldValueOperator() {
Expand All @@ -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 <condition> 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))
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 + " ?")
);
}

Expand All @@ -38,4 +40,4 @@ public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuer
new TypedValue(new StringType(), value)
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -27,4 +27,4 @@ class PgJsonEqualsIntegrationSpec extends Specification {
'Iván' || 2
'John' || 0
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 92cf99b

Please sign in to comment.