diff --git a/README.md b/README.md index 8e7447eb..a729c775 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,7 @@ List> dataList= operator.dml().query() # 使用 建议配合[hsweb4](https://github.com/hs-web/hsweb-framework/tree/4.0.x)使用. + +## 聚合计数 + +内置 `count` 函数默认生成 `count(列)`。查询方可通过 `SelectColumn.option("countRows", true)` 显式请求行数计数;表元数据确认目标列非空时生成 `count(*)`,此时 `countRows` 优先于 `distinct`、`arg` 等选项,其他情况沿用原有生成逻辑。调用方需确保关联查询不会将目标列扩展为 NULL。自定义 `count` 函数仍按其自身实现处理该选项;旧版本不识别 `countRows` 时继续按原有选项生成 SQL。实际性能取决于索引和执行计划。 diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/metadata/RDBFeatures.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/metadata/RDBFeatures.java index be993619..985dd04f 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/metadata/RDBFeatures.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/metadata/RDBFeatures.java @@ -2,6 +2,7 @@ import org.hswebframework.ezorm.core.FeatureId; import org.hswebframework.ezorm.core.param.TermType; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.function.CountFunctionFragmentBuilder; import org.hswebframework.ezorm.rdb.operator.builder.fragments.function.SimpleFunctionFragmentBuilder; import org.hswebframework.ezorm.rdb.operator.builder.fragments.query.QuerySqlBuilder; import org.hswebframework.ezorm.rdb.operator.builder.fragments.query.QuerySqlFragmentBuilder; @@ -51,7 +52,7 @@ public interface RDBFeatures { /*===============函数==============*/ /*==聚合函数==*/ - SimpleFunctionFragmentBuilder count = new SimpleFunctionFragmentBuilder("count", "计数"); + SimpleFunctionFragmentBuilder count = new CountFunctionFragmentBuilder(); SimpleFunctionFragmentBuilder sum = new SimpleFunctionFragmentBuilder("sum", "求和"); SimpleFunctionFragmentBuilder avg = new SimpleFunctionFragmentBuilder("avg", "平均值"); SimpleFunctionFragmentBuilder max = new SimpleFunctionFragmentBuilder("max", "最大值"); diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilder.java new file mode 100644 index 00000000..3aa35ae4 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilder.java @@ -0,0 +1,31 @@ +package org.hswebframework.ezorm.rdb.operator.builder.fragments.function; + +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; + +import java.util.Map; + +/** + * 内置 count 函数。调用方明确启用 countRows,且查询不会使非空列因关联变为 NULL 时, + * 可以将列计数生成为行数计数,并优先于 distinct、arg 等选项;其他情况沿用普通 count 的行为。 + */ +public class CountFunctionFragmentBuilder extends SimpleFunctionFragmentBuilder { + + public static final String COUNT_ROWS = "countRows"; + + public CountFunctionFragmentBuilder() { + super("count", "计数"); + } + + @Override + public SqlFragments create(String columnFullName, RDBColumnMetadata metadata, Map opts) { + if (columnFullName != null + && metadata != null + && metadata.isNotNull() + && opts != null + && Boolean.TRUE.equals(opts.get(COUNT_ROWS))) { + return SqlFragments.single("count(*)"); + } + return super.create(columnFullName, metadata, opts); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilderTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilderTest.java new file mode 100644 index 00000000..146a7a8e --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/function/CountFunctionFragmentBuilderTest.java @@ -0,0 +1,193 @@ +package org.hswebframework.ezorm.rdb.operator.builder.fragments.function; + +import org.hswebframework.ezorm.rdb.metadata.JdbcDataType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBFeatures; +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.DefaultDatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.dml.query.SelectColumn; +import org.hswebframework.ezorm.rdb.supports.postgres.PostgresqlSchemaMetadata; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.sql.JDBCType; +import java.util.Collections; + +public class CountFunctionFragmentBuilderTest { + + private RDBDatabaseMetadata database; + private PostgresqlSchemaMetadata schema; + private RDBTableMetadata table; + + @Before + public void init() { + database = new RDBDatabaseMetadata(Dialect.POSTGRES); + schema = new PostgresqlSchemaMetadata("public"); + database.addSchema(schema); + database.setCurrentSchema(schema); + + table = schema.newTable("metrics"); + addColumn(table, "id", true); + addColumn(table, "value", false); + schema.addTable(table); + } + + @Test + public void shouldOnlyRewriteRequestedNonNullCount() { + String normalSql = querySql(count("id", "total")); + Assert.assertTrue(normalSql, normalSql.contains("count( " + table.getColumnNow("id").getFullName() + " ) as \"total\"")); + Assert.assertFalse(normalSql, normalSql.contains("count(*)")); + + SelectColumn countRows = count("id", "total"); + countRows.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + String rowsSql = querySql(countRows); + Assert.assertTrue(rowsSql, rowsSql.contains("count(*) as \"total\"")); + } + + @Test + public void shouldKeepNullableColumnCount() { + SelectColumn countRows = count("value", "total"); + countRows.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + + String sql = querySql(countRows); + Assert.assertTrue(sql, sql.contains("count( " + table.getColumnNow("value").getFullName() + " ) as \"total\"")); + Assert.assertFalse(sql, sql.contains("count(*)")); + } + + @Test + public void shouldRequireEnabledOptionAndResolvedColumn() { + SelectColumn disabled = count("id", "total"); + disabled.option(CountFunctionFragmentBuilder.COUNT_ROWS, false); + String disabledSql = querySql(disabled); + Assert.assertTrue(disabledSql, disabledSql.contains("count( " + table.getColumnNow("id").getFullName() + " )")); + + Assert.assertTrue(RDBFeatures.count + .create(null, + table.getColumnNow("id"), + Collections.singletonMap(CountFunctionFragmentBuilder.COUNT_ROWS, true)) + .isEmpty()); + } + + @Test + public void shouldPreferCountRowsOverDistinctAndArg() { + SelectColumn distinct = count("id", "total"); + distinct.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + distinct.option("distinct", true); + String distinctSql = querySql(distinct); + Assert.assertTrue(distinctSql, distinctSql.contains("count(*) as \"total\"")); + + SelectColumn arg = count("id", "total"); + arg.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + arg.option("arg", 1); + String argSql = querySql(arg); + Assert.assertTrue(argSql, argSql.contains("count(*) as \"total\"")); + } + + @Test + public void shouldKeepDistinctAndArgWithoutCountRows() { + SelectColumn distinct = count("id", "total"); + distinct.option("distinct", true); + String distinctSql = querySql(distinct); + Assert.assertTrue(distinctSql, distinctSql.contains("count( distinct " + table.getColumnNow("id").getFullName() + " )")); + + SelectColumn arg = count("id", "total"); + arg.option("arg", 1); + String argSql = querySql(arg); + Assert.assertTrue(argSql, argSql.contains("count( 1 ) as \"total\"")); + } + + @Test + public void shouldUseOriginalOptionsForNullableColumn() { + SelectColumn distinct = count("value", "total"); + distinct.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + distinct.option("distinct", true); + String distinctSql = querySql(distinct); + Assert.assertTrue(distinctSql, distinctSql.contains("count( distinct " + table.getColumnNow("value").getFullName() + " )")); + + SelectColumn arg = count("value", "total"); + arg.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + arg.option("arg", 1); + String argSql = querySql(arg); + Assert.assertTrue(argSql, argSql.contains("count( 1 ) as \"total\"")); + } + + @Test + public void shouldRespectCustomCountFunction() { + schema.addFeature(new SimpleFunctionFragmentBuilder("count", "schema_count", "自定义计数")); + SelectColumn countRows = count("id", "total"); + countRows.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + String schemaSql = querySql(countRows); + Assert.assertTrue(schemaSql, schemaSql.contains("schema_count( " + table.getColumnNow("id").getFullName() + " )")); + + table.addFeature(new SimpleFunctionFragmentBuilder("count", "table_count", "自定义计数")); + String tableSql = querySql(countRows); + Assert.assertTrue(tableSql, tableSql.contains("table_count( " + table.getColumnNow("id").getFullName() + " )")); + } + + @Test + public void shouldKeepLegacyBuilderBehaviorForUnknownOption() { + schema.addFeature(new SimpleFunctionFragmentBuilder("count", "计数")); + SelectColumn countRows = count("id", "total"); + countRows.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + + String sql = querySql(countRows); + Assert.assertTrue(sql, sql.contains("count( " + table.getColumnNow("id").getFullName() + " )")); + Assert.assertFalse(sql, sql.contains("count(*)")); + } + + @Test + public void shouldKeepOptInBehaviorForOtherDialects() { + RDBDatabaseMetadata h2Database = new RDBDatabaseMetadata(Dialect.H2); + RDBSchemaMetadata h2Schema = new RDBSchemaMetadata("PUBLIC"); + h2Database.addSchema(h2Schema); + h2Database.setCurrentSchema(h2Schema); + RDBTableMetadata h2Table = h2Schema.newTable("metrics"); + addColumn(h2Table, "id", true); + h2Schema.addTable(h2Table); + + String normalSql = DefaultDatabaseOperator.of(h2Database) + .dml() + .query("metrics") + .select(count("id", "total")) + .getSql() + .getSql(); + Assert.assertTrue(normalSql, normalSql.contains("count( " + h2Table.getColumnNow("id").getFullName() + " )")); + + SelectColumn countRows = count("id", "total"); + countRows.option(CountFunctionFragmentBuilder.COUNT_ROWS, true); + String rowsSql = DefaultDatabaseOperator.of(h2Database) + .dml() + .query("metrics") + .select(countRows) + .getSql() + .getSql(); + Assert.assertTrue(rowsSql, rowsSql.contains("count(*)")); + } + + private String querySql(SelectColumn column) { + return DefaultDatabaseOperator.of(database) + .dml() + .query("metrics") + .select(column) + .getSql() + .getSql(); + } + + private SelectColumn count(String property, String alias) { + SelectColumn column = SelectColumn.of(property, alias); + column.setFunction("count"); + return column; + } + + private void addColumn(RDBTableMetadata target, String name, boolean notNull) { + RDBColumnMetadata column = target.newColumn(); + column.setName(name); + column.setType(JdbcDataType.of(JDBCType.VARCHAR, String.class)); + column.setNotNull(notNull); + target.addColumn(column); + } +}