#99 - ENH (419) : Simplify aggregation queries

This commit is contained in:
Rob Bygrave
2016-11-02 22:10:22 +13:00
parent 18f943ab2b
commit 3526f71f0a
33 changed files with 559 additions and 217 deletions
Executable → Regular
View File
Executable → Regular
View File
@@ -1,6 +1,7 @@
package com.avaje.ebean;
import com.avaje.ebean.RawSql.Sql;
import com.avaje.ebeaninternal.server.querydefn.SimpleTextParser;
/**
* Parses sql-select queries to try and determine the location where WHERE and
@@ -8,13 +9,13 @@ import com.avaje.ebean.RawSql.Sql;
*/
class DRawSqlParser {
public static final String $_AND_HAVING = "${andHaving}";
private static final String $_AND_HAVING = "${andHaving}";
public static final String $_HAVING = "${having}";
private static final String $_HAVING = "${having}";
public static final String $_AND_WHERE = "${andWhere}";
private static final String $_AND_WHERE = "${andWhere}";
public static final String $_WHERE = "${where}";
private static final String $_WHERE = "${where}";
private final SimpleTextParser textParser;
@@ -1,167 +0,0 @@
package com.avaje.ebean;
class SimpleTextParser {
private final String oql;
private final char[] chars;
private final int eof;
private int pos;
private String word;
private String lowerWord;
SimpleTextParser(String oql) {
this.oql = oql;
this.chars = oql.toCharArray();
this.eof = oql.length();
}
public int getPos() {
return pos;
}
public String getWord() {
return word;
}
public String peekNextWord() {
int origPos = pos;
String nw = nextWordInternal();
pos = origPos;
return nw;
}
/**
* Match the current and the next word.
*/
public boolean isMatch(String lowerMatch, String nextWordMatch) {
if (isMatch(lowerMatch)) {
String nw = peekNextWord();
if (nw != null) {
nw = nw.toLowerCase();
return nw.equals(nextWordMatch);
}
}
return false;
}
public boolean isFinished() {
return word == null;
}
public int findWordLower(String lowerMatch, int afterPos) {
this.pos = afterPos;
return findWordLower(lowerMatch);
}
public int findWordLower(String lowerMatch) {
do {
if (nextWord() == null) {
return -1;
}
if (lowerMatch.equals(lowerWord)) {
return pos - lowerWord.length();
}
} while (true);
}
/**
* Match the current word.
*/
public boolean isMatch(String lowerMatch) {
return lowerMatch.equals(lowerWord);
}
public String nextWord() {
word = nextWordInternal();
if (word != null) {
lowerWord = word.toLowerCase();
}
return word;
}
private String nextWordInternal() {
trimLeadingWhitespace();
if (pos >= eof) {
return null;
}
int start = pos;
if (chars[pos] == '(') {
moveToClose();
} else {
moveToEndOfWord();
}
return oql.substring(start, pos);
}
private void moveToClose() {
pos++;
int openParenthesisCount = 0;
for (; pos < eof; pos++) {
char c = chars[pos];
if (c == '(') {
// count nested parenthesis
openParenthesisCount++;
} else if (c == ')') {
if (openParenthesisCount > 0) {
// still in nested parenthesis
--openParenthesisCount;
} else {
// we have found the end
pos++;
return;
}
}
}
}
private void moveToEndOfWord() {
char c = chars[pos];
boolean isOperator = isOperator(c);
for (; pos < eof; pos++) {
c = chars[pos];
if (isWordTerminator(c, isOperator)) {
return;
}
}
}
private boolean isWordTerminator(char c, boolean isOperator) {
if (Character.isWhitespace(c)) {
return true;
}
if (isOperator(c)) {
return !isOperator;
}
return c == '(' || isOperator;
}
private boolean isOperator(char c) {
switch (c) {
case '<':
return true;
case '>':
return true;
case '=':
return true;
case '!':
return true;
default:
return false;
}
}
private void trimLeadingWhitespace() {
for (; pos < eof; pos++) {
char c = chars[pos];
if (!Character.isWhitespace(c)) {
break;
}
}
}
}
@@ -0,0 +1,50 @@
package com.avaje.ebean.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Specify a property to be an aggregation formula.
* <p>
* The aggregation formula should be a sum, count, avg, min or max.
* By default aggregation properties are treated as transient and not
* included in a query. To populate the aggregation property it must be
* explicitly included in the select().
* </p>
*
* <h3>Example:</h3>
* <pre>{@code
*
* @Aggregation("count(details)")
* Long totalCount;
*
* @Aggregation("sum(details.quantity*details.unitPrice)")
* Long totalAmount;
*
* }</pre>
*
* <h3>Example query</h3>
* <pre>{@code
*
* List<TEventOne> list = Ebean.find(TEventOne.class)
* .select("name, totalCount, totalUnits, totalAmount")
* .where()
* .startsWith("logs.description", "a")
* .having()
* .ge("count", 1)
* .orderBy().asc("name")
* .findList();
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Aggregation {
/**
* Aggregation formula using sum, count, avg, min, max.
*/
String value();
}
@@ -66,7 +66,7 @@ public class VisitAllUsing {
BeanProperty[] propertiesNonTransient = desc.propertiesNonTransient();
for (int i = 0; i < propertiesNonTransient.length; i++) {
BeanProperty p = propertiesNonTransient[i];
if (!p.isFormula() && !p.isSecondaryTable()) {
if (p.isDDLColumn()) {
visit(propertyVisitor, p);
}
}
@@ -25,6 +25,8 @@ public class ManyWhereJoins implements Serializable {
private boolean formulaWithJoin;
private boolean aggregation;
/**
* 'Mode' indicating that joins added while this is true are required to be outer joins.
*/
@@ -93,10 +95,10 @@ public class ManyWhereJoins implements Serializable {
}
/**
* Return true if there are no extra many where joins.
* Return true if this is an aggregation query or if there are no extra many where joins.
*/
public boolean isEmpty() {
return joins.isEmpty();
public boolean requireSqlDistinct() {
return !aggregation && !joins.isEmpty();
}
/**
@@ -146,4 +148,11 @@ public class ManyWhereJoins implements Serializable {
return formulaProperties.toString();
}
/**
* Mark this as part of an aggregation query (so using group by clause).
*/
public void setAggregation() {
aggregation = true;
}
}
@@ -38,7 +38,6 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanTable;
import com.avaje.ebeaninternal.server.deploy.meta.DeployTableJoin;
import com.avaje.ebeaninternal.server.deploy.parse.AnnotationBase;
import com.avaje.ebeaninternal.server.deploy.parse.DeployBeanInfo;
import com.avaje.ebeaninternal.server.deploy.parse.DeployCreateProperties;
import com.avaje.ebeaninternal.server.deploy.parse.DeployInherit;
@@ -381,13 +380,13 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
try {
entityClass = Class.forName(entityClassName, false, classLoader);
} catch (Exception e) {
logger.error("Could not load entity bean class "+entityClassName+" for ebean.xml entry");
logger.error("Could not load entity bean class " + entityClassName + " for ebean.xml entry");
return;
}
DeployBeanInfo<?> info = deployInfoMap.get(entityClass);
if (info == null) {
logger.error("No entity bean for ebean.xml entry "+entityClassName);
logger.error("No entity bean for ebean.xml entry " + entityClassName);
} else {
for (XmRawSql sql : entityDeploy.getRawSql()) {
@@ -1351,6 +1350,9 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
prop.setPropertyIndex(propertyIndex);
prop.setGetter(beanReflect.getGetter(propertyIndex));
prop.setSetter(beanReflect.getSetter(propertyIndex));
if (prop.isAggregation()) {
prop.setAggregationPrefix(DetermineAggPath.manyPath(prop.getAggregation(), desc));
}
}
}
}
@@ -35,6 +35,11 @@ public final class BeanFkeyProperty implements ElPropertyValue {
return "prefix:" + prefix + " name:" + name + " dbColumn:" + dbColumn + " ph:" + placeHolder;
}
@Override
public boolean isAggregation() {
return false;
}
public int getDeployOrder() {
return deployOrder;
}
@@ -158,6 +158,7 @@ public class BeanProperty implements ElPropertyValue, Property {
*/
final String dbColumn;
final String elPrefix;
final String elPlaceHolder;
final String elPlaceHolderEncrypted;
@@ -171,6 +172,8 @@ public class BeanProperty implements ElPropertyValue, Property {
*/
final String sqlFormulaJoin;
final String aggregation;
final boolean formula;
/**
@@ -311,6 +314,7 @@ public class BeanProperty implements ElPropertyValue, Property {
this.dbColumn = tableAliasIntern(descriptor, deploy.getDbColumn(), false, null);
this.dbComment = deploy.getDbComment();
this.aggregation = deploy.getAggregation();
this.sqlFormulaJoin = InternString.intern(deploy.getSqlFormulaJoin());
this.sqlFormulaSelect = InternString.intern(deploy.getSqlFormulaSelect());
this.formula = sqlFormulaSelect != null;
@@ -324,6 +328,7 @@ public class BeanProperty implements ElPropertyValue, Property {
this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null);
this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), dbEncrypted, dbColumn);
this.elPrefix = deploy.getElPrefix();
this.softDelete = deploy.isSoftDelete();
if (softDelete) {
@@ -370,6 +375,7 @@ public class BeanProperty implements ElPropertyValue, Property {
this.sqlFormulaJoin = null;
this.sqlFormulaSelect = null;
this.formula = false;
this.aggregation = null;
this.excludedFromHistory = source.excludedFromHistory;
this.draft = source.draft;
@@ -421,6 +427,7 @@ public class BeanProperty implements ElPropertyValue, Property {
this.field = source.getField();
this.docOptions = source.docOptions;
this.elPrefix = override.replace(source.elPrefix, source.dbColumn);
this.elPlaceHolder = override.replace(source.elPlaceHolder, source.dbColumn);
this.elPlaceHolderEncrypted = override.replace(source.elPlaceHolderEncrypted, source.dbColumn);
@@ -474,6 +481,13 @@ public class BeanProperty implements ElPropertyValue, Property {
return true;
}
/**
* Return true if this property should have a DB Column created in DDL.
*/
public boolean isDDLColumn() {
return !formula && !secondaryTable && (aggregation == null);
}
/**
* Return true if this property is based on a formula.
*/
@@ -537,8 +551,16 @@ public class BeanProperty implements ElPropertyValue, Property {
return secondaryTableJoinPrefix;
}
public boolean isAggregation() {
return aggregation != null;
}
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
if (formula) {
if (aggregation != null) {
ctx.appendRawColumn(aggregation);
} else if (formula) {
ctx.appendFormulaSelect(sqlFormulaSelect);
} else if (!isTransient && !ignoreDraftOnlyProperty(ctx.isDraftQuery())) {
@@ -843,7 +865,7 @@ public class BeanProperty implements ElPropertyValue, Property {
}
public boolean containsMany() {
return false;
return aggregation != null;
}
@Override
@@ -893,7 +915,7 @@ public class BeanProperty implements ElPropertyValue, Property {
}
public String getElPrefix() {
return secondaryTableJoinPrefix;
return elPrefix;
}
/**
@@ -125,4 +125,8 @@ public interface DbSqlContext {
*/
boolean isDraftQuery();
/**
* Start group by clause.
*/
void startGroupBy();
}
@@ -0,0 +1,96 @@
package com.avaje.ebeaninternal.server.deploy;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany;
import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne;
class DetermineAggPath {
/**
* Return the many path for the given aggregation formula.
*/
static String manyPath(String aggregation, DeployBeanDescriptor<?> desc) {
DetermineAggPath.Path path = paths(aggregation);
return path.getManyPath(0, desc);
}
static Path paths(String aggregation) {
String aggPath = path(aggregation);
return new Path(aggPath.split("\\."), aggregation);
}
/**
* Parse and return the full path for the aggregation.
*/
static String path(String aggregation) {
// aggregations always have a form of sum(), avg(), max(), count() etc
// so find the first open bracket
int start = aggregation.indexOf('(');
if (start == -1) {
throw new IllegalArgumentException("Aggregation formula ["+aggregation+"] is expected to have a '(' ?");
}
for (int i = start + 1; i< aggregation.length(); i++) {
char ch = aggregation.charAt(i);
if (!isNamePart(ch)) {
return aggregation.substring(start + 1, i);
}
}
throw new IllegalArgumentException("Could not find path in aggregation formula ["+aggregation+"]");
}
private static boolean isNamePart(char ch) {
return ch == '.' || Character.isJavaIdentifierPart(ch);
}
/**
* Helper class holding aggregation path segments.
*/
static class Path {
final String aggregation;
final String[] paths;
Path(String[] paths, String aggregation) {
this.paths = paths;
this.aggregation = aggregation;
}
int length() {
return paths.length;
}
String path(int pos) {
if (pos == 0) {
return paths[0];
} else {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < pos; i++) {
if (i > 0) {
sb.append(".");
}
sb.append(paths[i]);
}
return sb.toString();
}
}
String getManyPath(int pos, DeployBeanDescriptor<?> desc) {
String path = paths[pos];
DeployBeanProperty details = desc.getBeanProperty(path);
if (details instanceof DeployBeanPropertyAssocMany<?>) {
return path(pos);
} else if (details instanceof DeployBeanPropertyAssocOne<?>) {
DeployBeanPropertyAssocOne<?> one = (DeployBeanPropertyAssocOne<?>)details;
DeployBeanDescriptor<?> targetDesc = one.getTargetDeploy();
return getManyPath(pos + 1, targetDesc);
}
throw new IllegalArgumentException("Can not find path to many in aggregation formula ["+aggregation+"]");
}
}
}
@@ -160,6 +160,9 @@ public class DeployBeanProperty {
*/
private String dbColumn;
private String aggregationPrefix;
private String aggregation;
private String sqlFormulaSelect;
private String sqlFormulaJoin;
@@ -585,8 +588,42 @@ public class DeployBeanProperty {
this.dbUpdateable = false;
}
public boolean isAggregation() {
return aggregation != null;
}
public String getAggregation() {
return aggregation;
}
public void setAggregation(String aggregation) {
this.aggregation = aggregation;
this.dbRead = true;
this.dbInsertable = false;
this.dbUpdateable = false;
}
/**
* Set the path to the aggregation.
*/
public void setAggregationPrefix(String aggregationPrefix) {
this.aggregationPrefix = aggregationPrefix;
this.aggregation = aggregation.replace(aggregationPrefix, "u1");
}
public String getElPrefix() {
if (aggregation != null) {
return aggregationPrefix;
} else {
return secondaryTableJoinPrefix;
}
}
public String getElPlaceHolder() {
if (sqlFormulaSelect != null) {
if (aggregation != null) {
return aggregation;
} else if (sqlFormulaSelect != null) {
return sqlFormulaSelect;
} else {
if (secondaryTableJoinPrefix != null) {
@@ -604,6 +641,9 @@ public class DeployBeanProperty {
if (sqlFormulaSelect != null) {
return sqlFormulaSelect;
}
if (aggregation != null) {
return aggregation;
}
return dbColumn;
}
@@ -212,6 +212,11 @@ public class AnnotationFields extends AnnotationParser {
prop.setSqlFormula(formula.select(), formula.join());
}
Aggregation aggregation = get(prop, Aggregation.class);
if (aggregation != null) {
prop.setAggregation(aggregation.value());
}
Version version = get(prop, Version.class);
if (version != null) {
// explicitly specify a version column
@@ -83,6 +83,11 @@ public class ElPropertyChain implements ElPropertyValue {
return "expr:" + expression + " chain:" + Arrays.toString(chain);
}
@Override
public boolean isAggregation() {
return false;
}
private String getElPlaceHolder(String prefix, ElPropertyValue lastElPropertyValue, boolean encrypted) {
if (prefix == null) {
return lastElPropertyValue.getElPlaceholder(encrypted);
@@ -69,4 +69,9 @@ public interface ElPropertyDeploy {
* Return the underlying bean property.
*/
BeanProperty getBeanProperty();
/**
* Return true if this is an aggregation property.
*/
boolean isAggregation();
}
@@ -72,6 +72,9 @@ public abstract class AbstractExpression implements SpiExpression {
if (elProp.containsMany()) {
// for findRowCount we join to a many property
manyWhereJoin.add(elProp);
if (elProp.isAggregation()) {
manyWhereJoin.setAggregation();
}
}
}
}
@@ -531,6 +531,16 @@ public class CQueryBuilder {
}
}
String groupBy = select.getGroupBy();
if (groupBy != null) {
sb.append(" group by ").append(groupBy);
}
String dbHaving = predicates.getDbHaving();
if (!isEmpty(dbHaving)) {
sb.append(" having ").append(dbHaving);
}
if (dbOrderBy != null) {
sb.append(" order by ").append(dbOrderBy);
}
@@ -26,7 +26,7 @@ public class DefaultDbSqlContext implements DbSqlContext {
private final ArrayStack<String> prefixStack = new ArrayStack<String>();
private final boolean useColumnAlias;
private boolean useColumnAlias;
private int columnIndex;
@@ -66,6 +66,11 @@ public class DefaultDbSqlContext implements DbSqlContext {
this.historyQuery = (historySupport != null);
}
@Override
public void startGroupBy() {
this.useColumnAlias = false;
}
public void addEncryptedProp(BeanProperty p) {
if (encryptedProps == null) {
encryptedProps = new ArrayList<BeanProperty>();
@@ -31,6 +31,8 @@ public class SqlTree {
private final String fromSql;
private final String groupBy;
/**
* Encrypted Properties require additional binding.
*/
@@ -46,13 +48,14 @@ public class SqlTree {
/**
* Create the SqlSelectClause.
*/
public SqlTree(String summary, SqlTreeNode rootNode, String selectSql, String fromSql, String inheritanceWhereSql,
public SqlTree(String summary, SqlTreeNode rootNode, String selectSql, String fromSql, String groupBy, String inheritanceWhereSql,
BeanProperty[] encryptedProps, BeanPropertyAssocMany<?> manyProperty, Set<String> includes, boolean includeJoins) {
this.summary = summary;
this.rootNode = rootNode;
this.selectSql = selectSql;
this.fromSql = fromSql;
this.groupBy = groupBy;
this.inheritanceWhereSql = inheritanceWhereSql;
this.encryptedProps = encryptedProps;
this.manyProperty = manyProperty;
@@ -108,6 +111,13 @@ public class SqlTree {
return fromSql;
}
/**
* Return the groupBy clause.
*/
public String getGroupBy() {
return groupBy;
}
/**
* Return the where clause for inheritance.
*/
@@ -129,17 +129,19 @@ public class SqlTreeBuilder {
String selectSql = null;
String fromSql = null;
String inheritanceWhereSql = null;
String groupBy = null;
BeanProperty[] encryptedProps = null;
if (!rawSql) {
selectSql = buildSelectClause();
fromSql = buildFromClause();
inheritanceWhereSql = buildWhereClause();
groupBy = buildGroupByClause();
encryptedProps = ctx.getEncryptedProps();
}
boolean includeJoins = (alias == null) ? false : alias.isIncludeJoins();
return new SqlTree(summary.toString(), rootNode, selectSql, fromSql, inheritanceWhereSql, encryptedProps,
return new SqlTree(summary.toString(), rootNode, selectSql, fromSql, groupBy, inheritanceWhereSql, encryptedProps,
manyProperty, queryDetail.getFetchPaths(), includeJoins);
}
@@ -149,15 +151,28 @@ public class SqlTreeBuilder {
return "Not Used";
}
rootNode.appendSelect(ctx, subQuery);
return trimComma(ctx.getContent());
}
String selectSql = ctx.getContent();
private String buildGroupByClause() {
// trim off the first comma
if (selectSql.length() >= SqlTreeNode.COMMA.length()) {
selectSql = selectSql.substring(SqlTreeNode.COMMA.length());
if (rawSql || !rootNode.isAggregation()) {
return null;
}
ctx.startGroupBy();
rootNode.appendGroupBy(ctx, subQuery);
String groupBy = ctx.getContent();
return trimComma(groupBy);
}
return selectSql;
/**
* Trim the first comma.
*/
private String trimComma(String groupBy) {
if (groupBy.length() >= SqlTreeNode.COMMA.length()) {
groupBy = groupBy.substring(SqlTreeNode.COMMA.length());
}
return groupBy;
}
private String buildWhereClause() {
@@ -19,12 +19,22 @@ public interface SqlTreeNode {
*/
void buildRawSqlSelectChain(List<String> selectChain);
/**
* Return true if this node includes an aggregation.
*/
boolean isAggregation();
/**
* Append the required column information to the SELECT part of the sql
* statement.
*/
void appendSelect(DbSqlContext ctx, boolean subQuery);
/**
* Append the group by clause.
*/
void appendGroupBy(DbSqlContext ctx, boolean subQuery);
/**
* Append to the FROM part of the sql.
*/
@@ -83,6 +83,8 @@ public class SqlTreeNodeBean implements SqlTreeNode {
*/
private boolean intersectionAsOfTableAlias;
private boolean aggregation;
/**
* Construct for leaf node.
*/
@@ -381,6 +383,23 @@ public class SqlTreeNodeBean implements SqlTreeNode {
}
}
@Override
public void appendGroupBy(DbSqlContext ctx, boolean subQuery) {
ctx.pushJoin(prefix);
ctx.pushTableAlias(prefix);
if (readId) {
appendSelectId(ctx, idBinder.getBeanProperty());
}
for (int i = 0; i < properties.length; i++) {
if (!properties[i].isAggregation()) {
properties[i].appendSelect(ctx, subQuery);
}
}
ctx.popTableAlias();
ctx.popJoin();
}
/**
* Append the property columns to the buffer.
*/
@@ -417,6 +436,10 @@ public class SqlTreeNodeBean implements SqlTreeNode {
ctx.popJoin();
}
public boolean isAggregation() {
return aggregation;
}
/**
* Append the properties to the buffer.
*/
@@ -424,6 +447,9 @@ public class SqlTreeNodeBean implements SqlTreeNode {
for (int i = 0; i < props.length; i++) {
props[i].appendSelect(ctx, subQuery);
if (props[i].isAggregation()) {
aggregation = true;
}
}
}
@@ -56,6 +56,16 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode {
// nothing to do here
}
@Override
public boolean isAggregation() {
return false;
}
@Override
public void appendGroupBy(DbSqlContext ctx, boolean subQuery) {
// nothing to do here
}
@Override
public BeanProperty getSingleProperty() {
throw new IllegalStateException("No expected");
@@ -55,6 +55,16 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode {
// do nothing here ...
}
@Override
public boolean isAggregation() {
return false;
}
@Override
public void appendGroupBy(DbSqlContext ctx, boolean subQuery) {
// do nothing here
}
/**
* Append to the FROM clause for this node.
*/
@@ -370,7 +370,10 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
if (whereExpressions != null) {
whereExpressions.containsMany(beanDescriptor, manyWhereJoins);
}
if (!manyWhereJoins.isEmpty()) {
if (havingExpressions != null) {
havingExpressions.containsMany(beanDescriptor, manyWhereJoins);
}
if (manyWhereJoins.requireSqlDistinct()) {
setSqlDistinct(true);
}
}
@@ -29,6 +29,11 @@ public class CtCompoundPropertyElAdapter implements ElPropertyValue {
this.deployOrder = deployOrder;
}
@Override
public boolean isAggregation() {
return false;
}
@Override
public Object convert(Object value) {
return value;
@@ -0,0 +1,34 @@
package com.avaje.ebeaninternal.server.deploy;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class DetermineAggPathTest {
@Test
public void path() throws Exception {
assertThat(DetermineAggPath.path("count(details)")).isEqualTo("details");
assertThat(DetermineAggPath.path("count(details )")).isEqualTo("details");
assertThat(DetermineAggPath.path("count(person.contacts)")).isEqualTo("person.contacts");
assertThat(DetermineAggPath.path("sum(details.quantity*details.unitPrice)")).isEqualTo("details.quantity");
}
@Test
public void paths_simple() throws Exception {
DetermineAggPath.Path paths = DetermineAggPath.paths("count(details)");
assertThat(paths.paths).isEqualTo(new String[]{"details"});
}
@Test
public void paths_nested() throws Exception {
DetermineAggPath.Path paths = DetermineAggPath.paths("count(person.contacts)");
assertThat(paths.paths).isEqualTo(new String[]{"person", "contacts"});
}
}
@@ -17,10 +17,12 @@ public class TestOrderTotalAmountFormula extends BaseTestCase {
ResetBasicData.reset();
List<Customer> l0 = Ebean.find(Customer.class).select("id, name")
.fetch("orders", "status, totalAmount").where().eq("orders.details.product.name", "Desk")
.like("contacts.firstName", "Ji%")
List<Customer> l0 = Ebean.find(Customer.class)
.select("id, name")
.fetch("orders", "status, totalAmount")
.where()
.eq("orders.details.product.name", "Desk")
.like("contacts.firstName", "Ji%")
.findList();
for (Customer c0 : l0) {
@@ -11,14 +11,24 @@ public class TEventMany {
@Id
Long id;
String many;
String description;
@ManyToOne
TEventOne one;
TEventOne event;
int units;
double amount;
@Version
Long version;
public TEventMany(String description, int units, double amount) {
this.description = description;
this.units = units;
this.amount = amount;
}
public Long getId() {
return id;
}
@@ -27,20 +37,36 @@ public class TEventMany {
this.id = id;
}
public String getMany() {
return many;
public String getDescription() {
return description;
}
public void setMany(String many) {
this.many = many;
public void setDescription(String description) {
this.description = description;
}
public TEventOne getOne() {
return one;
public TEventOne getEvent() {
return event;
}
public void setOne(TEventOne one) {
this.one = one;
public void setEvent(TEventOne event) {
this.event = event;
}
public int getUnits() {
return units;
}
public void setUnits(int units) {
this.units = units;
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public Long getVersion() {
@@ -1,5 +1,8 @@
package com.avaje.tests.model.tevent;
import com.avaje.ebean.annotation.Aggregation;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@@ -13,7 +16,7 @@ public class TEventOne {
@Id
Long id;
String one;
String name;
@Version
Long version;
@@ -21,8 +24,21 @@ public class TEventOne {
@OneToOne
TEvent event;
@OneToMany(mappedBy = "one")
List<TEventMany> many;
@Aggregation("count(logs.*)")
Long count;
@Aggregation("sum(logs.units)")
Double totalUnits;
@Aggregation("sum(logs.units * logs.amount)")
Double totalAmount;
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
List<TEventMany> logs;
public TEventOne(String name) {
this.name = name;
}
public Long getId() {
return id;
@@ -32,12 +48,24 @@ public class TEventOne {
this.id = id;
}
public String getOne() {
return one;
public Long getCount() {
return count;
}
public void setOne(String one) {
this.one = one;
public Double getTotalUnits() {
return totalUnits;
}
public Double getTotalAmount() {
return totalAmount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getVersion() {
@@ -56,11 +84,11 @@ public class TEventOne {
this.event = event;
}
public List<TEventMany> getMany() {
return many;
public List<TEventMany> getLogs() {
return logs;
}
public void setMany(List<TEventMany> many) {
this.many = many;
public void setLogs(List<TEventMany> logs) {
this.logs = logs;
}
}
@@ -13,7 +13,20 @@ public class TestAssocOneNullTraverse extends BaseTestCase {
Ebean.save(event);
Ebean.find(TEvent.class)
.fetch("one.many")
.fetch("one.logs")
.findList();
}
// @Test
// public void testSelectAggregation() {
//
// Query<TEvent> query = Ebean.find(TEvent.class)
// .select("id, name")
// .fetch("one", "count");
//
// query.findList();
//
// String sql = query.getGeneratedSql();
// assertThat(sql).contains("asd");
// }
}
@@ -0,0 +1,55 @@
package com.avaje.tests.query.aggregation;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.Query;
import com.avaje.tests.model.tevent.TEventMany;
import com.avaje.tests.model.tevent.TEventOne;
import org.junit.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestAggregationCount extends BaseTestCase {
@Test
public void test() {
TEventOne one = new TEventOne("first");
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");
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);
Query<TEventOne> query = Ebean.find(TEventOne.class)
.select("name, count, totalUnits, totalAmount")
.where()
.startsWith("logs.description", "a")
.having()
.ge("count", 1)
.orderBy().asc("name");
List<TEventOne> list = query.findList();
for (TEventOne eventOne : list) {
System.out.println(eventOne.getId() + " " + eventOne.getName() + " count:" + eventOne.getCount() + " units:" + eventOne.getTotalUnits() + " amount:" + eventOne.getTotalAmount());
}
assertThat(list).isNotEmpty();
String sql = query.getGeneratedSql();
assertThat(sql).contains("select t0.id c0, t0.name c1, count(u1.*) c2, sum(u1.units) c3, sum(u1.units * u1.amount) c4 from tevent_one t0");
assertThat(sql).contains("from tevent_one t0 join tevent_many u1 on u1.event_id = t0.id ");
assertThat(sql).contains("where u1.description like ? ");
assertThat(sql).contains(" group by t0.id, t0.name having count(u1.*) >= ? order by t0.name");
}
}