From 790e807c4e77bc8096f08c46a0ab636eac5b7f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Tue, 19 May 2026 10:54:06 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(PostgreSQL):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E7=B1=BB=E5=9E=8B=E5=AD=97=E6=AE=B5=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgres/PostgresqlArrayType.java | 266 ++++++++++++++++++ .../supports/postgres/PostgresqlDialect.java | 14 + .../postgres/PostgresqlArraySupportTest.java | 63 +++++ .../PostgresqlTableMetaParserTest.java | 57 +++- .../postgres/array/PostgresqlArrayTest.java | 218 ++++++++++++++ 5 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java create mode 100644 hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArraySupportTest.java create mode 100644 hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java new file mode 100644 index 00000000..ef62253b --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java @@ -0,0 +1,266 @@ +package org.hswebframework.ezorm.rdb.supports.postgres; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.ezorm.core.ValueCodec; +import org.hswebframework.ezorm.rdb.metadata.DataType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.DataTypeBuilder; +import org.postgresql.util.PGobject; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.sql.SQLType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor(staticName = "of") +public class PostgresqlArrayType implements DataType, ValueCodec, DataTypeBuilder { + + public static final PostgresqlArrayType VARCHAR_ARRAY = PostgresqlArrayType.of("varchar[]", String.class, String[].class); + + public static final PostgresqlArrayType TEXT_ARRAY = PostgresqlArrayType.of("text[]", String.class, String[].class); + + public static final PostgresqlArrayType SMALLINT_ARRAY = PostgresqlArrayType.of("smallint[]", Short.class, Short[].class); + + public static final PostgresqlArrayType INTEGER_ARRAY = PostgresqlArrayType.of("integer[]", Integer.class, Integer[].class); + + public static final PostgresqlArrayType BIGINT_ARRAY = PostgresqlArrayType.of("bigint[]", Long.class, Long[].class); + + private final String name; + + private final Class componentType; + + private final Class javaType; + + @Override + public String getId() { + return name; + } + + @Override + public SQLType getSqlType() { + return JDBCType.ARRAY; + } + + @Override + public Object encode(Object value) { + return convert(value); + } + + @Override + public Object decode(Object data) { + return convert(data); + } + + @Override + public String createColumnDataType(RDBColumnMetadata columnMetaData) { + return name; + } + + private Object convert(Object value) { + if (value == null) { + return null; + } + if (javaType.isInstance(value)) { + return value; + } + if (value instanceof java.sql.Array sqlArray) { + return convertSqlArray(sqlArray); + } + if (value instanceof PGobject pgObject) { + return convert(pgObject.getValue()); + } + if (value instanceof Collection collection) { + return convertCollection(collection); + } + if (value.getClass().isArray()) { + return convertJavaArray(value); + } + if (value instanceof CharSequence sequence) { + return parseArrayLiteral(sequence.toString()); + } + return newArray(convertElement(value)); + } + + private Object convertSqlArray(java.sql.Array sqlArray) { + try { + return convert(sqlArray.getArray()); + } catch (SQLException e) { + throw new IllegalArgumentException("Failed to read PostgreSQL array value", e); + } finally { + try { + sqlArray.free(); + } catch (Exception ignore) { + } + } + } + + private Object convertCollection(Collection collection) { + Object result = Array.newInstance(componentType, collection.size()); + int index = 0; + for (Object element : collection) { + Array.set(result, index++, convertElement(element)); + } + return result; + } + + private Object convertJavaArray(Object array) { + int length = Array.getLength(array); + Object result = Array.newInstance(componentType, length); + for (int i = 0; i < length; i++) { + Array.set(result, i, convertElement(Array.get(array, i))); + } + return result; + } + + private Object parseArrayLiteral(String literal) { + String value = StringUtils.trimToEmpty(literal); + if (value.isEmpty()) { + return Array.newInstance(componentType, 0); + } + if ((value.startsWith("{") && value.endsWith("}")) + || (value.startsWith("[") && value.endsWith("]")) + || (value.startsWith("(") && value.endsWith(")"))) { + value = value.substring(1, value.length() - 1); + } + if (value.isEmpty()) { + return Array.newInstance(componentType, 0); + } + List tokens = split(value); + Object result = Array.newInstance(componentType, tokens.size()); + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + Array.set(result, i, convertElement(token.value, token.quoted)); + } + return result; + } + + private List split(String value) { + List tokens = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + boolean inQuotes = false; + boolean escaping = false; + boolean quoted = false; + int depth = 0; + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (escaping) { + builder.append(ch); + escaping = false; + continue; + } + if (inQuotes) { + if (ch == '\\') { + escaping = true; + continue; + } + if (ch == '"') { + inQuotes = false; + quoted = true; + continue; + } + builder.append(ch); + continue; + } + if (ch == '"') { + inQuotes = true; + continue; + } + if (ch == '{') { + depth++; + builder.append(ch); + continue; + } + if (ch == '}') { + if (depth > 0) { + depth--; + } + builder.append(ch); + continue; + } + if (ch == ',' && depth == 0) { + tokens.add(new Token(builder.toString(), quoted)); + builder.setLength(0); + quoted = false; + continue; + } + builder.append(ch); + } + tokens.add(new Token(builder.toString(), quoted)); + return tokens; + } + + private Object convertElement(Object element) { + return convertElement(element, false); + } + + private Object convertElement(Object element, boolean quoted) { + if (element == null) { + return null; + } + if (componentType == String.class) { + String value = String.valueOf(element); + if (!quoted) { + value = value.trim(); + if ("null".equalsIgnoreCase(value)) { + return null; + } + } + return value; + } + if (componentType.isInstance(element)) { + return element; + } + if (element instanceof Number number) { + return fromNumber(number); + } + String value = quoted ? String.valueOf(element) : StringUtils.trimToNull(String.valueOf(element)); + if (value == null) { + return null; + } + if (!quoted && "null".equalsIgnoreCase(value)) { + return null; + } + if (componentType == Short.class) { + return new BigDecimal(value).shortValue(); + } + if (componentType == Integer.class) { + return new BigDecimal(value).intValue(); + } + if (componentType == Long.class) { + return new BigDecimal(value).longValue(); + } + return value; + } + + private Object fromNumber(Number number) { + if (componentType == Short.class) { + return number.shortValue(); + } + if (componentType == Integer.class) { + return number.intValue(); + } + if (componentType == Long.class) { + return number.longValue(); + } + return String.valueOf(number); + } + + private Object newArray(Object value) { + Object result = Array.newInstance(componentType, 1); + Array.set(result, 0, value); + return result; + } + + @RequiredArgsConstructor + private static class Token { + private final String value; + private final boolean quoted; + } +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlDialect.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlDialect.java index a6648918..6d5a779e 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlDialect.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlDialect.java @@ -51,6 +51,20 @@ public PostgresqlDialect() { registerDataType("halfvec", VectorType.HALF_VECTOR); registerDataType("sparsevec", VectorType.SPARSE_VECTOR); + registerDataType("varchar[]", PostgresqlArrayType.VARCHAR_ARRAY); + registerDataType("text[]", PostgresqlArrayType.TEXT_ARRAY); + registerDataType("smallint[]", PostgresqlArrayType.SMALLINT_ARRAY); + registerDataType("integer[]", PostgresqlArrayType.INTEGER_ARRAY); + registerDataType("bigint[]", PostgresqlArrayType.BIGINT_ARRAY); + registerDataType("int2[]", PostgresqlArrayType.SMALLINT_ARRAY); + registerDataType("int4[]", PostgresqlArrayType.INTEGER_ARRAY); + registerDataType("int8[]", PostgresqlArrayType.BIGINT_ARRAY); + registerDataType("_varchar", PostgresqlArrayType.VARCHAR_ARRAY); + registerDataType("_text", PostgresqlArrayType.TEXT_ARRAY); + registerDataType("_int2", PostgresqlArrayType.SMALLINT_ARRAY); + registerDataType("_int4", PostgresqlArrayType.INTEGER_ARRAY); + registerDataType("_int8", PostgresqlArrayType.BIGINT_ARRAY); + registerDataType("character varying[]", PostgresqlArrayType.VARCHAR_ARRAY); registerDataType("json", JsonType.INSTANCE); registerDataType("jsonb", JsonbType.INSTANCE); diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArraySupportTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArraySupportTest.java new file mode 100644 index 00000000..6880a22e --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArraySupportTest.java @@ -0,0 +1,63 @@ +package org.hswebframework.ezorm.rdb.supports.postgres; + +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.junit.Assert; +import org.junit.Test; +import org.postgresql.util.PGobject; + +import java.util.List; + +public class PostgresqlArraySupportTest { + + @Test + public void testStringArrayCodec() throws Exception { + Assert.assertArrayEquals(new String[]{"a", "b"}, (String[]) PostgresqlArrayType.VARCHAR_ARRAY.encode(List.of("a", "b"))); + Assert.assertArrayEquals(new String[]{"a", "b"}, (String[]) PostgresqlArrayType.VARCHAR_ARRAY.encode(new String[]{"a", "b"})); + Assert.assertArrayEquals(new String[]{"a", "b"}, (String[]) PostgresqlArrayType.VARCHAR_ARRAY.decode("{a,b}")); + Assert.assertArrayEquals(new String[]{"white shirt", "glasses", null}, (String[]) PostgresqlArrayType.TEXT_ARRAY.decode("{\"white shirt\",glasses,NULL}")); + Assert.assertArrayEquals(new String[]{"NULL", "a,b"}, (String[]) PostgresqlArrayType.TEXT_ARRAY.decode("{\"NULL\",\"a,b\"}")); + + PGobject pgObject = new PGobject(); + pgObject.setType("text[]"); + pgObject.setValue("{\"white shirt\",glasses}"); + Assert.assertArrayEquals(new String[]{"white shirt", "glasses"}, (String[]) PostgresqlArrayType.TEXT_ARRAY.decode(pgObject)); + } + + @Test + public void testNumberArrayCodec() { + Assert.assertArrayEquals(new Short[]{1, 2, 3}, (Short[]) PostgresqlArrayType.SMALLINT_ARRAY.encode(new short[]{1, 2, 3})); + Assert.assertArrayEquals(new Short[]{1, 2, 3}, (Short[]) PostgresqlArrayType.SMALLINT_ARRAY.decode("{1,2,3}")); + Assert.assertArrayEquals(new Integer[]{1, 2, 3}, (Integer[]) PostgresqlArrayType.INTEGER_ARRAY.encode(List.of(1L, 2L, 3L))); + Assert.assertArrayEquals(new Integer[]{1, 2, 3}, (Integer[]) PostgresqlArrayType.INTEGER_ARRAY.decode("{1,2,3}")); + Assert.assertArrayEquals(new Long[]{1L, 2L, 3L}, (Long[]) PostgresqlArrayType.BIGINT_ARRAY.encode(new int[]{1, 2, 3})); + Assert.assertArrayEquals(new Long[]{1L, null, 3L}, (Long[]) PostgresqlArrayType.BIGINT_ARRAY.decode("{1,NULL,3}")); + } + + @Test + public void testArrayColumnType() { + RDBSchemaMetadata schema = createSchema(); + RDBTableMetadata table = schema.newTable("test_array"); + schema.addTable(table); + + RDBColumnMetadata column = table.newColumn(); + column.setName("tags"); + column.setOwner(table); + column.setType(table.getDialect().convertDataType("smallint[]")); + table.addColumn(column); + + Assert.assertEquals("smallint[]", column.getDataType()); + Assert.assertEquals(Short[].class, column.getJavaType()); + } + + private RDBSchemaMetadata createSchema() { + RDBDatabaseMetadata database = new RDBDatabaseMetadata(Dialect.POSTGRES); + RDBSchemaMetadata schema = new PostgresqlSchemaMetadata("public"); + database.addSchema(schema); + database.setCurrentSchema(schema); + return schema; + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java index 7f43cc42..04e7a265 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java @@ -58,7 +58,12 @@ public void testParse() { "name varchar(128) not null," + "age int4," + "json1 json," + - "json2 jsonb" + + "json2 jsonb," + + "tags varchar[]," + + "keywords text[]," + + "codes smallint[]," + + "nums int4[]," + + "ids int8[]" + ")")); try { RDBTableMetadata metaData = parser.parseByName("test_table").orElseThrow(NullPointerException::new); @@ -120,6 +125,56 @@ public void testParse() { Assert.assertEquals(column.getSqlType(), JDBCType.CLOB); Assert.assertEquals(column.getJavaType(), String.class); } + //varchar[] + { + RDBColumnMetadata column = metaData.getColumn("tags").orElseThrow(NullPointerException::new); + + Assert.assertNotNull(column); + Assert.assertEquals(column.getDataType(), "varchar[]"); + Assert.assertEquals(column.getType().getId(), "varchar[]"); + Assert.assertEquals(column.getSqlType(), JDBCType.ARRAY); + Assert.assertEquals(column.getJavaType(), String[].class); + } + //text[] + { + RDBColumnMetadata column = metaData.getColumn("keywords").orElseThrow(NullPointerException::new); + + Assert.assertNotNull(column); + Assert.assertEquals(column.getDataType(), "text[]"); + Assert.assertEquals(column.getType().getId(), "text[]"); + Assert.assertEquals(column.getSqlType(), JDBCType.ARRAY); + Assert.assertEquals(column.getJavaType(), String[].class); + } + //smallint[] + { + RDBColumnMetadata column = metaData.getColumn("codes").orElseThrow(NullPointerException::new); + + Assert.assertNotNull(column); + Assert.assertEquals(column.getDataType(), "smallint[]"); + Assert.assertEquals(column.getType().getId(), "smallint[]"); + Assert.assertEquals(column.getSqlType(), JDBCType.ARRAY); + Assert.assertEquals(column.getJavaType(), Short[].class); + } + //int4[] + { + RDBColumnMetadata column = metaData.getColumn("nums").orElseThrow(NullPointerException::new); + + Assert.assertNotNull(column); + Assert.assertEquals(column.getDataType(), "integer[]"); + Assert.assertEquals(column.getType().getId(), "integer[]"); + Assert.assertEquals(column.getSqlType(), JDBCType.ARRAY); + Assert.assertEquals(column.getJavaType(), Integer[].class); + } + //int8[] + { + RDBColumnMetadata column = metaData.getColumn("ids").orElseThrow(NullPointerException::new); + + Assert.assertNotNull(column); + Assert.assertEquals(column.getDataType(), "bigint[]"); + Assert.assertEquals(column.getType().getId(), "bigint[]"); + Assert.assertEquals(column.getSqlType(), JDBCType.ARRAY); + Assert.assertEquals(column.getJavaType(), Long[].class); + } } finally { executor.execute(prepare("drop table test_table")); } diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java new file mode 100644 index 00000000..492e7605 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java @@ -0,0 +1,218 @@ +package org.hswebframework.ezorm.rdb.supports.postgres.array; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.core.DefaultValueGenerator; +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.core.meta.ObjectMetadata; +import org.hswebframework.ezorm.rdb.TestReactiveSqlExecutor; +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SqlRequests; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.mapping.EntityColumnMapping; +import org.hswebframework.ezorm.rdb.mapping.MappingFeatureType; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.ezorm.rdb.mapping.SyncRepository; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.defaults.DefaultReactiveRepository; +import org.hswebframework.ezorm.rdb.mapping.defaults.DefaultSyncRepository; +import org.hswebframework.ezorm.rdb.mapping.jpa.JpaEntityTableMetadataParser; +import org.hswebframework.ezorm.rdb.mapping.wrapper.EntityResultWrapper; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.DefaultDatabaseOperator; +import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlR2dbcConnectionProvider; +import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlSchemaMetadata; +import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlConnectionProvider; +import org.junit.Assert; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.UUID; + +@Slf4j +public class PostgresqlArrayTest { + + @Test + public void testSyncArrayField() { + RDBDatabaseMetadata database = getSyncDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + SyncSqlExecutor executor = getSyncSqlExecutor(); + try { + SyncRepository repository = createSyncRepository(database, operator); + + ArrayEntity entity = new ArrayEntity(); + entity.setId("arr-sync"); + entity.setTags(new Short[]{1, 2, 3}); + entity.setKeywords(new String[]{"person", "white shirt", "glasses"}); + + repository.insert(entity); + + ArrayEntity loaded = repository.findById("arr-sync").orElseThrow(NullPointerException::new); + Assert.assertArrayEquals(new Short[]{1, 2, 3}, loaded.getTags()); + Assert.assertArrayEquals(new String[]{"person", "white shirt", "glasses"}, loaded.getKeywords()); + + ArrayEntity byTags = repository + .createQuery() + .where(ArrayEntity::getTags, new Short[]{1, 2, 3}) + .fetchOne() + .orElseThrow(NullPointerException::new); + Assert.assertEquals("arr-sync", byTags.getId()); + + ArrayEntity byKeywords = repository + .createQuery() + .where(ArrayEntity::getKeywords, new String[]{"person", "white shirt", "glasses"}) + .fetchOne() + .orElseThrow(NullPointerException::new); + Assert.assertEquals("arr-sync", byKeywords.getId()); + } finally { + try { + executor.execute(SqlRequests.of("drop table test_pg_array_basic")); + } catch (Exception ignore) { + } + } + } + + @Test + public void testReactiveArrayField() { + RDBDatabaseMetadata database = getReactiveDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + ReactiveSqlExecutor executor = getReactiveSqlExecutor(); + try { + ReactiveRepository repository = createReactiveRepository(database, operator); + + ArrayEntity entity = new ArrayEntity(); + entity.setId("arr-reactive"); + entity.setTags(new Short[]{4, 5, 6}); + entity.setKeywords(new String[]{"vehicle", "white", "plate"}); + + repository.insert(Mono.just(entity)) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + repository.findById(Mono.just("arr-reactive")) + .as(StepVerifier::create) + .assertNext(loaded -> { + Assert.assertArrayEquals(new Short[]{4, 5, 6}, loaded.getTags()); + Assert.assertArrayEquals(new String[]{"vehicle", "white", "plate"}, loaded.getKeywords()); + }) + .verifyComplete(); + + repository + .createQuery() + .where(ArrayEntity::getTags, new Short[]{4, 5, 6}) + .fetch() + .as(StepVerifier::create) + .assertNext(loaded -> Assert.assertEquals("arr-reactive", loaded.getId())) + .verifyComplete(); + } finally { + try { + executor.execute(Mono.just(SqlRequests.of("drop table test_pg_array_basic"))).block(); + } catch (Exception ignore) { + } + } + } + + private SyncRepository createSyncRepository(RDBDatabaseMetadata database, DatabaseOperator operator) { + JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser(); + parser.setDatabaseMetadata(database); + RDBTableMetadata table = parser.parseTableMetadata(ArrayEntity.class).orElseThrow(NullPointerException::new); + operator.ddl().createOrAlter(table).commit().sync(); + + EntityResultWrapper wrapper = new EntityResultWrapper<>(ArrayEntity::new); + wrapper.setMapping(table + .getFeature(MappingFeatureType.columnPropertyMapping.createFeatureId(ArrayEntity.class)) + .orElseThrow(NullPointerException::new)); + return new DefaultSyncRepository<>(operator, table, ArrayEntity.class, wrapper); + } + + private ReactiveRepository createReactiveRepository(RDBDatabaseMetadata database, DatabaseOperator operator) { + JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser(); + parser.setDatabaseMetadata(database); + RDBTableMetadata table = parser.parseTableMetadata(ArrayEntity.class).orElseThrow(NullPointerException::new); + operator.ddl().createOrAlter(table).commit().reactive().block(); + + EntityResultWrapper wrapper = new EntityResultWrapper<>(ArrayEntity::new); + wrapper.setMapping(table + .getFeature(MappingFeatureType.columnPropertyMapping.createFeatureId(ArrayEntity.class)) + .orElseThrow(NullPointerException::new)); + return new DefaultReactiveRepository<>(operator, table, ArrayEntity.class, wrapper); + } + + private RDBDatabaseMetadata getSyncDatabase() { + RDBDatabaseMetadata metadata = new RDBDatabaseMetadata(Dialect.POSTGRES); + RDBSchemaMetadata schema = getSchema(); + metadata.setCurrentSchema(schema); + metadata.addSchema(schema); + metadata.addFeature(getSyncSqlExecutor()); + return metadata; + } + + private RDBDatabaseMetadata getReactiveDatabase() { + RDBDatabaseMetadata metadata = new RDBDatabaseMetadata(Dialect.POSTGRES); + RDBSchemaMetadata schema = getSchema(); + metadata.setCurrentSchema(schema); + metadata.addSchema(schema); + ReactiveSqlExecutor executor = getReactiveSqlExecutor(); + metadata.addFeature(executor); + metadata.addFeature(ReactiveSyncSqlExecutor.of(executor)); + return metadata; + } + + private RDBSchemaMetadata getSchema() { + PostgresqlSchemaMetadata schema = new PostgresqlSchemaMetadata("public"); + schema.addFeature(new DefaultValueGenerator() { + @Override + public String getSortId() { + return "uuid"; + } + + @Override + public RuntimeDefaultValue generate(ObjectMetadata meta) { + return () -> UUID.randomUUID().toString().replace("-", ""); + } + + @Override + public String getName() { + return "UUID"; + } + }); + return schema; + } + + private SyncSqlExecutor getSyncSqlExecutor() { + return new TestSyncSqlExecutor(new PostgresqlConnectionProvider()); + } + + private ReactiveSqlExecutor getReactiveSqlExecutor() { + return new TestReactiveSqlExecutor(new PostgresqlR2dbcConnectionProvider()); + } + + @Setter + @Getter + @Table(name = "test_pg_array_basic") + public static class ArrayEntity { + @Id + @Column(length = 32) + private String id; + + @Column(nullable = false) + @ColumnType(typeId = "smallint[]") + private Short[] tags; + + @Column(nullable = false) + @ColumnType(typeId = "text[]") + private String[] keywords; + } +} From ee18bcebaff260f61cc0148f4481e25d0e4bc547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Tue, 19 May 2026 11:56:36 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(PostgreSQL):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E4=B8=8E?= =?UTF-8?q?Docker=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hsweb-easy-orm-rdb/pom.xml | 2 +- .../rdb/executor/JdbcParameterBinder.java | 9 +++ .../rdb/executor/R2dbcParameterBinder.java | 8 +++ .../executor/jdbc/JdbcSqlExecutorHelper.java | 3 + .../r2dbc/R2dbcReactiveSqlExecutor.java | 3 + .../postgres/PostgresqlArrayParameter.java | 65 +++++++++++++++++++ .../postgres/PostgresqlArrayType.java | 21 ++++-- .../PostgresqlTableMetaParserTest.java | 6 +- 8 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/JdbcParameterBinder.java create mode 100644 hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/R2dbcParameterBinder.java create mode 100644 hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayParameter.java diff --git a/hsweb-easy-orm-rdb/pom.xml b/hsweb-easy-orm-rdb/pom.xml index d7a67e09..409ec036 100644 --- a/hsweb-easy-orm-rdb/pom.xml +++ b/hsweb-easy-orm-rdb/pom.xml @@ -139,7 +139,7 @@ org.testcontainers testcontainers - 1.17.1 + 1.21.4 test diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/JdbcParameterBinder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/JdbcParameterBinder.java new file mode 100644 index 00000000..33207a96 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/JdbcParameterBinder.java @@ -0,0 +1,9 @@ +package org.hswebframework.ezorm.rdb.executor; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface JdbcParameterBinder { + + void bind(PreparedStatement statement, int index) throws SQLException; +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/R2dbcParameterBinder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/R2dbcParameterBinder.java new file mode 100644 index 00000000..3de0f177 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/R2dbcParameterBinder.java @@ -0,0 +1,8 @@ +package org.hswebframework.ezorm.rdb.executor; + +import io.r2dbc.spi.Statement; + +public interface R2dbcParameterBinder { + + void bind(Statement statement, String identifier); +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java index 1f1e4123..f21de54a 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java @@ -2,6 +2,7 @@ import lombok.SneakyThrows; import org.hswebframework.ezorm.rdb.codec.LongCharSequence; +import org.hswebframework.ezorm.rdb.executor.JdbcParameterBinder; import org.hswebframework.ezorm.rdb.executor.NullValue; import java.io.ByteArrayInputStream; @@ -37,6 +38,8 @@ protected static void preparedStatementParameter(PreparedStatement statement, Ob for (Object object : parameter) { if (object == null) { statement.setNull(index++, Types.NULL); + } else if (object instanceof JdbcParameterBinder binder) { + binder.bind(statement, index++); } else if (object instanceof NullValue nullValue) { statement.setNull(index++, nullValue.getDataType().getSqlType().getVendorTypeNumber()); } else if (object instanceof Date) { diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/reactive/r2dbc/R2dbcReactiveSqlExecutor.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/reactive/r2dbc/R2dbcReactiveSqlExecutor.java index 8aaa718f..23b0f0be 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/reactive/r2dbc/R2dbcReactiveSqlExecutor.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/reactive/r2dbc/R2dbcReactiveSqlExecutor.java @@ -9,6 +9,7 @@ import org.hswebframework.ezorm.rdb.executor.BatchSqlRequest; import org.hswebframework.ezorm.rdb.executor.DefaultColumnWrapperContext; import org.hswebframework.ezorm.rdb.executor.NullValue; +import org.hswebframework.ezorm.rdb.executor.R2dbcParameterBinder; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; @@ -251,6 +252,8 @@ protected Statement prepareStatement(Statement statement, SqlRequest request) { for (Object parameter : request.getParameters()) { if (parameter == null) { bindNull(statement, index, String.class); + } else if (parameter instanceof R2dbcParameterBinder binder) { + binder.bind(statement, getBindSymbol() + (index + getBindFirstIndex())); } else if (parameter instanceof NullValue nullValue) { Class javaType = nullValue.getType(); if (javaType == LongCharSequence.class) { diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayParameter.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayParameter.java new file mode 100644 index 00000000..8331cbb1 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayParameter.java @@ -0,0 +1,65 @@ +package org.hswebframework.ezorm.rdb.supports.postgres; + +import io.r2dbc.spi.Parameters; +import io.r2dbc.spi.Statement; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.hswebframework.ezorm.rdb.executor.JdbcParameterBinder; +import org.hswebframework.ezorm.rdb.executor.R2dbcParameterBinder; + +import java.lang.reflect.Array; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.StringJoiner; + +@Getter +@RequiredArgsConstructor(staticName = "of") +class PostgresqlArrayParameter implements JdbcParameterBinder, R2dbcParameterBinder { + + private final PostgresqlArrayType type; + + private final Object value; + + @Override + public void bind(PreparedStatement statement, int index) throws SQLException { + if (value == null) { + statement.setNull(index, Types.ARRAY); + return; + } + statement.setArray(index, statement.getConnection().createArrayOf(type.getJdbcElementType(), toObjectArray())); + } + + @Override + public void bind(Statement statement, String identifier) { + if (value == null) { + statement.bind(identifier, Parameters.in(type.getR2dbcArrayType())); + return; + } + statement.bind(identifier, Parameters.in(type.getR2dbcArrayType(), value)); + } + + private Object[] toObjectArray() { + if (value instanceof Object[] values) { + return values; + } + int len = Array.getLength(value); + Object[] values = new Object[len]; + for (int i = 0; i < len; i++) { + values[i] = Array.get(value, i); + } + return values; + } + + @Override + public String toString() { + if (value == null) { + return "null::" + type.getId(); + } + StringJoiner joiner = new StringJoiner(",", "{", "}::" + type.getId()); + for (Object element : toObjectArray()) { + joiner.add(element == null ? "NULL" : String.valueOf(element)); + } + return joiner.toString(); + } +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java index ef62253b..4c254bb4 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayType.java @@ -1,9 +1,11 @@ package org.hswebframework.ezorm.rdb.supports.postgres; +import io.r2dbc.postgresql.codec.PostgresqlObjectId; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.hswebframework.ezorm.core.ValueCodec; +import org.hswebframework.ezorm.core.meta.ColumnMetadata; import org.hswebframework.ezorm.rdb.metadata.DataType; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.ezorm.rdb.metadata.dialect.DataTypeBuilder; @@ -22,18 +24,22 @@ @RequiredArgsConstructor(staticName = "of") public class PostgresqlArrayType implements DataType, ValueCodec, DataTypeBuilder { - public static final PostgresqlArrayType VARCHAR_ARRAY = PostgresqlArrayType.of("varchar[]", String.class, String[].class); + public static final PostgresqlArrayType VARCHAR_ARRAY = PostgresqlArrayType.of("varchar[]", "varchar", PostgresqlObjectId.VARCHAR_ARRAY, String.class, String[].class); - public static final PostgresqlArrayType TEXT_ARRAY = PostgresqlArrayType.of("text[]", String.class, String[].class); + public static final PostgresqlArrayType TEXT_ARRAY = PostgresqlArrayType.of("text[]", "text", PostgresqlObjectId.TEXT_ARRAY, String.class, String[].class); - public static final PostgresqlArrayType SMALLINT_ARRAY = PostgresqlArrayType.of("smallint[]", Short.class, Short[].class); + public static final PostgresqlArrayType SMALLINT_ARRAY = PostgresqlArrayType.of("smallint[]", "int2", PostgresqlObjectId.INT2_ARRAY, Short.class, Short[].class); - public static final PostgresqlArrayType INTEGER_ARRAY = PostgresqlArrayType.of("integer[]", Integer.class, Integer[].class); + public static final PostgresqlArrayType INTEGER_ARRAY = PostgresqlArrayType.of("integer[]", "int4", PostgresqlObjectId.INT4_ARRAY, Integer.class, Integer[].class); - public static final PostgresqlArrayType BIGINT_ARRAY = PostgresqlArrayType.of("bigint[]", Long.class, Long[].class); + public static final PostgresqlArrayType BIGINT_ARRAY = PostgresqlArrayType.of("bigint[]", "int8", PostgresqlObjectId.INT8_ARRAY, Long.class, Long[].class); private final String name; + private final String jdbcElementType; + + private final PostgresqlObjectId r2dbcArrayType; + private final Class componentType; private final Class javaType; @@ -53,6 +59,11 @@ public Object encode(Object value) { return convert(value); } + @Override + public Object encode(Object value, ColumnMetadata column) { + return PostgresqlArrayParameter.of(this, convert(value)); + } + @Override public Object decode(Object data) { return convert(data); diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java index 04e7a265..cc816ac9 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlTableMetaParserTest.java @@ -112,7 +112,7 @@ public void testParse() { Assert.assertNotNull(column); Assert.assertEquals(column.getDataType(), "json"); - Assert.assertEquals(column.getSqlType(), JDBCType.CLOB); + Assert.assertEquals(column.getSqlType(), JDBCType.OTHER); Assert.assertEquals(column.getJavaType(), String.class); } //jsonb @@ -122,7 +122,7 @@ public void testParse() { Assert.assertNotNull(column); Assert.assertEquals(column.getDataType(), "jsonb"); Assert.assertEquals(column.getType().getId(), "jsonb"); - Assert.assertEquals(column.getSqlType(), JDBCType.CLOB); + Assert.assertEquals(column.getSqlType(), JDBCType.OTHER); Assert.assertEquals(column.getJavaType(), String.class); } //varchar[] @@ -182,4 +182,4 @@ public void testParse() { } -} \ No newline at end of file +} From 9233ef9f01b618b519389b836442a32531b220df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Tue, 19 May 2026 12:09:34 +0800 Subject: [PATCH 3/5] =?UTF-8?q?test(PostgreSQL):=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E4=BB=93=E5=82=A8=E9=9B=86=E6=88=90=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgres/array/PostgresqlArrayTest.java | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java index 492e7605..5b7c921a 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java @@ -19,6 +19,7 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.defaults.DefaultReactiveRepository; import org.hswebframework.ezorm.rdb.mapping.defaults.DefaultSyncRepository; +import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.ezorm.rdb.mapping.jpa.JpaEntityTableMetadataParser; import org.hswebframework.ezorm.rdb.mapping.wrapper.EntityResultWrapper; import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; @@ -32,13 +33,17 @@ import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlConnectionProvider; import org.junit.Assert; import org.junit.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import javax.persistence.Column; import javax.persistence.Id; import javax.persistence.Table; +import java.util.Arrays; +import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j public class PostgresqlArrayTest { @@ -83,6 +88,83 @@ public void testSyncArrayField() { } } + @Test + public void testSyncRepositoryCrud() { + RDBDatabaseMetadata database = getSyncDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + SyncSqlExecutor executor = getSyncSqlExecutor(); + try { + SyncRepository repository = createSyncRepository(database, operator); + + SaveResult initial = repository.save(Arrays.asList( + entity("sync-save-1", new Short[]{1, 2}, new String[]{"person", "white shirt"}), + entity("sync-save-2", new Short[]{2, 3}, new String[]{"vehicle", "white car"}) + )); + Assert.assertEquals(2, initial.getTotal()); + + SaveResult updated = repository.save(Arrays.asList( + entity("sync-save-1", new Short[]{7, 8}, new String[]{"person", "glasses"}), + entity("sync-save-3", new Short[]{3, 4}, new String[]{"event", "night"}) + )); + Assert.assertEquals(2, updated.getTotal()); + + Assert.assertArrayEquals( + new Short[]{7, 8}, + repository.findById("sync-save-1").orElseThrow(NullPointerException::new).getTags() + ); + + Assert.assertEquals(2, repository.insertBatch(Arrays.asList( + entity("sync-batch-1", new Short[]{8, 9}, new String[]{"batch", "one"}), + entity("sync-batch-2", new Short[]{9, 10}, new String[]{"batch", "two"}) + ))); + + Assert.assertEquals(1, repository.updateById( + "sync-save-2", + entity("sync-save-2", new Short[]{5, 6}, new String[]{"vehicle", "updated"}) + )); + + Assert.assertArrayEquals( + new String[]{"vehicle", "updated"}, + repository.findById("sync-save-2").orElseThrow(NullPointerException::new).getKeywords() + ); + + List pagedIds = repository + .createQuery() + .where(ArrayEntity::getTags, new Short[]{8, 9}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()); + Assert.assertEquals(List.of("sync-batch-1"), pagedIds); + + Assert.assertEquals(1, repository + .createUpdate() + .set(ArrayEntity::getKeywords, new String[]{"dsl", "updated"}) + .where(ArrayEntity::getId, "sync-batch-2") + .execute()); + + Assert.assertArrayEquals( + new String[]{"dsl", "updated"}, + repository.findById("sync-batch-2").orElseThrow(NullPointerException::new).getKeywords() + ); + + Assert.assertEquals(1, repository + .createDelete() + .where(ArrayEntity::getId, "sync-batch-1") + .execute()); + + Assert.assertTrue(repository.findById("sync-batch-1").isEmpty()); + Assert.assertEquals(2, repository.deleteById(Arrays.asList("sync-save-1", "sync-save-3"))); + Assert.assertEquals(1, repository.deleteById("sync-save-2")); + Assert.assertEquals(1, repository.deleteById("sync-batch-2")); + } finally { + try { + executor.execute(SqlRequests.of("drop table test_pg_array_basic")); + } catch (Exception ignore) { + } + } + } + @Test public void testReactiveArrayField() { RDBDatabaseMetadata database = getReactiveDatabase(); @@ -124,6 +206,119 @@ public void testReactiveArrayField() { } } + @Test + public void testReactiveRepositoryCrud() { + RDBDatabaseMetadata database = getReactiveDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + ReactiveSqlExecutor executor = getReactiveSqlExecutor(); + try { + ReactiveRepository repository = createReactiveRepository(database, operator); + + repository.save(Arrays.asList( + entity("reactive-save-1", new Short[]{11, 12}, new String[]{"person", "hat"}), + entity("reactive-save-2", new Short[]{12, 13}, new String[]{"vehicle", "black"}) + )) + .as(StepVerifier::create) + .assertNext(result -> Assert.assertEquals(2, result.getTotal())) + .verifyComplete(); + + repository.save(Arrays.asList( + entity("reactive-save-1", new Short[]{21, 22}, new String[]{"person", "updated"}), + entity("reactive-save-3", new Short[]{13, 14}, new String[]{"event", "gate"}) + )) + .as(StepVerifier::create) + .assertNext(result -> Assert.assertEquals(2, result.getTotal())) + .verifyComplete(); + + repository.findById("reactive-save-1") + .as(StepVerifier::create) + .assertNext(entity -> Assert.assertArrayEquals(new Short[]{21, 22}, entity.getTags())) + .verifyComplete(); + + repository.insertBatch(Arrays.asList( + entity("reactive-batch-1", new Short[]{31, 32}, new String[]{"batch", "reactive-1"}), + entity("reactive-batch-2", new Short[]{32, 33}, new String[]{"batch", "reactive-2"}) + )) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + repository.updateById("reactive-save-2", + Mono.just(entity("reactive-save-2", + new Short[]{15, 16}, + new String[]{"vehicle", "updated"}))) + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + repository.findById("reactive-save-2") + .as(StepVerifier::create) + .assertNext(entity -> Assert.assertArrayEquals( + new String[]{"vehicle", "updated"}, + entity.getKeywords())) + .verifyComplete(); + + repository + .createQuery() + .where(ArrayEntity::getTags, new Short[]{31, 32}) + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> Assert.assertEquals(List.of("reactive-batch-1"), ids)) + .verifyComplete(); + + repository.createUpdate() + .set(ArrayEntity::getKeywords, new String[]{"dsl", "reactive"}) + .where(ArrayEntity::getId, "reactive-batch-2") + .execute() + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + repository.findById("reactive-batch-2") + .as(StepVerifier::create) + .assertNext(entity -> Assert.assertArrayEquals( + new String[]{"dsl", "reactive"}, + entity.getKeywords())) + .verifyComplete(); + + repository.createDelete() + .where(ArrayEntity::getId, "reactive-batch-1") + .execute() + .as(StepVerifier::create) + .expectNext(1) + .verifyComplete(); + + repository.findById("reactive-batch-1") + .as(StepVerifier::create) + .verifyComplete(); + + repository.deleteById(Flux.just("reactive-save-1", "reactive-save-3")) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + + repository.deleteById(Arrays.asList("reactive-save-2", "reactive-batch-2")) + .as(StepVerifier::create) + .expectNext(2) + .verifyComplete(); + } finally { + try { + executor.execute(Mono.just(SqlRequests.of("drop table test_pg_array_basic"))).block(); + } catch (Exception ignore) { + } + } + } + + private ArrayEntity entity(String id, Short[] tags, String[] keywords) { + ArrayEntity entity = new ArrayEntity(); + entity.setId(id); + entity.setTags(tags); + entity.setKeywords(keywords); + return entity; + } + private SyncRepository createSyncRepository(RDBDatabaseMetadata database, DatabaseOperator operator) { JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser(); parser.setDatabaseMetadata(database); From 305d875197ff1982aa148c7832b33805f1ce440b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Tue, 19 May 2026 13:10:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(test):=20=E4=BF=AE=E5=A4=8D=20PostgreSQ?= =?UTF-8?q?L=20actions=20=E5=A4=B1=E8=B4=A5=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../executor/jdbc/JdbcSqlExecutorHelper.java | 6 +++++- .../insert/BatchInsertSqlBuilder.java | 18 ++++++++++------- .../hswebframework/ezorm/rdb/Containers.java | 4 ++-- .../ezorm/rdb/supports/BasicCommonTests.java | 20 +++++++++++-------- .../PostgresqlConnectionProvider.java | 3 --- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java index f21de54a..b1e4b954 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/executor/jdbc/JdbcSqlExecutorHelper.java @@ -41,7 +41,11 @@ protected static void preparedStatementParameter(PreparedStatement statement, Ob } else if (object instanceof JdbcParameterBinder binder) { binder.bind(statement, index++); } else if (object instanceof NullValue nullValue) { - statement.setNull(index++, nullValue.getDataType().getSqlType().getVendorTypeNumber()); + int sqlType = nullValue.getDataType().getSqlType().getVendorTypeNumber(); + if (nullValue.getType() == LongCharSequence.class) { + sqlType = Types.LONGVARCHAR; + } + statement.setNull(index++, sqlType); } else if (object instanceof Date) { statement.setTimestamp(index++, new java.sql.Timestamp(((Date) object).getTime())); } else if (object instanceof byte[] b) { diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java index 9e51329a..f7d867db 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java @@ -114,22 +114,26 @@ public SqlRequest build(InsertOperatorParameter parameter) { int indexSize = primaryIndex.size(); int vSize = values.size(); - if(shoudCheckDumplicateKey(primaryIndex,values)){ + if (shoudCheckDumplicateKey(primaryIndex, values)) { // id if (indexSize == 1) { int idx = primaryIndex.get(0); - Object idValue = values.get(idx); - if (idValue != null && vSize > idx && !duplicatePrimary.add(idValue)) { - continue; + if (idx < vSize) { + Object idValue = values.get(idx); + if (idValue != null && !duplicatePrimary.add(idValue)) { + continue; + } } } // 唯一索引? else if (indexSize >= 1) { Set dis = Sets.newHashSetWithExpectedSize(indexSize); for (Integer i : primaryIndex) { - Object value = values.get(i); - if (vSize > i && value != null) { - dis.add(value); + if (i < vSize) { + Object value = values.get(i); + if (value != null) { + dis.add(value); + } } } // 存在重复数据 ? diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/Containers.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/Containers.java index 9eaa78ba..74f89cd2 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/Containers.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/Containers.java @@ -28,8 +28,8 @@ public static GenericContainer newPostgresql(String version) { .withEnv("POSTGRES_DB", "ezorm") .withCommand("postgres", "-c", "max_connections=500") .withExposedPorts(5432) - .waitingFor(Wait.forListeningPort()); -// .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*",1)); + .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\n", 2) + .withStartupTimeout(Duration.ofMinutes(2))); } @SneakyThrows diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/BasicCommonTests.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/BasicCommonTests.java index c799a072..8dd77dd8 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/BasicCommonTests.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/BasicCommonTests.java @@ -217,17 +217,21 @@ public void testRepositoryBatchSave() { Assert.assertEquals(2, repository.save(e1, e2).getTotal()); - getSqlExecutor() - .select("select * from entity_test_table where id in(?,?)",e1.getId(),e2.getId()) - .forEach(System.out::println); - - repository + List saved = repository .createQuery() - .select("id","tags") + .select("*") .where() .in("id", Arrays.asList(e1.getId(), e2.getId())) - .fetch() - .forEach(System.out::println); + .fetch(); + + Assert.assertEquals(2, saved.size()); + + java.util.Map entityMap = saved + .stream() + .collect(Collectors.toMap(BasicTestEntity::getId, entity -> entity)); + + Assert.assertEquals(e1.getTags(), entityMap.get(e1.getId()).getTags()); + Assert.assertNull(entityMap.get(e2.getId()).getTags()); } diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlConnectionProvider.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlConnectionProvider.java index 705aca06..55c9d9d6 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlConnectionProvider.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlConnectionProvider.java @@ -6,7 +6,6 @@ import org.junit.Assert; import org.postgresql.Driver; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; import java.sql.Connection; import java.sql.DriverManager; @@ -18,8 +17,6 @@ public class PostgresqlConnectionProvider implements ConnectionProvider { static { Assert.assertTrue(Driver.isRegistered()); GenericContainer container = Containers.newPostgresql("11"); - - container.waitingFor(Wait.forListeningPort()); container.start(); port = container.getMappedPort(5432); } From baa35cceeb1f2fe77c5e32f19e8ec8441d31a335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Tue, 19 May 2026 14:54:45 +0800 Subject: [PATCH 5/5] feat(PostgreSQL): support array term types and upgrade jacoco --- .../ezorm/core/Conditional.java | 126 ++++++++++++++ .../ezorm/core/NestConditional.java | 126 ++++++++++++++ .../ezorm/core/param/TermType.java | 42 +++++ .../ezorm/core/dsl/QueryTest.java | 12 +- .../PostgresqlArrayTermFragmentBuilder.java | 150 +++++++++++++++++ .../postgres/PostgresqlSchemaMetadata.java | 11 ++ .../postgres/array/PostgresqlArrayTest.java | 158 ++++++++++++++++++ pom.xml | 4 +- 8 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayTermFragmentBuilder.java diff --git a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/Conditional.java b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/Conditional.java index cb953c02..41c524fd 100644 --- a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/Conditional.java +++ b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/Conditional.java @@ -869,6 +869,132 @@ default T notIn(StaticMethodReferenceColumn column, Collection values) return accept(column, TermType.nin, values); } + /** + * 追加包含条件(静态方法引用方式): column contains ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T contains(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.contains, value); + } + + /** + * 追加包含条件(方法引用方式): column contains ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T contains(MethodReferenceColumn column) { + return accept(column, TermType.contains); + } + + /** + * 追加不包含条件(静态方法引用方式): column not contains ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T notContains(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.ncontains, value); + } + + /** + * 追加不包含条件(方法引用方式): column not contains ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T notContains(MethodReferenceColumn column) { + return accept(column, TermType.ncontains); + } + + /** + * 追加被包含条件(静态方法引用方式): column contained by ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T contained(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.contained, value); + } + + /** + * 追加被包含条件(方法引用方式): column contained by ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T contained(MethodReferenceColumn column) { + return accept(column, TermType.contained); + } + + /** + * 追加不被包含条件(静态方法引用方式): column not contained by ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T notContained(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.ncontained, value); + } + + /** + * 追加不被包含条件(方法引用方式): column not contained by ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T notContained(MethodReferenceColumn column) { + return accept(column, TermType.ncontained); + } + + /** + * 追加相交条件(静态方法引用方式): column overlap ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T overlap(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.overlap, value); + } + + /** + * 追加相交条件(方法引用方式): column overlap ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T overlap(MethodReferenceColumn column) { + return accept(column, TermType.overlap); + } + + /** + * 追加不相交条件(静态方法引用方式): column not overlap ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前条件构造器 + */ + default T notOverlap(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.noverlap, value); + } + + /** + * 追加不相交条件(方法引用方式): column not overlap ? + * + * @param column 列引用 + * @return 当前条件构造器 + */ + default T notOverlap(MethodReferenceColumn column) { + return accept(column, TermType.noverlap); + } + /** * 追加为空条件(静态方法引用方式): column = '' * diff --git a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/NestConditional.java b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/NestConditional.java index c3513293..fcb40fd4 100644 --- a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/NestConditional.java +++ b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/NestConditional.java @@ -685,6 +685,132 @@ default NestConditional notIn(MethodReferenceColumn column) { return accept(column, TermType.nin); } + /** + * 追加包含条件(方法引用方式): column contains ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional contains(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.contains, value); + } + + /** + * 追加包含条件(方法引用方式): column contains ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional contains(MethodReferenceColumn column) { + return accept(column, TermType.contains); + } + + /** + * 追加不包含条件(方法引用方式): column not contains ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional notContains(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.ncontains, value); + } + + /** + * 追加不包含条件(方法引用方式): column not contains ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional notContains(MethodReferenceColumn column) { + return accept(column, TermType.ncontains); + } + + /** + * 追加被包含条件(方法引用方式): column contained by ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional contained(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.contained, value); + } + + /** + * 追加被包含条件(方法引用方式): column contained by ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional contained(MethodReferenceColumn column) { + return accept(column, TermType.contained); + } + + /** + * 追加不被包含条件(方法引用方式): column not contained by ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional notContained(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.ncontained, value); + } + + /** + * 追加不被包含条件(方法引用方式): column not contained by ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional notContained(MethodReferenceColumn column) { + return accept(column, TermType.ncontained); + } + + /** + * 追加相交条件(方法引用方式): column overlap ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional overlap(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.overlap, value); + } + + /** + * 追加相交条件(方法引用方式): column overlap ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional overlap(MethodReferenceColumn column) { + return accept(column, TermType.overlap); + } + + /** + * 追加不相交条件(方法引用方式): column not overlap ? + * + * @param column 列引用 + * @param value 条件值 + * @return 当前嵌套条件构造器 + */ + default NestConditional notOverlap(StaticMethodReferenceColumn column, Object value) { + return accept(column, TermType.noverlap, value); + } + + /** + * 追加不相交条件(方法引用方式): column not overlap ? + * + * @param column 列引用 + * @return 当前嵌套条件构造器 + */ + default NestConditional notOverlap(MethodReferenceColumn column) { + return accept(column, TermType.noverlap); + } + /** * 追加为空条件(方法引用方式): column = '' or column is null * diff --git a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/param/TermType.java b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/param/TermType.java index 2557b3fa..e838734f 100644 --- a/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/param/TermType.java +++ b/hsweb-easy-orm-core/src/main/java/org/hswebframework/ezorm/core/param/TermType.java @@ -120,4 +120,46 @@ public interface TermType { */ String nbtw = "nbtw"; + /** + * contains + * + * @since 4.2 + */ + String contains = "contains"; + + /** + * not contains + * + * @since 4.2 + */ + String ncontains = "ncontains"; + + /** + * contained by + * + * @since 4.2 + */ + String contained = "contained"; + + /** + * not contained by + * + * @since 4.2 + */ + String ncontained = "ncontained"; + + /** + * overlap + * + * @since 4.2 + */ + String overlap = "overlap"; + + /** + * not overlap + * + * @since 4.2 + */ + String noverlap = "noverlap"; + } diff --git a/hsweb-easy-orm-core/src/test/java/org/hswebframework/ezorm/core/dsl/QueryTest.java b/hsweb-easy-orm-core/src/test/java/org/hswebframework/ezorm/core/dsl/QueryTest.java index e989b8bc..0f796bc5 100644 --- a/hsweb-easy-orm-core/src/test/java/org/hswebframework/ezorm/core/dsl/QueryTest.java +++ b/hsweb-easy-orm-core/src/test/java/org/hswebframework/ezorm/core/dsl/QueryTest.java @@ -55,6 +55,16 @@ public void testWhere() { assertSingleCondition(query -> query.in(TestEntity::getId, Arrays.asList(1, 2, 3)), "in", Arrays.asList(1, 2, 3)); } + //array-like terms + { + assertSingleCondition(query -> query.contains(TestEntity::getId, 1), "contains", 1); + assertSingleCondition(query -> query.notContains(TestEntity::getId, 1), "ncontains", 1); + assertSingleCondition(query -> query.contained(TestEntity::getId, 1), "contained", 1); + assertSingleCondition(query -> query.notContained(TestEntity::getId, 1), "ncontained", 1); + assertSingleCondition(query -> query.overlap(TestEntity::getId, 1), "overlap", 1); + assertSingleCondition(query -> query.notOverlap(TestEntity::getId, 1), "noverlap", 1); + } + //gt lt { assertSingleCondition(query -> query.gt(TestEntity::getId, 1), "gt", 1); @@ -85,4 +95,4 @@ static class TestEntity implements Serializable { String id; String name; } -} \ No newline at end of file +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayTermFragmentBuilder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayTermFragmentBuilder.java new file mode 100644 index 00000000..1189c96c --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlArrayTermFragmentBuilder.java @@ -0,0 +1,150 @@ +package org.hswebframework.ezorm.rdb.supports.postgres; + +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.List; + +public class PostgresqlArrayTermFragmentBuilder extends AbstractTermFragmentBuilder { + + public static final PostgresqlArrayTermFragmentBuilder in = new PostgresqlArrayTermFragmentBuilder( + TermType.in, + "数组包含任一值", + null, + false + ); + + public static final PostgresqlArrayTermFragmentBuilder notIn = new PostgresqlArrayTermFragmentBuilder( + TermType.nin, + "数组不包含任一值", + null, + true + ); + + public static final PostgresqlArrayTermFragmentBuilder contains = new PostgresqlArrayTermFragmentBuilder( + TermType.contains, + "数组包含", + Operator.contains, + false + ); + + public static final PostgresqlArrayTermFragmentBuilder notContains = new PostgresqlArrayTermFragmentBuilder( + TermType.ncontains, + "数组不包含", + Operator.contains, + true + ); + + public static final PostgresqlArrayTermFragmentBuilder contained = new PostgresqlArrayTermFragmentBuilder( + TermType.contained, + "数组被包含", + Operator.contained, + false + ); + + public static final PostgresqlArrayTermFragmentBuilder notContained = new PostgresqlArrayTermFragmentBuilder( + TermType.ncontained, + "数组不被包含", + Operator.contained, + true + ); + + public static final PostgresqlArrayTermFragmentBuilder overlap = new PostgresqlArrayTermFragmentBuilder( + TermType.overlap, + "数组相交", + Operator.overlap, + false + ); + + public static final PostgresqlArrayTermFragmentBuilder notOverlap = new PostgresqlArrayTermFragmentBuilder( + TermType.noverlap, + "数组不相交", + Operator.overlap, + true + ); + + private final Operator operator; + + private final boolean not; + + public PostgresqlArrayTermFragmentBuilder(String termType, + String name, + Operator operator, + boolean not) { + super(termType, name); + this.operator = operator; + this.not = not; + } + + @Override + public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { + Object value = term.getValue(); + if (isEmptyValue(value)) { + return EmptySqlFragments.INSTANCE; + } + PrepareSqlFragments fragments = PrepareSqlFragments.of(); + if (not) { + fragments.addSql("not", "("); + } + fragments.addSql(columnFullName, resolveOperator(term).sql); + appendPrepareOrNative(fragments, encodeValue(column, value)); + if (not) { + fragments.addSql(")"); + } + return fragments; + } + + private Object encodeValue(RDBColumnMetadata column, Object value) { + if (value instanceof NativeSql) { + return value; + } + return column.encode(value); + } + + private Operator resolveOperator(Term term) { + if (operator != null) { + return operator; + } + List options = term.getOptions(); + if (options.contains("all") || options.contains("contains")) { + return Operator.contains; + } + if (options.contains("contained")) { + return Operator.contained; + } + return Operator.overlap; + } + + private boolean isEmptyValue(Object value) { + if (value == null) { + return true; + } + if (value instanceof Collection collection) { + return collection.isEmpty(); + } + if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } + return false; + } + + private enum Operator { + contains("@>"), + contained("<@"), + overlap("&&"); + + private final String sql; + + Operator(String sql) { + this.sql = sql; + } + } +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlSchemaMetadata.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlSchemaMetadata.java index 3d32f605..8a428934 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlSchemaMetadata.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlSchemaMetadata.java @@ -8,6 +8,7 @@ import org.hswebframework.ezorm.rdb.metadata.ValueCodecFactory; import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; import org.hswebframework.ezorm.rdb.operator.CompositeExceptionTranslation; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NotFillOrNullFragmentBuilder; import org.hswebframework.ezorm.rdb.utils.FeatureUtils; import java.util.Optional; @@ -59,6 +60,16 @@ public RDBTableMetadata newTable(String name) { column.addFeature(PostgresqlEnumInFragmentBuilder.in); column.addFeature(PostgresqlEnumInFragmentBuilder.notIn); } + if (column.getType() instanceof PostgresqlArrayType) { + column.addFeature(PostgresqlArrayTermFragmentBuilder.in); + column.addFeature(new NotFillOrNullFragmentBuilder(PostgresqlArrayTermFragmentBuilder.notIn)); + column.addFeature(PostgresqlArrayTermFragmentBuilder.contains); + column.addFeature(new NotFillOrNullFragmentBuilder(PostgresqlArrayTermFragmentBuilder.notContains)); + column.addFeature(PostgresqlArrayTermFragmentBuilder.contained); + column.addFeature(new NotFillOrNullFragmentBuilder(PostgresqlArrayTermFragmentBuilder.notContained)); + column.addFeature(PostgresqlArrayTermFragmentBuilder.overlap); + column.addFeature(new NotFillOrNullFragmentBuilder(PostgresqlArrayTermFragmentBuilder.notOverlap)); + } if (column.getValueCodec() instanceof VectorType) { column.addFeature(new PostgresqlVectorDistanceFunctionFragmentBuilder()); PostgresqlVectorDistanceTermFragmentBuilder.ALL.values().forEach(column::addFeature); diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java index 5b7c921a..56166476 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/array/PostgresqlArrayTest.java @@ -6,6 +6,7 @@ import org.hswebframework.ezorm.core.DefaultValueGenerator; import org.hswebframework.ezorm.core.RuntimeDefaultValue; import org.hswebframework.ezorm.core.meta.ObjectMetadata; +import org.hswebframework.ezorm.core.param.TermType; import org.hswebframework.ezorm.rdb.TestReactiveSqlExecutor; import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; import org.hswebframework.ezorm.rdb.executor.SqlRequests; @@ -165,6 +166,88 @@ public void testSyncRepositoryCrud() { } } + @Test + public void testSyncArrayTerms() { + RDBDatabaseMetadata database = getSyncDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + SyncSqlExecutor executor = getSyncSqlExecutor(); + try { + SyncRepository repository = createSyncRepository(database, operator); + repository.insertBatch(Arrays.asList( + entity("term-sync-1", new Short[]{1, 2, 3}, new String[]{"person", "white shirt", "glasses"}), + entity("term-sync-2", new Short[]{2, 4}, new String[]{"person", "black jacket"}), + entity("term-sync-3", new Short[]{5}, new String[]{"vehicle", "white"}) + )); + + assertIds(repository.createQuery() + .contains(ArrayEntity::getTags, new Short[]{1, 2}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1"); + + assertIds(repository.createQuery() + .contained(ArrayEntity::getTags, new Short[]{1, 2, 3, 4}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1", + "term-sync-2"); + + assertIds(repository.createQuery() + .overlap(ArrayEntity::getTags, new Short[]{2, 5}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1", + "term-sync-2", + "term-sync-3"); + + assertIds(repository.createQuery() + .notOverlap(ArrayEntity::getTags, new Short[]{5}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1", + "term-sync-2"); + + assertIds(repository.createQuery() + .in(ArrayEntity::getTags, new Short[]{2, 5}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1", + "term-sync-2", + "term-sync-3"); + + assertIds(repository.createQuery() + .and("tags", TermType.in + "$all", new Short[]{1, 2}) + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1"); + + assertIds(repository.createQuery() + .contains(ArrayEntity::getKeywords, "white shirt") + .fetch() + .stream() + .map(ArrayEntity::getId) + .collect(Collectors.toList()), + "term-sync-1"); + } finally { + try { + executor.execute(SqlRequests.of("drop table test_pg_array_basic")); + } catch (Exception ignore) { + } + } + } + @Test public void testReactiveArrayField() { RDBDatabaseMetadata database = getReactiveDatabase(); @@ -311,6 +394,75 @@ public void testReactiveRepositoryCrud() { } } + @Test + public void testReactiveArrayTerms() { + RDBDatabaseMetadata database = getReactiveDatabase(); + DatabaseOperator operator = DefaultDatabaseOperator.of(database); + ReactiveSqlExecutor executor = getReactiveSqlExecutor(); + try { + ReactiveRepository repository = createReactiveRepository(database, operator); + + repository.insertBatch(Arrays.asList( + entity("term-reactive-1", new Short[]{1, 2, 3}, new String[]{"person", "white shirt", "glasses"}), + entity("term-reactive-2", new Short[]{2, 4}, new String[]{"person", "black jacket"}), + entity("term-reactive-3", new Short[]{5}, new String[]{"vehicle", "white"}) + )) + .as(StepVerifier::create) + .expectNext(3) + .verifyComplete(); + + repository.createQuery() + .contains(ArrayEntity::getTags, new Short[]{1, 2}) + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> assertIds(ids, "term-reactive-1")) + .verifyComplete(); + + repository.createQuery() + .overlap(ArrayEntity::getTags, new Short[]{2, 5}) + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> assertIds(ids, "term-reactive-1", "term-reactive-2", "term-reactive-3")) + .verifyComplete(); + + repository.createQuery() + .and("tags", TermType.in + "$all", new Short[]{1, 2}) + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> assertIds(ids, "term-reactive-1")) + .verifyComplete(); + + repository.createQuery() + .in(ArrayEntity::getTags, new Short[]{2, 5}) + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> assertIds(ids, "term-reactive-1", "term-reactive-2", "term-reactive-3")) + .verifyComplete(); + + repository.createQuery() + .contains(ArrayEntity::getKeywords, "white shirt") + .fetch() + .map(ArrayEntity::getId) + .collectList() + .as(StepVerifier::create) + .assertNext(ids -> assertIds(ids, "term-reactive-1")) + .verifyComplete(); + } finally { + try { + executor.execute(Mono.just(SqlRequests.of("drop table test_pg_array_basic"))).block(); + } catch (Exception ignore) { + } + } + } + private ArrayEntity entity(String id, Short[] tags, String[] keywords) { ArrayEntity entity = new ArrayEntity(); entity.setId(id); @@ -319,6 +471,12 @@ private ArrayEntity entity(String id, Short[] tags, String[] keywords) { return entity; } + private void assertIds(List ids, String... expected) { + List actual = ids.stream().sorted().collect(Collectors.toList()); + List expect = Arrays.stream(expected).sorted().collect(Collectors.toList()); + Assert.assertEquals(expect, actual); + } + private SyncRepository createSyncRepository(RDBDatabaseMetadata database, DatabaseOperator operator) { JpaEntityTableMetadataParser parser = new JpaEntityTableMetadataParser(); parser.setDatabaseMetadata(database); diff --git a/pom.xml b/pom.xml index 347e8934..dda4cd71 100644 --- a/pom.xml +++ b/pom.xml @@ -124,7 +124,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.14 @@ -355,4 +355,4 @@ - \ No newline at end of file +