From ffa26bed700e48945085081a00aec33545654ed9 Mon Sep 17 00:00:00 2001 From: grabdoc Date: Sun, 14 Jan 2024 13:00:54 -0600 Subject: [PATCH] schema is now optional, add support for union query --- .../homihq/db2rest/mybatis/MyBatisTable.java | 20 +++++ .../db2rest/rest/read/ReadController.java | 2 +- .../homihq/db2rest/rest/read/ReadService.java | 31 ++----- .../db2rest/rest/read/helper/JoinBuilder.java | 5 ++ .../db2rest/rest/read/helper/ReadContext.java | 84 ++++++++++++++++--- .../rest/read/helper/SelectBuilder.java | 62 +++++++++++--- .../rest/read/helper/WhereBuilder.java | 2 + .../homihq/db2rest/schema/SchemaManager.java | 36 ++++++-- 8 files changed, 187 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/homihq/db2rest/mybatis/MyBatisTable.java b/src/main/java/com/homihq/db2rest/mybatis/MyBatisTable.java index d6e7e6d8..6c117fd7 100644 --- a/src/main/java/com/homihq/db2rest/mybatis/MyBatisTable.java +++ b/src/main/java/com/homihq/db2rest/mybatis/MyBatisTable.java @@ -1,6 +1,8 @@ package com.homihq.db2rest.mybatis; import lombok.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlTable; import schemacrawler.schema.Column; @@ -22,6 +24,8 @@ public class MyBatisTable extends SqlTable{ Table table; + boolean root; + public MyBatisTable(String schemaName, String tableName, Table table) { super(tableName); this.tableName = tableName; @@ -41,4 +45,20 @@ public void addColumn(Column column) { public void addColumn(String columnName, String alias) { sqlColumnList.add(column(columnName).as(alias)); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + MyBatisTable table = (MyBatisTable) o; + + return new EqualsBuilder().append(tableName, table.tableName).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(tableName).toHashCode(); + } } diff --git a/src/main/java/com/homihq/db2rest/rest/read/ReadController.java b/src/main/java/com/homihq/db2rest/rest/read/ReadController.java index ea3ed8ae..900b877b 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/ReadController.java +++ b/src/main/java/com/homihq/db2rest/rest/read/ReadController.java @@ -19,7 +19,7 @@ public class ReadController { @GetMapping(value = "/{tableName}" , produces = "application/json") public Object findByJoinTable(@PathVariable String tableName, - @RequestHeader(name = "Accept-Profile") String schemaName, + @RequestHeader(name = "Accept-Profile", required = false) String schemaName, @RequestParam(name = "select", required = false, defaultValue = "") String select, @RequestParam(name = "filter", required = false, defaultValue = "") String filter, Sort sort, diff --git a/src/main/java/com/homihq/db2rest/rest/read/ReadService.java b/src/main/java/com/homihq/db2rest/rest/read/ReadService.java index fd58e051..528fc138 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/ReadService.java +++ b/src/main/java/com/homihq/db2rest/rest/read/ReadService.java @@ -1,17 +1,13 @@ package com.homihq.db2rest.rest.read; -import com.homihq.db2rest.config.Db2RestConfigProperties; import com.homihq.db2rest.rest.read.helper.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - -import org.mybatis.dynamic.sql.SqlTable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; - -import java.util.ArrayList; +import java.util.Map; @Service @@ -19,21 +15,20 @@ @RequiredArgsConstructor public class ReadService { - private final JdbcTemplate jdbcTemplate; private final SelectBuilder selectBuilder; private final JoinBuilder joinBuilder; private final WhereBuilder whereBuilder; private final LimitPaginationBuilder limitPaginationBuilder; private final SortBuilder sortBuilder; - private final Db2RestConfigProperties db2RestConfigProperties; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; public Object findAll(String schemaName, String tableName, String select, String filter, Pageable pageable, Sort sort) { - ReadContext ctx = ReadContext.builder().from(SqlTable.of(tableName)) + ReadContext ctx = ReadContext.builder() .pageable(pageable).sort(sort) - .schemaName(schemaName).tableName(tableName).select(select).filter(filter).build(); - + .schemaName(schemaName) + .tableName(tableName).select(select).filter(filter).build(); selectBuilder.build(ctx); joinBuilder.build(ctx); @@ -42,23 +37,13 @@ public Object findAll(String schemaName, String tableName, String select, String sortBuilder.build(ctx); String sql = ctx.prepareSQL(); - - log.info("SQL - {}", sql); - - /* - Query query = createQuery(schemaName, tableName,select,filter, joinTable, pageable); - - String sql = query.getSQL(); - List bindValues = query.getBindValues(); + Map bindValues = ctx.prepareParameters(); log.info("SQL - {}", sql); log.info("Bind variables - {}", bindValues); - return jdbcTemplate.queryForList(sql, bindValues.toArray()); - - */ + return namedParameterJdbcTemplate.queryForList(sql, bindValues); - return new ArrayList(); } diff --git a/src/main/java/com/homihq/db2rest/rest/read/helper/JoinBuilder.java b/src/main/java/com/homihq/db2rest/rest/read/helper/JoinBuilder.java index 8f180ef7..2af00fe2 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/helper/JoinBuilder.java +++ b/src/main/java/com/homihq/db2rest/rest/read/helper/JoinBuilder.java @@ -19,8 +19,13 @@ public class JoinBuilder { private final SchemaManager schemaManager; public void build(ReadContext context) { + + if(context.isUnion()) return; + List tableList = context.getTables(); + log.info("Table list - {}", tableList); + if(tableList.size() > 1) { //table join required for(int i = 0 ; i < tableList.size() ; i = i + 2) { MyBatisTable root = tableList.get(i); diff --git a/src/main/java/com/homihq/db2rest/rest/read/helper/ReadContext.java b/src/main/java/com/homihq/db2rest/rest/read/helper/ReadContext.java index d1e336ba..7c0b2fd0 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/helper/ReadContext.java +++ b/src/main/java/com/homihq/db2rest/rest/read/helper/ReadContext.java @@ -1,18 +1,24 @@ package com.homihq.db2rest.rest.read.helper; +import com.homihq.db2rest.exception.GenericDataAccessException; import com.homihq.db2rest.mybatis.MyBatisTable; import lombok.*; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.SqlColumn; import org.mybatis.dynamic.sql.SqlCriterion; -import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.render.RenderingStrategies; import org.mybatis.dynamic.sql.select.QueryExpressionDSL; import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import static org.mybatis.dynamic.sql.select.SelectDSL.select; @@ -30,22 +36,19 @@ public class ReadContext { Pageable pageable; Sort sort; - SqlTable from; + MyBatisTable from; List tables; - QueryExpressionDSL queryExpressionDSL; + boolean union; + QueryExpressionDSL queryExpressionDSL; + SelectStatementProvider selectStatementProvider; public void addWhereClause(SqlCriterion condition) { queryExpressionDSL.where(condition); } - private List getAllColumns() { - return tables.stream() - .flatMap(t -> t.getSqlColumnList().stream()) - .map(t -> (BasicColumn)t) - .toList(); - } + public SqlColumn getSortColumn(String columnName) { //for now just support root table @@ -57,16 +60,73 @@ public void createSelect() { List columns = getAllColumns(); - queryExpressionDSL = select(columns).from(from); + detectUnion(); + + if(union) { + createUnionQuery(); + } + else{ + from = getRootTable(); + queryExpressionDSL = select(columns).from(from); + } } - public String prepareSQL() { + private MyBatisTable getRootTable() { + return + tables.stream().filter(MyBatisTable::isRoot).findFirst() + .orElseThrow(() -> new GenericDataAccessException("Unable to detect root table.")); + } + + private void createUnionQuery() { + + for(MyBatisTable table : tables) { + + if(Objects.isNull(queryExpressionDSL)) { + + queryExpressionDSL = select(getAllColumns()).from(table, table.getAlias()); + } + else { + queryExpressionDSL.union(). + select(getAllColumns()).from(table, table.getAlias()); + } + } - return queryExpressionDSL.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER).getSelectStatement(); } + private void detectUnion() { + Set result = tables.stream() + .collect(Collectors.groupingBy(Function.identity() + , Collectors.counting())) + .entrySet().stream() + .filter(m -> m.getValue() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + union = (this.tables.size() == result.size()) && result.size() > 1; + if(result.size() > 1 && this.tables.size() > result.size()) + throw new GenericDataAccessException("Unable to create SQL. Seems union but too many tables."); + } + + private List getAllColumns() { + return tables.stream() + .flatMap(t -> t.getSqlColumnList().stream()) + .map(t -> (BasicColumn)t) + .toList(); + } + + public String prepareSQL() { + + selectStatementProvider = queryExpressionDSL.build().render(RenderingStrategies.SPRING_NAMED_PARAMETER); + + return this.selectStatementProvider.getSelectStatement(); + + } + + public Map prepareParameters() { + return this.selectStatementProvider.getParameters(); + } } diff --git a/src/main/java/com/homihq/db2rest/rest/read/helper/SelectBuilder.java b/src/main/java/com/homihq/db2rest/rest/read/helper/SelectBuilder.java index 0acbcf7f..090a6c97 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/helper/SelectBuilder.java +++ b/src/main/java/com/homihq/db2rest/rest/read/helper/SelectBuilder.java @@ -1,9 +1,11 @@ package com.homihq.db2rest.rest.read.helper; +import com.homihq.db2rest.config.Db2RestConfigProperties; import com.homihq.db2rest.mybatis.MyBatisTable; import com.homihq.db2rest.schema.SchemaManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import java.util.ArrayList; @@ -15,6 +17,7 @@ public class SelectBuilder{ private final SchemaManager schemaManager; + private final Db2RestConfigProperties db2RestConfigProperties; public void build(ReadContext context) { context.setTables(createTables(context)); @@ -24,30 +27,42 @@ public void build(ReadContext context) { private List createTables(ReadContext context) { List tables = new ArrayList<>(); - + log.info("context.select - {}", context.select); //split to get all fragments String [] tabCols = context.select.split(";"); int counter = 0; + log.info("tabCols - {}", tabCols.length); + //process the fragments for(String tabCol : tabCols) { - MyBatisTable table; + List myBatisTables; //check for presence of open '(' and close ')' brackets //now check for embedded table and columns. - if(tabCol.contains("(") && tabCol.contains(")")) { //join table + if(tabCol.contains("(") && tabCol.contains(")")) { //join tables String joinTableName = tabCol.substring(0, tabCol.indexOf("(")); //look for columns String colString = tabCol.substring(tabCol.indexOf("(") + 1 , tabCol.indexOf(")")); - table = createTable(context.schemaName, joinTableName, colString, counter); + myBatisTables = createTables(context.schemaName, joinTableName, colString, counter); } else{ //root table - table = createTable(context.schemaName, context.tableName, tabCol, counter); + + log.info("Creating root tables"); + myBatisTables = createTables(context.schemaName, context.tableName, tabCol, counter); + + log.info("myBatisTables - {}", myBatisTables); + + for(MyBatisTable table : myBatisTables) { + table.setRoot(true); + } + + //TODO - multiple root tables and no other table = union query, else unsupported exception } - tables.add(table); + tables.addAll(myBatisTables); counter++; } @@ -56,15 +71,40 @@ private List createTables(ReadContext context) { return tables; } - private MyBatisTable createTable(String schemaName, String tableName, String colStr, int counter) { - //MyBatisTable table = schemaManager.findTable(schemaName, tableName, counter); + private List createTables(String schemaName, String tableName, String colStr, int counter) { + + String sName = schemaName; + String tName = tableName; + + + if(!this.db2RestConfigProperties.getMultiTenancy().isEnabled()) { + String [] tableNameParts = tableName.split("|."); + + if(tableNameParts.length == 2) { //table name contains schema + sName = tableNameParts[0]; + tName = tableNameParts[1]; + } + } + + if(StringUtils.isNotBlank(sName)) { + MyBatisTable table = schemaManager.findTable(sName, tName, counter); + + addColumns(table, colStr); + + return List.of(table); + } + else { + List tables = schemaManager.findTables(tName); - MyBatisTable table = schemaManager.findTable(schemaName, tableName, counter); + for(MyBatisTable table : tables) { + addColumns(table, colStr); + } + + return tables; + } - addColumns(table, colStr); - return table; } private void addColumns(MyBatisTable table, String colStr) { diff --git a/src/main/java/com/homihq/db2rest/rest/read/helper/WhereBuilder.java b/src/main/java/com/homihq/db2rest/rest/read/helper/WhereBuilder.java index 742d758d..32746a5e 100644 --- a/src/main/java/com/homihq/db2rest/rest/read/helper/WhereBuilder.java +++ b/src/main/java/com/homihq/db2rest/rest/read/helper/WhereBuilder.java @@ -21,6 +21,8 @@ public class WhereBuilder{ private final SchemaManager schemaManager; public void build(ReadContext context) { + if(context.isUnion()) return; + if(StringUtils.isNotBlank(context.filter)) { log.info("-Creating where condition -"); diff --git a/src/main/java/com/homihq/db2rest/schema/SchemaManager.java b/src/main/java/com/homihq/db2rest/schema/SchemaManager.java index 4b5a300a..517410b1 100644 --- a/src/main/java/com/homihq/db2rest/schema/SchemaManager.java +++ b/src/main/java/com/homihq/db2rest/schema/SchemaManager.java @@ -14,6 +14,7 @@ import us.fatehi.utility.datasource.DatabaseConnectionSources; import javax.sql.DataSource; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,6 +29,7 @@ public final class SchemaManager { private final DataSource dataSource; private final Map tableMap = new ConcurrentHashMap<>(); + private final List tableList = new ArrayList<>(); @PostConstruct public void reload() { @@ -56,18 +58,12 @@ public void createSchemaCache() { for (final Table table : catalog.getTables(schema)) { - //TODO - move DB specific handling to Dialect class - String schemaName = table.getSchema().getCatalogName(); - - if(StringUtils.isBlank(schemaName)) { - //POSTGRESQL - - schemaName = table.getSchema().getName(); - } + String schemaName = getSchemaName(table); String fullName = schemaName + "." + table.getName(); log.info("Full name - {}", fullName); tableMap.put(fullName, table); + tableList.add(table); } } @@ -76,6 +72,18 @@ public void createSchemaCache() { } + private String getSchemaName(Table table) { + //TODO - move DB specific handling to Dialect class + String schemaName = table.getSchema().getCatalogName(); + + if(StringUtils.isBlank(schemaName)) { + //POSTGRESQL + + schemaName = table.getSchema().getName(); + } + return schemaName; + } + public Optional
getTable(String schemaName, String tableName) { log.info("Get table - {}.{}", schemaName, tableName); Table table = tableMap.get(schemaName + "." + tableName); @@ -102,6 +110,18 @@ public List getForeignKeysBetween(String schemaName, String rootTabl && StringUtils.equalsIgnoreCase(fk.getReferencedTable().getName(), childTable)).toList(); } + public List findTables(String tableName) { + return tableList.stream() + .filter(t -> StringUtils.equalsIgnoreCase(t.getName(), tableName)) + .toList() + .stream() + .map(t -> + new MyBatisTable( + getSchemaName(t), tableName, t)) + .toList(); + + } + public MyBatisTable findTable(String schemaName, String tableName, int counter) { Table table = getTable(schemaName, tableName).orElseThrow(() -> new InvalidTableException(tableName));