diff --git a/hsweb-commons/hsweb-commons-crud/pom.xml b/hsweb-commons/hsweb-commons-crud/pom.xml index f92d08a4b..bb3c9e839 100644 --- a/hsweb-commons/hsweb-commons-crud/pom.xml +++ b/hsweb-commons/hsweb-commons-crud/pom.xml @@ -143,6 +143,12 @@ spring-webmvc true + + + com.github.jsqlparser + jsqlparser + 4.6 + \ No newline at end of file diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java index 90663255c..c3ebe0a7e 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/DefaultQueryHelper.java @@ -1,16 +1,26 @@ package org.hswebframework.web.crud.query; import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.hswebframework.ezorm.core.*; import org.hswebframework.ezorm.core.dsl.Query; import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.SqlRequests; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; import org.hswebframework.ezorm.rdb.executor.wrapper.ColumnWrapperContext; import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.mapping.defaults.record.DefaultRecord; +import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBFeatureType; import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.builder.Paginator; import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; import org.hswebframework.ezorm.rdb.operator.dml.Join; import org.hswebframework.ezorm.rdb.operator.dml.JoinType; import org.hswebframework.ezorm.rdb.operator.dml.QueryOperator; @@ -19,7 +29,9 @@ import org.hswebframework.ezorm.rdb.operator.dml.query.Selects; import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.bean.FastBeanCopier; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,8 +40,9 @@ import javax.persistence.Table; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; -import static org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers.column; @AllArgsConstructor public class DefaultQueryHelper implements QueryHelper { @@ -38,6 +51,27 @@ public class DefaultQueryHelper implements QueryHelper { private final Map, Table> nameMapping = new ConcurrentHashMap<>(); + private final Map analyzerCaches = new ConcurrentReferenceHashMap<>(); + + static final ResultWrapper countWrapper = ResultWrappers.column("_total", i -> ((Number) i).intValue()); + + @Override + public QueryAnalyzer analysis(String selectSql) { + return analyzerCaches.computeIfAbsent(selectSql, sql -> new QueryAnalyzerImpl(database, sql)); + } + + @Override + public NativeQuerySpec select(String sql, Object... args) { + return new NativeQuerySpecImpl<>(this, sql, args, DefaultRecord::new); + } + + @Override + public NativeQuerySpec select(String sql, + Supplier newInstance, + Object... args) { + return new NativeQuerySpecImpl<>(this, sql, args, map -> FastBeanCopier.copy(map, newInstance)); + } + @Override public SelectColumnMapperSpec select(Class resultType) { return new QuerySpec<>(resultType, this); @@ -87,6 +121,94 @@ public class DefaultQueryHelper implements QueryHelper { return arr; } + @RequiredArgsConstructor + static class NativeQuerySpecImpl implements NativeQuerySpec { + private final DefaultQueryHelper parent; + private final String sql; + private final Object[] args; + + private final Function, R> mapper; + + private QueryParamEntity param; + + private SqlRequest createQuerySql() { +// if (param == null) { +// return SqlRequests.prepare(sql, args); +// } + return parent.analysis(sql).inject(param, args); + } + + @Override + public ExecuteSpec where(QueryParamEntity param) { + this.param = param; + return this; + } + + @Override + public Flux fetch() { + return parent + .database + .sql() + .reactive() + .select(createQuerySql(), ResultWrappers.map()) + .map(mapper); + } + + @Override + public Mono> fetchPaged() { + if (param == null) { + return fetchPaged(0, 25); + } + return fetchPaged(param.getPageIndex(), param.getPageSize()); + } + + private SqlRequest createPagingSql(SqlRequest request, int pageIndex, int pageSize) { + PrepareSqlFragments sql = PrepareSqlFragments.of(request.getSql(), request.getParameters()); + + Paginator paginator = parent + .database + .getMetadata() + .getCurrentSchema() + .findFeatureNow(RDBFeatureType.paginator.getId()); + + return paginator.doPaging(sql, pageIndex, pageSize).toRequest(); + } + + @Override + public Mono> fetchPaged(int pageIndex, int pageSize) { + SqlRequest listSql = createQuerySql(); + + SqlRequest countSql = SqlRequests.prepare( + "select count(1) as _total from (" + listSql.getSql() + ") t", + listSql.getParameters() + ); + ReactiveSqlExecutor sqlExecutor = parent.database.sql().reactive(); + + QueryParamEntity param = this.param == null ? new QueryParamEntity().doPaging(pageIndex, pageSize) : this.param; + + if (param.getTotal() != null) { + return sqlExecutor + .select(createPagingSql(listSql, pageIndex, pageSize), ResultWrappers.map()).map(mapper) + .collectList() + .map(list -> PagerResult.of(param.getTotal(), list, param)); + } + + return sqlExecutor + .select(countSql, countWrapper) + .single(0) + .flatMap(total -> { + if (total == 0) { + return Mono.just(PagerResult.of(0, new ArrayList<>(), param)); + } else { + return sqlExecutor + .select(createPagingSql(listSql, param.getPageIndex(), param.getPageSize()), ResultWrappers.map()) + .map(mapper) + .collectList() + .map(list -> PagerResult.of(total, list, param)); + } + }); + } + } static abstract class ColumnMapping { final QuerySpec parent; @@ -353,9 +475,8 @@ public class DefaultQueryHelper implements QueryHelper { ? Mono.just(param.getTotal()) : query.clone() .select(Selects.count1().as("_total")) - .fetch(column("_total", Number.class::cast)) + .fetch(countWrapper) .reactive() - .map(Number::intValue) .single(0); Mono> results = createQuery() diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java new file mode 100644 index 000000000..5ebf97681 --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzer.java @@ -0,0 +1,75 @@ +package org.hswebframework.web.crud.query; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; + +import java.util.List; +import java.util.Map; + +public interface QueryAnalyzer { + + String nativeSql(); + + SqlRequest inject(QueryParamEntity entity,Object... args); + + Select select(); + + List joins(); + + @AllArgsConstructor + @Getter + class Join { + + final String alias; + final Type type; + final Table table; + + // final List on; + + enum Type { + left, right, inner + } + } + + @AllArgsConstructor + @Getter + class Select { + final Map columns; + final Table table; + + } + + @Getter + @AllArgsConstructor + class Table { + final String alias; + + final TableOrViewMetadata metadata; + } + + @AllArgsConstructor + @Getter + class Column { + String name; + String alias; + String owner; + RDBColumnMetadata metadata; + } + + class SelectTable extends Table { + final Map columns; + + public SelectTable(String alias, + Map columns, + TableOrViewMetadata metadata) { + super(alias, metadata); + this.columns = columns; + } + } + + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java new file mode 100644 index 000000000..4d4281b6e --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryAnalyzerImpl.java @@ -0,0 +1,572 @@ +package org.hswebframework.web.crud.query; + +import lombok.SneakyThrows; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.values.ValuesStatement; +import org.hswebframework.ezorm.core.meta.FeatureSupportedMetadata; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.AbstractTermsFragmentBuilder; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.springframework.util.StringUtils; + +import java.util.*; + +import static net.sf.jsqlparser.statement.select.PlainSelect.getStringList; +import static net.sf.jsqlparser.statement.select.PlainSelect.orderByToString; +import static org.hswebframework.ezorm.rdb.operator.builder.fragments.TermFragmentBuilder.createFeatureId; + + +class QueryAnalyzerImpl implements FromItemVisitor, SelectItemVisitor, SelectVisitor, QueryAnalyzer { + + private final DatabaseOperator database; + + private String sql; + + private SelectBody parsed; + + private QueryAnalyzer.Select select; + + private final Map joins = new LinkedHashMap<>(); + + private QueryInjector injector; + + @Override + public String nativeSql() { + return sql; + } + + @Override + public SqlRequest inject(QueryParamEntity entity, Object... args) { + if (injector == null) { + initInjector(); + } + return injector.inject(entity, args); + } + + @Override + public Select select() { + return select; + } + + @Override + public List joins() { + return new ArrayList<>(joins.values()); + } + + QueryAnalyzerImpl(DatabaseOperator database, String sql) { + this(database, parse(sql)); + this.sql = sql; + } + + @SneakyThrows + private static SelectBody parse(String sql) { + return ((net.sf.jsqlparser.statement.select.Select) CCJSqlParserUtil.parse(sql)).getSelectBody(); + } + + QueryAnalyzerImpl(DatabaseOperator database, SelectBody selectBody) { + this.database = database; + if (null != selectBody) { + this.parsed = selectBody; + selectBody.accept(this); + } else { + this.parsed = null; + } + } + + private String parsePlainName(String name) { + if (null == name) { + return null; + } + + if (name.startsWith(database.getMetadata().getDialect().getQuoteStart()) + && name.endsWith(database.getMetadata().getDialect().getQuoteEnd())) { + return name.substring(1, name.length() - 1); + } + return name; + } + + @Override + public void visit(net.sf.jsqlparser.schema.Table tableName) { + String schema = parsePlainName(tableName.getSchemaName()); + RDBSchemaMetadata schemaMetadata; + if (schema != null) { + schemaMetadata = database + .getMetadata() + .getSchema(schema) + .orElseThrow(() -> new IllegalStateException("schema " + schema + " not initialized")); + } else { + schemaMetadata = database.getMetadata().getCurrentSchema(); + } + + String alias = tableName.getAlias() == null ? tableName.getName() : tableName.getAlias().getName(); + + QueryAnalyzer.Table table = new QueryAnalyzer.Table( + parsePlainName(alias), + schemaMetadata + .getTableOrView(parsePlainName(tableName.getName()), false) + .orElseThrow(() -> { + throw new IllegalStateException("table or view " + tableName.getName() + " not found in " + schemaMetadata.getName()); + }) + ); + + select = new QueryAnalyzer.Select(new LinkedHashMap<>(), table); + + } + + // select * from ( select a,b,c from table ) t + @Override + public void visit(SubSelect subSelect) { + SelectBody body = subSelect.getSelectBody(); + QueryAnalyzerImpl sub = new QueryAnalyzerImpl(database, body); + String alias = subSelect.getAlias() == null ? null : subSelect.getAlias().getName(); + + Map columnMap = new LinkedHashMap<>(); + for (Map.Entry entry : sub.select.columns.entrySet()) { + Column val = entry.getValue(); + + columnMap.put(entry.getKey(), + new Column(val.alias, val.getAlias(), val.owner, val.metadata)); + } + + select = new QueryAnalyzer.Select( + new LinkedHashMap<>(), + new QueryAnalyzer.SelectTable( + parsePlainName(alias), + columnMap, + sub.select.table.metadata + ) + ); + } + + @Override + public void visit(SubJoin subjoin) { + for (net.sf.jsqlparser.statement.select.Join join : subjoin.getJoinList()) { + join.getRightItem().accept(this); + } + } + + @Override + public void visit(LateralSubSelect lateralSubSelect) { + + } + + @Override + public void visit(ValuesList valuesList) { + + } + + @Override + public void visit(TableFunction tableFunction) { + + } + + @Override + public void visit(ParenthesisFromItem aThis) { + + } + + @Override + public void visit(AllColumns allColumns) { + putSelectColumns(select.table, select.columns); + + for (QueryAnalyzer.Join value : new HashSet<>(joins.values())) { + putSelectColumns(value.table, select.columns); + } + } + + private void putSelectColumns(String prefix, QueryAnalyzer.Table table, Map container) { + + if (table instanceof QueryAnalyzer.SelectTable) { + QueryAnalyzer.SelectTable selectTable = ((QueryAnalyzer.SelectTable) table); + + for (QueryAnalyzer.Column column : selectTable.columns.values()) { + container.put(column.getAlias(), + new QueryAnalyzer.Column( + column.name, + column.getAlias(), + table.alias, + column.metadata + )); + } + } else { + for (RDBColumnMetadata column : table.metadata.getColumns()) { + container.put(column.getAlias(), + new QueryAnalyzer.Column( + column.getName(), + column.getAlias(), + table.alias, + column + )); + } + } + } + + private void putSelectColumns(QueryAnalyzer.Table table, Map container) { + putSelectColumns(null, table, container); + } + + @Override + public void visit(AllTableColumns allTableColumns) { + net.sf.jsqlparser.schema.Table table = allTableColumns.getTable(); + + QueryAnalyzer.Join join = joins.get(parsePlainName(table.getName())); + + if (join == null) { + throw new IllegalStateException("table " + table.getName() + " not found in join"); + } + putSelectColumns(join.table, select.columns); + } + + private QueryAnalyzer.Table getTable(net.sf.jsqlparser.schema.Table table) { + QueryAnalyzer.Table meta; + if (null == table) { + return select.table; + } + String tableName = parsePlainName(table.getName()); + + if (Objects.equals(tableName, select.table.alias)) { + meta = select.table; + } else { + QueryAnalyzer.Join join = joins.get(tableName); + if (join == null) { + throw new IllegalStateException("table " + table + " not found in from or join"); + } + meta = join.table; + } + return meta; + } + + + static class ExpressionColumn extends Column { + + private final SelectItem expr; + + public ExpressionColumn(String alias, String owner, RDBColumnMetadata metadata, SelectItem expr) { + super(alias, alias, owner, metadata); + this.expr = expr; + } + } + + @Override + public void visit(SelectExpressionItem selectExpressionItem) { + Expression expr = selectExpressionItem.getExpression(); + Alias alias = selectExpressionItem.getAlias(); + + if (!(expr instanceof net.sf.jsqlparser.schema.Column)) { + String aliasName = alias == null ? expr.toString() : alias.getName(); + select.columns.put(aliasName, new ExpressionColumn(aliasName, null, null, selectExpressionItem)); + + return; + } + net.sf.jsqlparser.schema.Column column = ((net.sf.jsqlparser.schema.Column) expr); + + String columnName = parsePlainName(column.getColumnName()); + + QueryAnalyzer.Table table = getTable(column.getTable()); + + String aliasName = alias == null ? columnName : alias.getName(); + + RDBColumnMetadata metadata = table + .getMetadata() + .getColumn(columnName) + .orElse(null); + + if (metadata == null) { + if (table instanceof QueryAnalyzer.SelectTable) { + Column c = ((SelectTable) table).columns.get(columnName); + if (null != c) { + metadata = c.metadata; + } + } + } + + if (metadata == null) { + throw new IllegalStateException("column [" + column.getColumnName() + "] not found in " + table.metadata.getName()); + } + + select.columns.put(aliasName, new QueryAnalyzer.Column(metadata.getName(), aliasName, table.alias, metadata)); + + + } + + @Override + public void visit(PlainSelect select) { + + FromItem from = select.getFromItem(); + + from.accept(this); + + List joinList = select.getJoins(); + + if (joinList != null) { + for (net.sf.jsqlparser.statement.select.Join join : joinList) { + FromItem fromItem = join.getRightItem(); + QueryAnalyzerImpl joinAn = new QueryAnalyzerImpl(database, (SelectBody) null); + fromItem.accept(joinAn); + + Join.Type type; + if (join.isLeft()) { + type = Join.Type.left; + } else if (join.isRight()) { + type = Join.Type.right; + } else if (join.isInner()) { + type = Join.Type.inner; + } else { + type = null; + } + joins.put(joinAn.select.table.alias, new Join(joinAn.select.table.alias, type, joinAn.select.table)); + } + } + + for (SelectItem selectItem : select.getSelectItems()) { + selectItem.accept(this); + } + } + + @Override + public void visit(SetOperationList setOpList) { + + } + + @Override + public void visit(WithItem withItem) { + + } + + @Override + public void visit(ValuesStatement aThis) { + + } + + private void initInjector() { + SimpleQueryInjector injector = new SimpleQueryInjector(); + parsed.accept(injector); + + this.injector = injector; + } + + static class QueryAnalyzerTermsFragmentBuilder extends AbstractTermsFragmentBuilder { + + @Override + public SqlFragments createTermFragments(QueryAnalyzerImpl parameter, List terms) { + return super.createTermFragments(parameter, terms); + } + + @Override + public SqlFragments createTermFragments(QueryAnalyzerImpl parameter, Term term) { + String column = term.getColumn(); + String alias; + + Table table; + String columnName = column; + + if (column.contains(".")) { + String[] split = column.split("[.]"); + alias = split[0]; + columnName = split[1]; + if (Objects.equals(parameter.select.table.alias, alias)) { + table = parameter.select.table; + } else { + QueryAnalyzer.Join join = parameter.joins.get(alias); + if (null != join) { + table = join.table; + } else { + throw new IllegalArgumentException("undefined column [" + column + "]"); + } + } + + } else { + table = parameter.select.table; + alias = parameter.select.table.alias; + } + + if (table instanceof SelectTable) { + SelectTable sTable = ((SelectTable) table); + Column c = sTable.columns.get(columnName); + if (c == null) { + return EmptySqlFragments.INSTANCE; + } + FeatureSupportedMetadata metadata = c.metadata; + if (c.metadata == null) { + metadata = table.metadata; + } + return metadata + .findFeature(createFeatureId(term.getTermType())) + .map(feature -> feature.createFragments(sTable.alias + "." + c.name, c.metadata, term)) + .orElse(EmptySqlFragments.INSTANCE); + } + + return table + .metadata + .getColumn(columnName) + .flatMap(metadata -> metadata + .findFeature(createFeatureId(term.getTermType())) + .map(feature -> feature.createFragments(metadata.getFullName(alias), metadata, term))) + .orElse(EmptySqlFragments.INSTANCE); + } + } + + + static QueryAnalyzerTermsFragmentBuilder TERMS_BUILDER = new QueryAnalyzerTermsFragmentBuilder(); + + class SimpleQueryInjector implements QueryInjector, SelectVisitor { + private String prefix; + private String where; + private String suffix; + + public SimpleQueryInjector() { + + } + + + @Override + public void visit(PlainSelect plainSelect) { + + StringBuilder prefix = new StringBuilder(); + StringBuilder suffix = new StringBuilder(); + + prefix.append("SELECT "); + + + int idx = 0; + + if (plainSelect.getDistinct() != null) { + prefix.append(plainSelect.getDistinct()); + } + Dialect dialect = database.getMetadata().getDialect(); + + for (Map.Entry entry : select.columns.entrySet()) { + if (idx++ > 0) { + prefix.append(","); + } + Column column = entry.getValue(); + if (column instanceof ExpressionColumn) { + prefix.append(((ExpressionColumn) column).expr); + continue; + } +// RDBColumnMetadata column=entry.getValue().metadata; + boolean sameTable = Objects.equals(column.owner, select.table.alias); + + String columnName = column.owner + "." + dialect.quote(column.name); + + prefix.append(columnName) + .append(" as ") + .append(sameTable + ? dialect.quote(column.alias, false) + : dialect.quote(column.owner + "." + column.alias, false)); + } + + // prefix.append(getStringList(plainSelect.getSelectItems())); + + if (null != plainSelect.getFromItem()) { + prefix.append(" FROM "); + + prefix.append(plainSelect.getFromItem()); + + if (plainSelect.getJoins() != null) { + for (net.sf.jsqlparser.statement.select.Join join : plainSelect.getJoins()) { + if (join.isSimple()) { + prefix.append(", ").append(join); + } else { + prefix.append(" ").append(join); + } + } + } + + if (null != plainSelect.getGroupBy()) { + suffix.append(' ').append(plainSelect.getGroupBy()); + } + + suffix.append(' '); + if (null != plainSelect.getHaving()) { + suffix.append(" HAVING ").append(plainSelect.getHaving()); + } + + } + + if (null != plainSelect.getWhere()) { + where = plainSelect.getWhere().toString(); + } + + if (plainSelect.getOrderByElements() != null) { + suffix.append(orderByToString(plainSelect.isOracleSiblings(), plainSelect.getOrderByElements())); + } + + if (plainSelect.getLimit() != null) { + suffix.append(plainSelect.getLimit()); + } + if (plainSelect.getOffset() != null) { + suffix.append(plainSelect.getOffset()); + } + + + this.prefix = prefix.toString(); + this.suffix = suffix.toString(); + + } + + @Override + public void visit(SetOperationList setOpList) { + + } + + @Override + public void visit(WithItem withItem) { + + } + + @Override + public void visit(ValuesStatement aThis) { + + } + + @Override + public SqlRequest inject(QueryParamEntity param, Object... args) { + PrepareSqlFragments sql = PrepareSqlFragments.of(prefix, args); + + SqlFragments fragments = TERMS_BUILDER.createTermFragments(QueryAnalyzerImpl.this, param.getTerms()); + + SqlRequest condition = fragments.toRequest(); + + if (condition.isNotEmpty() || StringUtils.hasText(where)) { + sql.addSql(" WHERE "); + } + if (StringUtils.hasText(where)) { + sql.addSql("("); + sql.addSql(where); + sql.addSql(")"); + } + if (condition.isNotEmpty()) { + if (StringUtils.hasText(where)) { + sql.addSql("AND"); + } + sql.addSql("("); + sql.addFragments(fragments); + sql.addSql(")"); + } + + sql.addSql(suffix); + + return sql.toRequest(); + } + + } + + private interface QueryInjector { + + SqlRequest inject(QueryParamEntity param, Object... args); + + } + +} diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java index 72aa4a111..26157fa85 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/query/QueryHelper.java @@ -1,6 +1,9 @@ package org.hswebframework.web.crud.query; import org.hswebframework.ezorm.core.Conditional; +import org.hswebframework.ezorm.core.dsl.Query; +import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record; +import org.hswebframework.ezorm.rdb.operator.dml.FunctionColumn; import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; @@ -11,6 +14,7 @@ import java.io.Serializable; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; /** * 使用DSL方式链式调用来构建复杂查询 @@ -28,7 +32,7 @@ import java.util.function.Function; * .from(A.class) * .leftJoin(B.class,spec-> spec.is(A::id, B::id)) * .where(dsl->dsl.like(B::getName,'zhang%')) - * .fetch() + * .fetch(); * * } * @@ -37,6 +41,70 @@ import java.util.function.Function; */ public interface QueryHelper { + /** + * 基于SQL创建分析器 + * + * @param selectSql SQL + * @return QueryAnalyzer + */ + QueryAnalyzer analysis(String selectSql); + + /** + * 逻辑和{@link QueryHelper#select(String, Object...)}相同,将查询结果转换为指定的实体类 + * + * @param sql SQL + * @param newInstance 实体类实例化方法 + * @param args 参数 + * @param 实体类型 + * @return NativeQuerySpec + */ + NativeQuerySpec select(String sql, + Supplier newInstance, + Object... args); + + /** + * 创建原生SQL查询器 + *

+ * 预编译参数仅支持?,如果要使用模版,请使用{@link org.hswebframework.ezorm.rdb.executor.SqlRequests#template(String, Object)} + * 构造sql以及参数 + *

{@code
+     *
+     *  Flux records = helper.select("select * from table where type = ?",type)
+     *         //注入动态查询条件
+     *        .where(param)
+     *        //执行查询
+     *        .fetch();
+     * }
+ *

+ * join逻辑: + * + *

{@code
+     *
+     *  helper.select("select t1.id,t2.* from table t1"+
+     *                " left join table2 t2 on t1.id = t2.id") ...
+     *
+     *  将返回结构:
+     *   [
+     *     {"id":"t1.id的值",
+     *     "t2":{
+     *         "c1":"t2的字段",
+     *         ...
+     *     }}
+     *   ]
+     *
+     *
+     * }
+ *

+ *

+ * ⚠️注意:避免动态拼接SQL语句,应该使用预编译参数或者动态注入动态条件来进行条件处理 + * + * @param sql SQL查询语句 + * @param args 预编译参数 + * @return 查询构造器 + */ + NativeQuerySpec select(String sql, Object... args); + + /** * 创建一个查询构造器 * @@ -58,6 +126,41 @@ public interface QueryHelper { Consumer> mapperSpec); + interface NativeQuerySpec extends ExecuteSpec { + + /** + * 以DSL方式构造查询条件 + *

{@code
+         *  helper
+         *  .select("select * from table t")
+         *  .where(dsl->dsl.is("type","device"))
+         * }
+ * + * @param dsl DSL + * @return this + */ + default ExecuteSpec where(Consumer> dsl) { + Query query = QueryParamEntity.newQuery(); + dsl.accept(query); + return where(query.getParam()); + } + + /** + * 指定动态查询条件,通常用于前端动态传入查询条件 + *
{@code
+         *  helper
+         *  .select("select * from table t")
+         *  .where(param)
+         *  .fetch()
+         * }
+ * + * @param param DSL + * @return this + */ + ExecuteSpec where(QueryParamEntity param); + + } + interface SelectSpec { /** diff --git a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java index 0ab0afa4c..cec6e0ce8 100644 --- a/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java +++ b/hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/service/ReactiveCrudService.java @@ -27,6 +27,8 @@ import java.util.function.Function; * @see GenericReactiveCrudService * @see GenericReactiveTreeSupportCrudService * @see EnableCacheReactiveCrudService + * @see org.hswebframework.web.crud.query.QueryHelper + * @since 4.0 */ public interface ReactiveCrudService { diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java index c10bbe1ef..2958b782c 100644 --- a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/DefaultQueryHelperTest.java @@ -29,6 +29,39 @@ class DefaultQueryHelperTest { private DatabaseOperator database; + @Test + public void testNative() { + database.dml() + .insert("s_test_event") + .value("id", "helper_testNative") + .value("name", "Ename2") + .execute() + .sync(); + + database.dml() + .insert("s_test") + .value("id", "helper_testNative") + .value("name", "main2") + .value("age", 20) + .execute() + .sync(); + + DefaultQueryHelper helper = new DefaultQueryHelper(database); + + helper.select("select e.*,t.id as \"id\" from s_test t " + + "left join s_test_event e on e.id = t.id" + + " where t.age = ? order by t.age desc", 20) + .where(dsl -> dsl + .is("e.id", "helper_testNative") + .is("t.age", 20)) + .fetch() + .doOnNext(v -> System.out.println(JSON.toJSONString(v, SerializerFeature.PrettyFormat))) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + } + @Test public void test() { diff --git a/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java new file mode 100644 index 000000000..51beaadbb --- /dev/null +++ b/hsweb-commons/hsweb-commons-crud/src/test/java/org/hswebframework/web/crud/query/QueryAnalyzerImplTest.java @@ -0,0 +1,73 @@ +package org.hswebframework.web.crud.query; + +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@RunWith(SpringJUnit4ClassRunner.class) +class QueryAnalyzerImplTest { + @Autowired + private DatabaseOperator database; + + + @Test + void testInject() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "select count(distinct time) t2, \"name\" n from \"s_test\" t"); + SqlRequest request = analyzer.inject( + QueryParamEntity + .newQuery() + .and("name", "123") + .getParam()); + + System.out.println(request); + } + + @Test + void test() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "select name n from s_test t"); + + assertNotNull(analyzer.select().table.alias, "t"); + assertNotNull(analyzer.select().table.metadata.getName(), "s_test"); + + assertNotNull(analyzer.select().columns.get("n")); + + + } + + @Test + void testSub() { + QueryAnalyzerImpl analyzer = new QueryAnalyzerImpl(database, + "select * from ( select distinct(name) as n from s_test ) t"); + + assertEquals(analyzer.select().table.alias, "t"); + + assertNotNull(analyzer.select().getColumns().get("n")); + + SqlRequest request = analyzer + .inject(QueryParamEntity + .newQuery() + .where("n", "123") + .getParam()); + + System.out.println(request); + + database.sql() + .reactive() + .select(request, ResultWrappers.map()) + .as(StepVerifier::create) + .expectComplete() + .verify(); + } +} \ No newline at end of file