From 3ca4efc818cd3a3331bf606f7bd8f1a29447e76a Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 6 Mar 2018 12:58:43 +1300 Subject: [PATCH] #1331 - ENH: Support Aggregation formula with findSingleAttribute() ... max, min, avg, count and count(distinct ..) --- .../server/deploy/BeanDescriptor.java | 42 ++++ .../server/deploy/BeanDescriptorManager.java | 12 +- .../server/deploy/BeanDescriptorMap.java | 6 + .../server/deploy/BeanProperty.java | 8 +- .../server/deploy/DbSqlContext.java | 5 + .../server/deploy/DeployParser.java | 15 +- .../deploy/DeployPropertyParserMap.java | 2 +- .../server/deploy/DetermineAggPath.java | 4 + .../DynamicPropertyAggregationFormula.java | 52 +++++ .../server/deploy/DynamicPropertyBase.java | 70 ++++++ .../server/deploy/FormulaPropertyPath.java | 89 ++++++++ .../deploy/meta/DeployBeanDescriptor.java | 55 ++++- .../deploy/meta/DeployBeanProperty.java | 24 +- .../server/query/DefaultDbSqlContext.java | 12 +- .../server/query/SqlBeanLoad.java | 17 ++ .../server/query/SqlTreeBuilder.java | 4 +- .../server/query/SqlTreeNodeBean.java | 23 +- .../server/query/SqlTreeProperties.java | 27 ++- .../server/query/SqlTreeProperty.java | 76 +++++++ .../deploy/FormulaPropertyPathTest.java | 35 +++ .../org/tests/model/tevent/TEventOne.java | 26 ++- .../aggregation/TestAggregationCount.java | 215 +++++++++++++++++- .../query/other/TestQuerySingleAttribute.java | 15 ++ 23 files changed, 783 insertions(+), 51 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java create mode 100644 src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyBase.java create mode 100644 src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java create mode 100644 src/main/java/io/ebeaninternal/server/query/SqlTreeProperty.java create mode 100644 src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 36330994a..2552d2600 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -62,11 +62,13 @@ import io.ebeaninternal.server.el.ElPropertyValue; import io.ebeaninternal.server.persist.DmlUtil; import io.ebeaninternal.server.query.CQueryPlan; import io.ebeaninternal.server.query.CQueryPlanStatsCollector; +import io.ebeaninternal.server.query.SqlTreeProperty; import io.ebeaninternal.server.querydefn.OrmQueryDetail; import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.server.text.json.ReadJson; import io.ebeaninternal.server.text.json.SpiJsonWriter; import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; import io.ebeaninternal.util.SortByClause; import io.ebeaninternal.util.SortByClauseParser; import io.ebeanservice.docstore.api.DocStoreBeanAdapter; @@ -112,6 +114,8 @@ public class BeanDescriptor implements BeanType { private final ConcurrentHashMap> comparatorCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap dynamicProperty = new ConcurrentHashMap<>(); + private final Map namedRawSql; private final Map namedQuery; @@ -1055,6 +1059,13 @@ public class BeanDescriptor implements BeanType { return owner.getEncryptKey(tableName, columnName); } + /** + * Return the Scalar type for the given JDBC type. + */ + public ScalarType getScalarType(int jdbcType) { + return owner.getScalarType(jdbcType); + } + /** * Return true if this bean type has a default select clause that is not * simply select all properties. @@ -2437,6 +2448,37 @@ public class BeanDescriptor implements BeanType { return null; } + /** + * Return a 'dynamic property' used to read a formula. + */ + private SqlTreeProperty findSqlTreeFormula(String formulaExpression) { + + return dynamicProperty.computeIfAbsent(formulaExpression, (formula) -> { + FormulaPropertyPath propertyFormula = new FormulaPropertyPath(formula); + if (!propertyFormula.isFormula()) { + throw new IllegalStateException("unable to parse formula [" + formula + "} on bean type " + fullName); + } + String baseName = propertyFormula.basePropertyName(); + BeanProperty base = _findBeanProperty(baseName); + if (base == null) { + throw new IllegalStateException("unable to find property [" + baseName + "] from formula [" + formula + "} on bean type " + fullName); + } + return propertyFormula.formulaProperty(base); + }); + } + + /** + * Return a property that is part of the SQL tree. + * + * The property can be a dynamic formula or a well known bean property. + */ + public SqlTreeProperty findSqlTreeProperty(String propName) { + if (propName.indexOf('(') > -1) { + return findSqlTreeFormula(propName); + } + return _findBeanProperty(propName); + } + /** * Find a BeanProperty including searching the inheritance hierarchy. *

diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 618183220..9d8c6fbe6 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -52,7 +52,9 @@ import io.ebeaninternal.server.properties.BeanPropertiesReader; import io.ebeaninternal.server.properties.BeanPropertyAccess; import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess; import io.ebeaninternal.server.query.CQueryPlan; +import io.ebeaninternal.server.type.ScalarType; import io.ebeaninternal.server.type.ScalarTypeInteger; +import io.ebeaninternal.server.type.TypeManager; import io.ebeaninternal.xmlmapping.XmlMappingReader; import io.ebeaninternal.xmlmapping.model.XmAliasMapping; import io.ebeaninternal.xmlmapping.model.XmColumnMapping; @@ -139,6 +141,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { private final MultiValueBind multiValueBind; + private final TypeManager typeManager; + private int entityBeanCount; private final boolean updateChangesOnly; @@ -222,6 +226,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.dbIdentity = config.getDatabasePlatform().getDbIdentity(); this.deplyInherit = config.getDeployInherit(); this.deployUtil = config.getDeployUtil(); + this.typeManager = deployUtil.getTypeManager(); this.beanManagerFactory = new BeanManagerFactory(config.getDatabasePlatform()); @@ -260,6 +265,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { } } + @Override + public ScalarType getScalarType(int jdbcType) { + return typeManager.getScalarType(jdbcType); + } + /** * Return the AsOfViewSuffix based on the DbHistorySupport. */ @@ -1463,7 +1473,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { prop.setGetter(beanPropertyAccess.getGetter(propertyIndex)); prop.setSetter(beanPropertyAccess.getSetter(propertyIndex)); if (prop.isAggregation()) { - prop.setAggregationPrefix(DetermineAggPath.manyPath(prop.getAggregation(), desc)); + prop.setAggregationPrefix(DetermineAggPath.manyPath(prop.getRawAggregation(), desc)); } } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java index 2cb357a2e..9d8387565 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java @@ -6,6 +6,7 @@ import io.ebean.config.ServerConfig; import io.ebeaninternal.server.cache.SpiCacheManager; import io.ebeaninternal.server.deploy.id.IdBinder; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; +import io.ebeaninternal.server.type.ScalarType; import io.ebeanservice.docstore.api.DocStoreBeanAdapter; /** @@ -60,4 +61,9 @@ public interface BeanDescriptorMap { * Create a doc store specific adapter for this bean type. */ DocStoreBeanAdapter createDocStoreBeanAdapter(BeanDescriptor descriptor, DeployBeanDescriptor deploy); + + /** + * Return the scalarType for the given JDBC type. + */ + ScalarType getScalarType(int jdbcType); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 05e69add2..85f66e177 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -24,6 +24,7 @@ import io.ebeaninternal.server.properties.BeanPropertyGetter; import io.ebeaninternal.server.properties.BeanPropertySetter; import io.ebeaninternal.server.query.SqlBeanLoad; import io.ebeaninternal.server.query.SqlJoinType; +import io.ebeaninternal.server.query.SqlTreeProperty; import io.ebeaninternal.server.text.json.ReadJson; import io.ebeaninternal.server.text.json.SpiJsonWriter; import io.ebeaninternal.server.type.DataBind; @@ -55,7 +56,7 @@ import java.util.Set; * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class BeanProperty implements ElPropertyValue, Property { +public class BeanProperty implements ElPropertyValue, Property, SqlTreeProperty { private static final Logger logger = LoggerFactory.getLogger(BeanProperty.class); @@ -321,7 +322,7 @@ public class BeanProperty implements ElPropertyValue, Property { this.dbColumn = tableAliasIntern(descriptor, deploy.getDbColumn(), false, null); this.dbComment = deploy.getDbComment(); - this.aggregation = deploy.getAggregation(); + this.aggregation = deploy.parseAggregation(); this.sqlFormulaJoin = InternString.intern(deploy.getSqlFormulaJoin()); this.sqlFormulaSelect = InternString.intern(deploy.getSqlFormulaSelect()); this.formula = sqlFormulaSelect != null; @@ -575,8 +576,7 @@ public class BeanProperty implements ElPropertyValue, Property { public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (aggregation != null) { - ctx.appendRawColumn(aggregation); - + ctx.appendFormulaSelect(aggregation); } else if (formula) { ctx.appendFormulaSelect(sqlFormulaSelect); diff --git a/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java b/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java index ff5402dbc..5b7e335f0 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java +++ b/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java @@ -64,6 +64,11 @@ public interface DbSqlContext { */ void appendColumn(String column); + /** + * Parse and add formula with standard table alias replacement. + */ + void appendParseSelect(String parseSelect); + /** * Append a Sql Formula select. This converts the "${ta}" keyword to the * current table alias. diff --git a/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java b/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java index ff693b1ec..9d01766fa 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java +++ b/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java @@ -27,6 +27,8 @@ public abstract class DeployParser { */ protected static final char PERIOD = '.'; + protected static final char OPEN_BRACKET = '('; + protected boolean encrypted; protected String source; @@ -43,6 +45,8 @@ public abstract class DeployParser { protected char wordTerminator; + private StringBuilder wordBuffer; + protected abstract String convertWord(); public abstract String getDeployWord(String expression); @@ -77,7 +81,9 @@ public abstract class DeployParser { priorWord = deployWord; } if (pos < sourceLength) { - sb.append(wordTerminator); + if (wordTerminator != OPEN_BRACKET) { + sb.append(wordTerminator); + } if (wordTerminator == SINGLE_QUOTE) { readLiteral(); } @@ -97,7 +103,7 @@ public abstract class DeployParser { return false; } - StringBuilder wordBuffer = new StringBuilder(); + wordBuffer = new StringBuilder(); wordBuffer.append(source.charAt(pos)); while (++pos < sourceLength) { char ch = source.charAt(pos); @@ -172,6 +178,11 @@ public abstract class DeployParser { * return true if the char is a letter, digit or underscore. */ private boolean isWordPart(char ch) { + if (ch == OPEN_BRACKET) { + // include in the 'word' such that "count(" formula doesn't clash with property "count" + wordBuffer.append(ch); + return false; + } return Character.isLetterOrDigit(ch) || ch == UNDERSCORE || ch == PERIOD; } diff --git a/src/main/java/io/ebeaninternal/server/deploy/DeployPropertyParserMap.java b/src/main/java/io/ebeaninternal/server/deploy/DeployPropertyParserMap.java index 415ffd9d0..fc8aaadab 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/DeployPropertyParserMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/DeployPropertyParserMap.java @@ -7,7 +7,7 @@ import java.util.Set; /** * Converts logical property names to database columns using a Map. */ -public final class DeployPropertyParserMap extends DeployParser { +public class DeployPropertyParserMap extends DeployParser { private final Map map; diff --git a/src/main/java/io/ebeaninternal/server/deploy/DetermineAggPath.java b/src/main/java/io/ebeaninternal/server/deploy/DetermineAggPath.java index 83608456b..fff0a1438 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/DetermineAggPath.java +++ b/src/main/java/io/ebeaninternal/server/deploy/DetermineAggPath.java @@ -12,6 +12,10 @@ class DetermineAggPath { */ static String manyPath(String aggregation, DeployBeanDescriptor desc) { DetermineAggPath.Path path = paths(aggregation); + if (path.length() == 1) { + // a top level aggregation (so here we need to exclude Id property) + return null; + } return path.getManyPath(0, desc); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java b/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java new file mode 100644 index 000000000..e7f55721e --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java @@ -0,0 +1,52 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.server.query.SqlBeanLoad; +import io.ebeaninternal.server.type.ScalarType; + +import javax.persistence.PersistenceException; + +/** + * Dynamic property based on aggregation (max, min, avg, count). + */ +class DynamicPropertyAggregationFormula extends DynamicPropertyBase { + + private final String parsedAggregation; + + private final BeanProperty asTarget; + + DynamicPropertyAggregationFormula(String name, ScalarType scalarType, String parsedAggregation, BeanProperty asTarget) { + super(name, name, null, scalarType); + this.parsedAggregation = parsedAggregation; + this.asTarget = asTarget; + } + + @Override + public String toString() { + return "DynamicPropertyFormula[" + parsedAggregation + "]"; + } + + @Override + public boolean isAggregation() { + return true; + } + + @Override + public void load(SqlBeanLoad sqlBeanLoad) { + + try { + Object value = scalarType.read(sqlBeanLoad.ctx().getDataReader()); + if (asTarget != null) { + sqlBeanLoad.load(asTarget, value); + } + + } catch (Exception e) { + throw new PersistenceException("Error loading on " + fullName, e); + } + } + + @Override + public void appendSelect(DbSqlContext ctx, boolean subQuery) { + ctx.appendParseSelect(parsedAggregation); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyBase.java b/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyBase.java new file mode 100644 index 000000000..e0fae9d19 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyBase.java @@ -0,0 +1,70 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.server.query.SqlJoinType; +import io.ebeaninternal.server.query.SqlTreeProperty; +import io.ebeaninternal.server.type.ScalarType; + +import java.util.List; + +/** + * Abstract base for dynamic properties. + */ +abstract class DynamicPropertyBase implements SqlTreeProperty { + + final String name; + final String fullName; + final String elPrefix; + final ScalarType scalarType; + + public DynamicPropertyBase(String name, String fullName, String elPrefix, ScalarType scalarType) { + this.name = name; + this.fullName = fullName; + this.elPrefix = elPrefix; + this.scalarType = scalarType; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFullBeanName() { + return fullName; + } + + @Override + public boolean isId() { + return false; + } + + @Override + public boolean isEmbedded() { + return false; + } + + @Override + public String getElPrefix() { + return elPrefix; + } + + @Override + public ScalarType getScalarType() { + return scalarType; + } + + @Override + public void buildRawSqlSelectChain(String prefix, List selectChain) { + // do nothing, only for RawSql + } + + @Override + public void loadIgnore(DbReadContext ctx) { + scalarType.loadIgnore(ctx.getDataReader()); + } + + @Override + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { + // do not add to from usually + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java b/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java new file mode 100644 index 000000000..bb350f536 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/FormulaPropertyPath.java @@ -0,0 +1,89 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.server.query.SqlTreeProperty; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.Types; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class FormulaPropertyPath { + + private static final Pattern pattern = Pattern.compile("(max|min|avg|count)\\((.*)\\)"); + + private static final String DISTINCT_ = "distinct "; + + private final String aggType; + + private final String baseName; + + private boolean countDistinct; + + FormulaPropertyPath(String propName) { + + Matcher matcher = pattern.matcher(propName); + if (matcher.find()) { + aggType = matcher.group(1); + baseName = trimDistinct(matcher.group(2)); + + } else { + aggType = null; + baseName = null; + } + } + + private String trimDistinct(String propertyName) { + if (propertyName.startsWith(DISTINCT_)){ + countDistinct = true; + return propertyName.substring(DISTINCT_.length()); + } else { + return propertyName; + } + } + + boolean isFormula() { + return aggType != null; + } + + String aggType() { + return aggType; + } + + String basePropertyName() { + return baseName; + } + + /** + * Create a bean property dynamically for the formula in the select clause. + */ + SqlTreeProperty formulaProperty(BeanProperty base) { + + String parsedAggregation = buildFormula(base); + String name = logicalName(); + + ScalarType scalarType = base.getScalarType(); + if (isCount()) { + // count maps to Long / BIGINT + scalarType = base.getBeanDescriptor().getScalarType(Types.BIGINT); + } + + return new DynamicPropertyAggregationFormula(name, scalarType, parsedAggregation, null); + } + + private String buildFormula(BeanProperty base) { + if (countDistinct) { + return "count(distinct ${}"+base.getDbColumn()+")"; + } else { + return aggType+"(${}"+base.getDbColumn()+")"; + } + } + + private boolean isCount() { + return aggType.equals("count"); + } + + private String logicalName() { + return aggType+Character.toUpperCase(baseName.charAt(0))+baseName.substring(1); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index cfd837967..1df0d8ffd 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -25,6 +25,7 @@ import io.ebeaninternal.server.deploy.ChainedBeanPersistListener; import io.ebeaninternal.server.deploy.ChainedBeanPostConstructListener; import io.ebeaninternal.server.deploy.ChainedBeanPostLoad; import io.ebeaninternal.server.deploy.ChainedBeanQueryAdapter; +import io.ebeaninternal.server.deploy.DeployPropertyParserMap; import io.ebeaninternal.server.deploy.IndexDefinition; import io.ebeaninternal.server.deploy.InheritInfo; import io.ebeaninternal.server.deploy.TableJoin; @@ -849,18 +850,18 @@ public class DeployBeanDescriptor { this.idGeneratorName = PlatformIdGenerator.AUTO_UUID; switch (serverConfig.getUuidVersion()) { - case VERSION1: - this.idGenerator = UuidV1IdGenerator.getInstance(serverConfig.getUuidStateFile()); - break; + case VERSION1: + this.idGenerator = UuidV1IdGenerator.getInstance(serverConfig.getUuidStateFile()); + break; - case VERSION1RND: - this.idGenerator = UuidV1RndIdGenerator.INSTANCE; - break; + case VERSION1RND: + this.idGenerator = UuidV1RndIdGenerator.INSTANCE; + break; - case VERSION4: - default: - this.idGenerator = UuidV4IdGenerator.INSTANCE; - break; + case VERSION4: + default: + this.idGenerator = UuidV4IdGenerator.INSTANCE; + break; } } @@ -1168,4 +1169,38 @@ public class DeployBeanDescriptor { } namedRawSql.put(name, rawSql); } + + /** + * Parse the aggregation formula into expressions with table alias placeholders. + */ + public String parse(String aggregation) { + return new Parser(this).parse(aggregation); + } + + /** + * Parser for top level properties into EL expressions (table alias placeholders). + */ + private static class Parser extends DeployPropertyParserMap { + + private final DeployBeanDescriptor descriptor; + + Parser(DeployBeanDescriptor descriptor) { + super(null); + this.descriptor = descriptor; + } + + public String getDeployWord(String expression) { + return descriptor.getDeployWord(expression); + } + } + + private String getDeployWord(String expression) { + if (expression.charAt(expression.length() - 1) == '(') { + return null; + } + // use 'current' table alias - refer BeanProperty appendSelect() for aggregation + DeployBeanProperty property = propMap.get(expression); + return (property == null) ? null : "${ta}." + property.getDbColumn(); + } + } diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 5e2cfa517..a363fdbdd 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -175,6 +175,7 @@ public class DeployBeanProperty { private String aggregationPrefix; private String aggregation; + private String aggregationParsed; private String sqlFormulaSelect; private String sqlFormulaJoin; @@ -615,10 +616,23 @@ public class DeployBeanProperty { return aggregation != null; } - public String getAggregation() { + /** + * Get the raw/logical aggregation formula. + */ + public String getRawAggregation() { return aggregation; } + /** + * Get the parsed aggregation formula with table alias placeholders. + */ + public String parseAggregation() { + if (aggregation != null) { + aggregationParsed = desc.parse(aggregation); + } + return aggregationParsed; + } + public void setAggregation(String aggregation) { this.aggregation = aggregation; this.dbRead = true; @@ -629,9 +643,9 @@ public class DeployBeanProperty { /** * Set the path to the aggregation. */ - public void setAggregationPrefix(String aggregationPrefix) { - this.aggregationPrefix = aggregationPrefix; - this.aggregation = aggregation.replace(aggregationPrefix, "u1"); + public void setAggregationPrefix(String prefix) { + this.aggregationPrefix = prefix; + this.aggregation = (prefix == null) ? aggregation : aggregation.replace(aggregationPrefix, "u1"); } public String getElPrefix() { @@ -644,7 +658,7 @@ public class DeployBeanProperty { public String getElPlaceHolder() { if (aggregation != null) { - return aggregation; + return aggregationParsed; } else if (sqlFormulaSelect != null) { return sqlFormulaSelect; } else { diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java b/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java index a2138104b..dfb611e14 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -273,13 +273,19 @@ class DefaultDbSqlContext implements DbSqlContext { sb.append(" "); } + @Override + public void appendParseSelect(String parseSelect) { + String converted = alias.parse(parseSelect); + sb.append(COMMA); + sb.append(converted); + appendColumnAlias(); + } + @Override public void appendFormulaSelect(String sqlFormulaSelect) { String tableAlias = tableAliasStack.peek(); - String converted = StringHelper.replaceString(sqlFormulaSelect, tableAliasPlaceHolder, - tableAlias); - + String converted = StringHelper.replaceString(sqlFormulaSelect, tableAliasPlaceHolder, tableAlias); sb.append(COMMA); sb.append(converted); diff --git a/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java b/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java index fc851f006..b5855576d 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java @@ -44,6 +44,13 @@ public class SqlBeanLoad { return lazyLoading; } + /** + * Return the DB read context. + */ + public DbReadContext ctx() { + return ctx; + } + /** * Increment the resultSet index 1. */ @@ -86,4 +93,14 @@ public class SqlBeanLoad { } } + /** + * Load the given value into the property. + */ + public void load(BeanProperty target, Object dbVal) { + if (!refreshLoading) { + target.setValue(bean, dbVal); + } else { + target.setValueIntercept(bean, dbVal); + } + } } diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 5a26fcc5d..1c572cf91 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -418,7 +418,7 @@ public final class SqlTreeBuilder { // make sure we only included the base/embedded bean once if (!selectProps.containsProperty(baseName)) { - BeanProperty p = desc.findBeanProperty(baseName); + SqlTreeProperty p = desc.findSqlTreeProperty(baseName); if (p == null) { logger.error("property [" + propName + "] not found on " + desc + " for query - excluding it."); @@ -436,7 +436,7 @@ public final class SqlTreeBuilder { } else { // find the property including searching the // sub class hierarchy if required - BeanProperty p = desc.findBeanProperty(propName); + SqlTreeProperty p = desc.findSqlTreeProperty(propName); if (p == null) { logger.error("property [" + propName + "] not found on " + desc + " for query - excluding it."); p = desc.findBeanProperty("id"); diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java index 3d16f8ff2..806e64adb 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -48,7 +48,7 @@ class SqlTreeNodeBean implements SqlTreeNode { */ private final boolean partialObject; - protected final BeanProperty[] properties; + protected final SqlTreeProperty[] properties; /** * Extra where clause added by Where annotation on associated many. @@ -87,6 +87,7 @@ class SqlTreeNodeBean implements SqlTreeNode { private boolean intersectionAsOfTableAlias; private final boolean aggregation; + private final boolean aggregationRoot; /** * Construct for leaf node. @@ -124,13 +125,15 @@ class SqlTreeNodeBean implements SqlTreeNode { this.nodeBeanProp = beanProp; this.extraWhere = (beanProp == null) ? null : beanProp.getExtraWhere(); + this.aggregation = props.isAggregation(); + this.aggregationRoot = props.isAggregationRoot(); + // the bean has an Id property and we want to use it - this.readId = withId && (desc.getIdProperty() != null); + this.readId = !aggregationRoot && withId && (desc.getIdProperty() != null); this.disableLazyLoad = disableLazyLoad || !readId || desc.isRawSqlBased() || temporalVersions; this.partialObject = props.isPartialObject(); this.properties = props.getProps(); - this.aggregation = props.isAggregation(); this.children = myChildren == null ? NO_CHILDREN : myChildren.toArray(new SqlTreeNode[myChildren.size()]); pathMap = createPathMap(prefix, desc); @@ -186,7 +189,7 @@ class SqlTreeNodeBean implements SqlTreeNode { } idBinder.buildRawSqlSelectChain(prefix, selectChain); } - for (BeanProperty property : properties) { + for (SqlTreeProperty property : properties) { property.buildRawSqlSelectChain(prefix, selectChain); } // recursively continue reading... @@ -289,14 +292,14 @@ class SqlTreeNodeBean implements SqlTreeNode { if (inheritInfo == null) { // normal behavior with no inheritance - for (BeanProperty property : properties) { + for (SqlTreeProperty property : properties) { property.load(sqlBeanLoad); } } else { // take account of inheritance and due to subclassing approach // need to get a 'local' version of the property - for (BeanProperty property : properties) { + for (SqlTreeProperty property : properties) { // get a local version of the BeanProperty BeanProperty p = localDesc.getBeanProperty(property.getName()); if (p != null) { @@ -416,7 +419,7 @@ class SqlTreeNodeBean implements SqlTreeNode { if (readId) { appendSelectId(ctx, idBinder.getBeanProperty()); } - for (BeanProperty property : properties) { + for (SqlTreeProperty property : properties) { if (!property.isAggregation()) { property.appendSelect(ctx, subQuery); } @@ -483,9 +486,9 @@ class SqlTreeNodeBean implements SqlTreeNode { /** * Append the properties to the buffer. */ - private void appendSelect(DbSqlContext ctx, boolean subQuery, BeanProperty[] props) { + private void appendSelect(DbSqlContext ctx, boolean subQuery, SqlTreeProperty[] props) { - for (BeanProperty prop : props) { + for (SqlTreeProperty prop : props) { prop.appendSelect(ctx, subQuery); } } @@ -543,7 +546,7 @@ class SqlTreeNodeBean implements SqlTreeNode { // join and return SqlJoinType to use for child joins joinType = appendFromBaseTable(ctx, joinType); - for (BeanProperty property : properties) { + for (SqlTreeProperty property : properties) { // usually nothing... except for 1-1 Exported property.appendFrom(ctx, joinType); } diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeProperties.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeProperties.java index 9c762b7b8..9c717173e 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeProperties.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeProperties.java @@ -1,7 +1,6 @@ package io.ebeaninternal.server.query; import io.ebeaninternal.api.ManyWhereJoins; -import io.ebeaninternal.server.deploy.BeanProperty; import java.util.ArrayList; import java.util.Arrays; @@ -21,7 +20,7 @@ public class SqlTreeProperties { /** * The bean properties in order. */ - private final List propsList = new ArrayList<>(); + private final List propsList = new ArrayList<>(); /** * Maintain a list of property names to detect embedded bean additions. @@ -32,6 +31,8 @@ public class SqlTreeProperties { private boolean aggregation; + private String aggregationPath; + SqlTreeProperties() { } @@ -39,17 +40,17 @@ public class SqlTreeProperties { return propNames.contains(propName); } - public void add(BeanProperty[] props) { + public void add(SqlTreeProperty[] props) { propsList.addAll(Arrays.asList(props)); } - public void add(BeanProperty prop) { + public void add(SqlTreeProperty prop) { propsList.add(prop); propNames.add(prop.getName()); } - public BeanProperty[] getProps() { - return propsList.toArray(new BeanProperty[propsList.size()]); + public SqlTreeProperty[] getProps() { + return propsList.toArray(new SqlTreeProperty[propsList.size()]); } boolean isPartialObject() { @@ -77,7 +78,6 @@ public class SqlTreeProperties { boolean requireSqlDistinct(ManyWhereJoins manyWhereJoins) { String joinProperty = aggregationJoin(); if (joinProperty != null) { - aggregation = true; manyWhereJoins.addAggregationJoin(joinProperty); return false; } else { @@ -97,12 +97,21 @@ public class SqlTreeProperties { */ private String aggregationJoin() { if (!allProperties) { - for (BeanProperty beanProperty : propsList) { + for (SqlTreeProperty beanProperty : propsList) { if (beanProperty.isAggregation()) { - return beanProperty.getElPrefix(); + aggregation = true; + aggregationPath = beanProperty.getElPrefix(); + return aggregationPath; } } } return null; } + + /** + * Return true if a top level aggregation which means the Id property must be excluded. + */ + public boolean isAggregationRoot() { + return aggregation && (aggregationPath == null); + } } diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeProperty.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeProperty.java new file mode 100644 index 000000000..2543cc825 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeProperty.java @@ -0,0 +1,76 @@ +package io.ebeaninternal.server.query; + +import io.ebeaninternal.server.deploy.DbReadContext; +import io.ebeaninternal.server.deploy.DbSqlContext; +import io.ebeaninternal.server.type.ScalarType; + +import java.util.List; + +/** + * A property in the SQL Tree. + * + * A BeanProperty or a dynamically created property based on formula. + */ +public interface SqlTreeProperty { + + /** + * Return the property name. + */ + String getName(); + + /** + * Return the full property name (for error messages). + */ + String getFullBeanName(); + + /** + * Return true if the property is the Id. + */ + boolean isId(); + + /** + * Return true if the property is an embedded type. + */ + boolean isEmbedded(); + + /** + * Return true if the property is an aggregation. + */ + boolean isAggregation(); + + /** + * Return the Expression language prefix (join path). + */ + String getElPrefix(); + + /** + * Return the underlying scalar type for the property (for findSingleAttribute). + */ + ScalarType getScalarType(); + + /** + * For RawSql build the select chain. + */ + void buildRawSqlSelectChain(String prefix, List selectChain); + + /** + * Load into the bean (from the DataReader/ResultSet). + */ + void load(SqlBeanLoad sqlBeanLoad); + + /** + * Ignore the property (moving the column index position without reading). + */ + void loadIgnore(DbReadContext ctx); + + /** + * Append to the select clause. + */ + void appendSelect(DbSqlContext ctx, boolean subQuery); + + /** + * Append to the from clause. + */ + void appendFrom(DbSqlContext ctx, SqlJoinType joinType); + +} diff --git a/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java b/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java new file mode 100644 index 000000000..c3d840603 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/deploy/FormulaPropertyPathTest.java @@ -0,0 +1,35 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.BaseTestCase; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FormulaPropertyPathTest extends BaseTestCase { + + //private BeanDescriptor customerDesc = getBeanDescriptor(Customer.class); + + @Test + public void isFormula() { + + assertFormula("max(foo)", "max", "foo"); + assertFormula("min(bar)", "min", "bar"); + assertFormula("avg(baz)", "avg", "baz"); + } + + @Test + public void isFormula_count() { + assertFormula("count(moo)", "count", "moo"); + assertFormula("count(distinct joo)", "count", "joo"); + } + + private void assertFormula(String input, String aggType, String baseProperty) { + + FormulaPropertyPath propertyPath = new FormulaPropertyPath(input); + + assertThat(propertyPath.isFormula()).isTrue(); + assertThat(propertyPath.basePropertyName()).isEqualTo(baseProperty); + assertThat(propertyPath.aggType()).isEqualTo(aggType); + + } +} diff --git a/src/test/java/org/tests/model/tevent/TEventOne.java b/src/test/java/org/tests/model/tevent/TEventOne.java index c5b1dbb01..88bc2e2fd 100644 --- a/src/test/java/org/tests/model/tevent/TEventOne.java +++ b/src/test/java/org/tests/model/tevent/TEventOne.java @@ -13,17 +13,27 @@ import java.util.List; @Entity public class TEventOne { + public enum Status { + AA, + BB + } + @Id Long id; String name; + Status status; + @Version Long version; @OneToOne TEvent event; + @Aggregation("max(version)") + Long maxVersion; + @Aggregation("count(logs.id)") Long count; @@ -36,8 +46,14 @@ public class TEventOne { @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) List logs; - public TEventOne(String name) { + public TEventOne(String name, Status status) { this.name = name; + this.status = status; + } + + @Override + public String toString() { + return "id:" + id + " name:" + name + " status:" + status + " mv:" + maxVersion + " ct:" + count; } public Long getId() { @@ -60,10 +76,18 @@ public class TEventOne { return totalAmount; } + public Long getMaxVersion() { + return maxVersion; + } + public String getName() { return name; } + public Status getStatus() { + return status; + } + public void setName(String name) { this.name = name; } diff --git a/src/test/java/org/tests/query/aggregation/TestAggregationCount.java b/src/test/java/org/tests/query/aggregation/TestAggregationCount.java index abd586eb9..34c231b26 100644 --- a/src/test/java/org/tests/query/aggregation/TestAggregationCount.java +++ b/src/test/java/org/tests/query/aggregation/TestAggregationCount.java @@ -3,11 +3,18 @@ package org.tests.query.aggregation; import io.ebean.BaseTestCase; import io.ebean.Ebean; import io.ebean.Query; +import org.assertj.core.api.Assertions; +import org.ebeantest.LoggedSqlCollector; import org.junit.BeforeClass; import org.junit.Test; +import org.tests.model.basic.Contact; +import org.tests.model.basic.Order; +import org.tests.model.basic.OrderDetail; +import org.tests.model.basic.ResetBasicData; import org.tests.model.tevent.TEventMany; import org.tests.model.tevent.TEventOne; +import java.sql.Timestamp; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -16,17 +23,23 @@ public class TestAggregationCount extends BaseTestCase { @BeforeClass public static void setup() { - TEventOne one = new TEventOne("first"); + TEventOne one = new TEventOne("first", TEventOne.Status.AA); one.getLogs().add(new TEventMany("all", 1, 10)); one.getLogs().add(new TEventMany("be", 2, 12.2)); one.getLogs().add(new TEventMany("add", 3, 13)); Ebean.save(one); - TEventOne two = new TEventOne("second"); + TEventOne two = new TEventOne("second", TEventOne.Status.AA); two.getLogs().add(new TEventMany("at", 10, 10)); two.getLogs().add(new TEventMany("add", 30, 13)); two.getLogs().add(new TEventMany("alf", 30, 13)); Ebean.save(two); + + TEventOne three = new TEventOne("thrird", TEventOne.Status.BB); + Ebean.save(three); + three.setName("third"); + Ebean.save(three); + } @Test @@ -36,7 +49,7 @@ public class TestAggregationCount extends BaseTestCase { List list = query.findList(); String sql = sqlOf(query, 5); - assertThat(sql).contains("select t0.id, t0.name, t0.version, t0.event_id from tevent_one t0"); + assertThat(sql).contains("select t0.id, t0.name, t0.status, t0.version, t0.event_id from tevent_one t0"); for (TEventOne eventOne : list) { // lazy loading on Aggregation properties @@ -172,4 +185,200 @@ public class TestAggregationCount extends BaseTestCase { assertThat(sql).contains("select t0.id, t0.name, count(u1.id), t1.id, t1.name from tevent_one t0 left join tevent t1 on t1.id = t0.event_id join tevent_many u1 on u1.event_id = t0.id "); assertThat(sql).contains("group by t0.id, t0.name, t1.id, t1.name"); } + + @Test + public void testTopLevelAggregation() { + + Query query0 = Ebean.find(TEventOne.class) + .select("status, maxVersion") + .where().isNotNull("name") + .having().ge("maxVersion", 1) + .query(); + + List list = query0.findList(); + + String sql = sqlOf(query0, 5); + assertThat(sql).contains("select t0.status, max(t0.version) from tevent_one t0"); + assertThat(sql).contains("where t0.name is not null"); + assertThat(sql).contains("group by t0.status"); + assertThat(sql).contains("having max(t0.version) >= ?"); + + + LoggedSqlCollector.start(); + + for (TEventOne eventOne : list) { + assertThat(eventOne.getStatus()).isNotNull(); + assertThat(eventOne.getMaxVersion()).isNotNull(); + + // bean has no Id, so it is not in Persistence Context, nor Load context + assertThat(eventOne.getId()).isNull(); + // ... and it will not invoke lazy loading + assertThat(eventOne.getName()).isNull(); + } + + List lazyLoadSql = LoggedSqlCollector.stop(); + assertThat(lazyLoadSql).isEmpty(); + } + + @Test + public void testDynamicSingleAttributeAggregation() { + + ResetBasicData.reset(); + + Query query0 = Ebean.find(Order.class) + .select("max(updtime)") + .where().eq("status", Order.Status.NEW) + .query(); + + Timestamp maxUpdateTime = query0.findSingleAttribute(); + System.out.println(""+maxUpdateTime); + assertThat(maxUpdateTime).isNotNull(); + + String sql = sqlOf(query0, 5); + assertThat(sql).contains("select max(t0.updtime) from o_order t0"); + + Timestamp maxNotNew = Ebean.find(Order.class) + .select("max(updtime)") + .where().ne("status", Order.Status.NEW) + .findSingleAttribute(); + + assertThat(maxNotNew).isNotNull(); + + } + + @Test + public void testDynamicSingleAttributeAggregation_maxInteger() { + + ResetBasicData.reset(); + + Query query = Ebean.find(OrderDetail.class) + .select("max(orderQty)"); + + Integer maxOrderQty = query.findSingleAttribute(); + System.out.println(""+maxOrderQty); + Assertions.assertThat(maxOrderQty).isGreaterThan(20); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select max(t0.order_qty) from o_order_detail t0"); + } + + @Test + public void testDynamicSingleAttributeAggregation_minInteger() { + + ResetBasicData.reset(); + + Query query = Ebean.find(OrderDetail.class) + .select("min(orderQty)"); + + Integer minOrderQty = query.findSingleAttribute(); + System.out.println(""+minOrderQty); + assertThat(minOrderQty).isLessThan(10); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select min(t0.order_qty) from o_order_detail t0"); + } + + @Test + public void testDynamicSingleAttributeAggregation_maxString() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("max(lastName)"); + + String maxName = query.findSingleAttribute(); + System.out.println(""+maxName); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select max(t0.last_name) from contact t0"); + } + + @Test + public void testDynamicSingleAttributeAggregation_minString() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("min(firstName)"); + + String minName = query.findSingleAttribute(); + System.out.println(""+minName); + assertThat(minName).isNotNull(); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select min(t0.first_name) from contact t0"); + } + + + @Test + public void testDynamicSingleAttributeAggregation_count() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("count(lastName)"); + + Long count = query.findSingleAttribute(); + System.out.println(""+count); + assertThat(count).isNotNull(); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select count(t0.last_name) from contact t0"); + } + + @Test + public void testDynamicSingleAttributeAggregation_countDistinct() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("count(distinct lastName)"); + + Long count = query.findSingleAttribute(); + System.out.println(""+count); + assertThat(count).isNotNull(); + + String sql = sqlOf(query, 5); + assertThat(sql).contains("select count(distinct t0.last_name) from contact t0"); + } + + @Test + public void example() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + String maxLastName = + Ebean.find(Contact.class) + .select("max(lastName)") + .where().isNull("phone") + .findSingleAttribute(); + + assertThat(maxLastName).isNotNull(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql.get(0)).contains("select max(t0.last_name) from contact t0"); + } + + @Test + public void example_countDistinct() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + Long count = + Ebean.find(Contact.class) + .select("count(distinct lastName)") + .where().isEmpty("notes") + .findSingleAttribute(); + + System.out.println(""+count); + assertThat(count).isNotNull(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql.get(0)).contains("select count(distinct t0.last_name) from contact t0 where not exists (select 1 from contact_note x where x.contact_id = t0.id)"); + } + } diff --git a/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index ade6d2a91..ff482a03e 100644 --- a/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -99,6 +99,21 @@ public class TestQuerySingleAttribute extends BaseTestCase { assertThat(name).isNotNull(); } + @Test + public void findSingleAttribute_with_aggregate() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("max(name)") + .where().gt("id", 1).query(); + + String name = query.findSingleAttribute(); + + assertThat(sqlOf(query)).contains("select max(t0.name) from o_customer t0"); + assertThat(name).isNotNull(); + } + @Test public void findSingleAttribute_viaExpression() {