Skip to content

Commit 87ee32a

Browse files
authored
feat: 添加pg中jsonb查询相关操作符支持 (#104)
* feat: 添加pg中jsonb查询相关操作符支持 * feat: 优化
1 parent 8f14f04 commit 87ee32a

7 files changed

Lines changed: 411 additions & 10 deletions

File tree

hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/JsonType.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import lombok.Getter;
44
import org.hswebframework.ezorm.rdb.metadata.DataType;
5+
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
6+
import org.hswebframework.ezorm.rdb.metadata.dialect.DataTypeBuilder;
57

68
import java.sql.JDBCType;
79
import java.sql.SQLType;
810

911

1012
@Getter
11-
public class JsonType implements DataType {
13+
public class JsonType implements DataType, DataTypeBuilder {
1214

1315
public static JsonType INSTANCE = new JsonType();
1416

@@ -29,8 +31,12 @@ public String getName() {
2931

3032
@Override
3133
public SQLType getSqlType() {
32-
return JDBCType.CLOB;
34+
return JDBCType.OTHER;
3335
}
3436

37+
@Override
38+
public String createColumnDataType(RDBColumnMetadata columnMetaData) {
39+
return "json";
40+
}
3541

3642
}

hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/JsonbType.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import lombok.AllArgsConstructor;
44
import lombok.Getter;
55
import org.hswebframework.ezorm.rdb.metadata.DataType;
6+
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
7+
import org.hswebframework.ezorm.rdb.metadata.dialect.DataTypeBuilder;
68

79
import java.sql.JDBCType;
810
import java.sql.SQLType;
911

1012
@Getter
1113
@AllArgsConstructor
12-
public class JsonbType implements DataType {
14+
public class JsonbType implements DataType , DataTypeBuilder {
1315
public static JsonbType INSTANCE = new JsonbType();
1416

1517
@Override
@@ -29,6 +31,11 @@ public String getName() {
2931

3032
@Override
3133
public SQLType getSqlType() {
32-
return JDBCType.CLOB;
34+
return JDBCType.OTHER;
35+
}
36+
37+
@Override
38+
public String createColumnDataType(RDBColumnMetadata columnMetaData) {
39+
return "jsonb";
3340
}
3441
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.hswebframework.ezorm.rdb.supports.postgres;
2+
3+
import com.google.common.collect.Lists;
4+
import lombok.SneakyThrows;
5+
import org.hswebframework.ezorm.core.param.Term;
6+
import org.hswebframework.ezorm.rdb.codec.JsonValueCodec;
7+
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
8+
import org.hswebframework.ezorm.rdb.operator.builder.fragments.*;
9+
import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder;
10+
import org.hswebframework.ezorm.rdb.utils.SqlUtils;
11+
12+
import java.util.ArrayList;
13+
import java.util.Collection;
14+
import java.util.Collections;
15+
import java.util.List;
16+
17+
public class PostgresqlJsonbExistTermFragmentBuilder extends AbstractTermFragmentBuilder {
18+
19+
public static final PostgresqlJsonbExistTermFragmentBuilder exist = new PostgresqlJsonbExistTermFragmentBuilder("exist", "jsonb包含");
20+
21+
interface Options {
22+
String contains = "contains";
23+
String contained = "contained";
24+
String all = "all";
25+
String any = "any";
26+
}
27+
28+
29+
public PostgresqlJsonbExistTermFragmentBuilder(String termType, String name) {
30+
super(termType, name);
31+
}
32+
33+
34+
@Override
35+
public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) {
36+
String operator = getOperator(term);
37+
if (Operator.needObject(operator)) {
38+
PrepareSqlFragments fragments = PrepareSqlFragments.of();
39+
fragments.addSql(columnFullName, operator);
40+
Object value = term.getValue();
41+
return appendPrepareOrNative(fragments, convertObjectValue(column, value));
42+
}
43+
if (!Operator.base.equals(operator)) {
44+
//转换值
45+
List<Object> values = convertList(term.getValue());
46+
if (values.isEmpty()) {
47+
return EmptySqlFragments.INSTANCE;
48+
}
49+
return new BatchSqlFragments(4, 1)
50+
.addSql(operator, "(", columnFullName, ",")
51+
.addSql("array[")
52+
.add(SqlUtils.createQuestionMarks(values.size()))
53+
.addSql("])")
54+
.addParameter(values);
55+
}
56+
57+
PrepareSqlFragments fragments = PrepareSqlFragments.of();
58+
fragments.addSql(Operator.base, "(", columnFullName, ",");
59+
appendPrepareOrNative(fragments, term.getValue());
60+
return fragments.addSql(")");
61+
}
62+
63+
@SneakyThrows
64+
private Object convertObjectValue(RDBColumnMetadata column, Object value) {
65+
if (value instanceof NativeSql) {
66+
return value;
67+
}
68+
if (column.getValueCodec() != null) {
69+
return column.getValueCodec().encode(value);
70+
}
71+
Object obj;
72+
if (value == null) {
73+
return null;
74+
}
75+
if (value instanceof CharSequence) {
76+
obj = value.toString();
77+
} else {
78+
obj = JsonValueCodec.defaultMapper.writeValueAsString(value);
79+
}
80+
return NativeSql.of("?::jsonb", obj);
81+
}
82+
83+
84+
private List<Object> convertList(Object value) {
85+
if (value == null) {
86+
return Collections.emptyList();
87+
}
88+
if (value instanceof String v) {
89+
value = v.split(",");
90+
}
91+
if (value instanceof Object[] v) {
92+
return Lists.newArrayList(v);
93+
}
94+
if (value instanceof Collection<?> v) {
95+
return new ArrayList<>(v);
96+
}
97+
return Collections.singletonList(value);
98+
}
99+
100+
public static String getOperator(Term term) {
101+
List<String> options = term.getOptions();
102+
if (options.contains(Options.contains)) {
103+
return Operator.contains;
104+
}
105+
if (options.contains(Options.contained)) {
106+
return Operator.contained;
107+
}
108+
if (options.contains(Options.all)) {
109+
return Operator.all;
110+
}
111+
if (options.contains(Options.any)) {
112+
return Operator.any;
113+
}
114+
return Operator.base;
115+
}
116+
117+
private interface Operator {
118+
//用函数规避SimpleParameterList#checkAllParametersSet的检查
119+
String base = "jsonb_exists";
120+
String all = "jsonb_exists_all";
121+
String any = "jsonb_exists_any";
122+
String contains = "@>";
123+
String contained = "<@";
124+
125+
static boolean needObject(String operator) {
126+
return contains.equals(operator) || contained.equals(operator);
127+
}
128+
}
129+
}

hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlSchemaMetadata.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public RDBTableMetadata newTable(String name) {
6363
column.addFeature(new PostgresqlVectorDistanceFunctionFragmentBuilder());
6464
PostgresqlVectorDistanceTermFragmentBuilder.ALL.values().forEach(column::addFeature);
6565
}
66+
if (column.getType() instanceof JsonbType) {
67+
column.addFeature(PostgresqlJsonbExistTermFragmentBuilder.exist);
68+
}
6669
});
6770
return metadata;
6871
}

hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/utils/SqlUtils.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ public static StringBuilder replaceSqlParameter(String sql,
291291
* - jsonb_column ? array['key1', 'key2']
292292
* <p>
293293
* 判断逻辑:
294-
* 1. 前面(跳过空格)必须是标识符字符(字母、数字、下划线、右括号、右方括号
295-
* 2. 后面(跳过空格)必须是单引号字符串或 array[
294+
* 1. 前面(跳过空格)必须是标识符字符(字母、数字、下划线、右括号、右方括号或字段名引号
295+
* 2. 后面(跳过空格)必须是单引号字符串、参数占位符?或 array[
296296
*
297297
* @param sql SQL 语句
298298
* @param index '?' 的位置
@@ -311,9 +311,10 @@ private static boolean isPostgresOperator(String sql, int index) {
311311
}
312312

313313
char prev = sql.charAt(prevIndex);
314-
// 标识符字符:字母、数字、下划线、右括号、右方括号
314+
// 标识符字符:字母、数字、下划线、右括号、右方括号、引号
315315
// 如果不是这些字符,则不是操作符(可能是 =, >, < 等操作符后的参数占位符)
316316
if (!(Character.isLetterOrDigit(prev)
317+
|| prev == '"'
317318
|| prev == '_'
318319
|| prev == ')'
319320
|| prev == ']')) {
@@ -337,8 +338,8 @@ private static boolean isPostgresOperator(String sql, int index) {
337338

338339
char next = sql.charAt(nextIndex);
339340

340-
// 检查是否是单引号字符串('key')
341-
if (next == '\'') {
341+
// 检查是否是单引号字符串('key')或 参数占位符 (?)
342+
if (next == '\'' || next == '?') {
342343
return true;
343344
}
344345

hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBasicTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
package org.hswebframework.ezorm.rdb.supports.postgres;
22

33
import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor;
4+
import org.hswebframework.ezorm.rdb.executor.SqlRequests;
45
import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor;
6+
import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers;
7+
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
8+
import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata;
59
import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata;
10+
import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
611
import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect;
12+
import org.hswebframework.ezorm.rdb.operator.DatabaseOperator;
13+
import org.hswebframework.ezorm.rdb.operator.DefaultDatabaseOperator;
714
import org.hswebframework.ezorm.rdb.supports.BasicCommonTests;
15+
import org.junit.Assert;
16+
import org.junit.Before;
17+
import org.junit.Test;
18+
19+
import java.sql.JDBCType;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.LinkedHashMap;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
825

926
public class PostgresqlBasicTest extends BasicCommonTests {
1027
@Override
@@ -21,4 +38,107 @@ protected Dialect getDialect() {
2138
protected SyncSqlExecutor getSqlExecutor() {
2239
return new TestSyncSqlExecutor(new PostgresqlConnectionProvider());
2340
}
41+
42+
String jsonbTableName = "test_jsonb_exist_sync";
43+
44+
@Test
45+
public void testJsonbExistTerm() {
46+
RDBDatabaseMetadata database = getDatabase();
47+
DatabaseOperator operator = DefaultDatabaseOperator.of(database);
48+
49+
try {
50+
51+
operator.sql()
52+
.sync()
53+
.execute(SqlRequests.of("insert into public." + jsonbTableName + " (id,data) values (?, ?::jsonb)",
54+
"1",
55+
"{\"name\":\"JetLinks\",\"age\":18}"));
56+
operator.sql()
57+
.sync()
58+
.execute(SqlRequests.of("insert into public." + jsonbTableName + " (id,data) values (?, ?::jsonb)",
59+
"2",
60+
"{\"name\":\"Other\"}"));
61+
operator.sql()
62+
.sync()
63+
.execute(SqlRequests.of("insert into public." + jsonbTableName + " (id,data) values (?, ?::jsonb)",
64+
"3",
65+
"{\"age\":20,\"status\":\"ok\"}"));
66+
67+
List<String> existsIds = operator.dml()
68+
.query(jsonbTableName)
69+
.select("id")
70+
.where(q -> q.where("data$exist", "name"))
71+
.fetch(ResultWrappers.mapStream())
72+
.sync()
73+
.map(map -> String.valueOf(map.get("id")))
74+
.collect(Collectors.toList());
75+
76+
List<String> anyIds = operator.dml()
77+
.query(jsonbTableName)
78+
.select("id")
79+
.where(q -> q.where("data$exist$any", Arrays.asList("name", "status")))
80+
.fetch(ResultWrappers.mapStream())
81+
.sync()
82+
.map(map -> String.valueOf(map.get("id")))
83+
.collect(Collectors.toList());
84+
85+
List<String> allIds = operator.dml()
86+
.query(jsonbTableName)
87+
.select("id")
88+
.where(q -> q.where("data$exist$all", Arrays.asList("name", "age")))
89+
.fetch(ResultWrappers.mapStream())
90+
.sync()
91+
.map(map -> String.valueOf(map.get("id")))
92+
.collect(Collectors.toList());
93+
94+
List<String> containsIds = operator.dml()
95+
.query(jsonbTableName)
96+
.select("id")
97+
.where(q -> q.where("data$exist$contains", Collections.singletonMap("name", "JetLinks")))
98+
.fetch(ResultWrappers.mapStream())
99+
.sync()
100+
.map(map -> String.valueOf(map.get("id")))
101+
.collect(Collectors.toList());
102+
103+
LinkedHashMap<String, Object> containedTarget = new LinkedHashMap<>();
104+
containedTarget.put("name", "JetLinks");
105+
containedTarget.put("age", 18);
106+
containedTarget.put("status", "ok");
107+
108+
List<String> containedIds = operator.dml()
109+
.query(jsonbTableName)
110+
.select("id")
111+
.where(q -> q.where("data$exist$contained", containedTarget))
112+
.fetch(ResultWrappers.mapStream())
113+
.sync()
114+
.map(map -> String.valueOf(map.get("id")))
115+
.collect(Collectors.toList());
116+
117+
Assert.assertEquals(Arrays.asList("1", "2"), existsIds);
118+
Assert.assertEquals(Arrays.asList("1", "2", "3"), anyIds);
119+
Assert.assertEquals(List.of("1"), allIds);
120+
Assert.assertEquals(List.of("1"), containsIds);
121+
Assert.assertEquals(List.of("1"), containedIds);
122+
} finally {
123+
try {
124+
operator.sql()
125+
.sync()
126+
.execute(SqlRequests.of("drop table if exists public." + jsonbTableName));
127+
} catch (Exception ignore) {
128+
}
129+
}
130+
}
131+
132+
@Before
133+
public void createJsonbTable() {
134+
RDBDatabaseMetadata database = getDatabase();
135+
DatabaseOperator operator = DefaultDatabaseOperator.of(database);
136+
137+
operator.ddl()
138+
.createOrAlter(jsonbTableName)
139+
.addColumn().name("id").varchar(32).primaryKey().comment("ID").commit()
140+
.addColumn().name("data").type(JsonbType.INSTANCE).comment("数据").commit()
141+
.commit()
142+
.sync();
143+
}
24144
}

0 commit comments

Comments
 (0)