diff --git a/querydsl-core/src/main/java/com/mysema/query/ResultTransformer.java b/querydsl-core/src/main/java/com/mysema/query/ResultTransformer.java new file mode 100644 index 000000000..a6f5fdf54 --- /dev/null +++ b/querydsl-core/src/main/java/com/mysema/query/ResultTransformer.java @@ -0,0 +1,8 @@ +package com.mysema.query; + + +public interface ResultTransformer { + + public T transform(Projectable projectable); + +} diff --git a/querydsl-core/src/main/java/com/mysema/query/support/Group.java b/querydsl-core/src/main/java/com/mysema/query/support/Group.java new file mode 100644 index 000000000..a001fbad6 --- /dev/null +++ b/querydsl-core/src/main/java/com/mysema/query/support/Group.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010 Mysema Ltd. + * All rights reserved. + * + */ +package com.mysema.query.support; + +import java.util.List; + +import javax.annotation.Nullable; + +import com.mysema.query.types.Expression; + +/** + * An interface for grouped results. Group identifier is always the first element. + * + * @author sasa + */ +public interface Group { + + /** + * Get an element of first row of this group by index + * + * @param + * @param index + * @param type + * @return + */ + @Nullable + T get(int index, Class type); + + /** + * Get an element of first row of this group by expr + * + * @param + * @param expr + * @return + */ + @Nullable + T get(Expression expr); + + /** + * Get a list of elements (all rows) of this group by expression + * + * @param + * @param index + * @param type + * @return + */ + @Nullable + List getList(int index, Class type); + + + /** + * Get a list of elements (all rows) of this group by expression + * + * @param + * @param expr + * @return + */ + @Nullable + List getList(Expression expr); + + int size(); + +} diff --git a/querydsl-core/src/main/java/com/mysema/query/support/GroupBy.java b/querydsl-core/src/main/java/com/mysema/query/support/GroupBy.java new file mode 100644 index 000000000..79122ab63 --- /dev/null +++ b/querydsl-core/src/main/java/com/mysema/query/support/GroupBy.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2010 Mysema Ltd. + * All rights reserved. + * + */ +package com.mysema.query.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +import com.mysema.commons.lang.CloseableIterator; +import com.mysema.query.Projectable; +import com.mysema.query.ResultTransformer; +import com.mysema.query.types.Expression; + +/** + * Groups results by the first expression. + *
    + *
  1. Order of groups by position of the first row of a group + *
  2. Rows belonging to a group may appear in any order + *
  3. Group of null is handled correctly + *
+ * + * @author sasa + */ +public class GroupBy implements ResultTransformer> { + + private static final long serialVersionUID = 1L; + + private final Expression[] expressions; + + public GroupBy(Expression groupBy, Expression... args) { + expressions = new Expression[args.length + 1]; + expressions[0] = groupBy; + for (int i=0; i < args.length; i++) { + expressions[i+1] = args[i]; + } + } + + @Override + public Collection transform(Projectable projectable) { + final LinkedHashMap groups = new LinkedHashMap(); + + CloseableIterator iter = projectable.iterate(expressions); + try { + while (iter.hasNext()) { + Object[] row = iter.next(); + Object groupBy = row[0]; + // groups.values() should return Collection instead of Collection + GTupleImpl group = (GTupleImpl) groups.get(groupBy); + if (group == null) { + group = new GTupleImpl(); + groups.put(groupBy, group); + } + group.add(row); + } + } finally { + iter.close(); + } + return groups.values(); + } + + private int indexOf(Expression expr) { + for (int i=0; i < expressions.length; i++) { + if (expressions[i].equals(expr)) { + return i; + } + } + return -1; + } + + @SuppressWarnings("unchecked") + private class GTupleImpl implements Group { + + private final List values = new ArrayList(); + + @Override + public T get(int index, Class type) { + return (T) values.get(0)[index]; + } + + private void add(Object[] row) { + this.values.add(row); + } + + @Override + public T get(Expression expr) { + int index = indexOf(expr); + return index != -1 ? (T) values.get(0)[index] : null; + } + + @Override + public List getList(int index, Class type) { + List list = new ArrayList(values.size()); + for (Object[] o : values) { + list.add((T) o[index]); + } + return list; + } + + @Override + public List getList(Expression expr) { + int index = indexOf(expr); + if (index < 0) { + return null; + } else { + List list = new ArrayList(values.size()); + for (Object[] o : values) { + list.add((T) o[index]); + } + return list; + } + } + + public int size() { + return values.size(); + } + } + +} diff --git a/querydsl-core/src/test/java/com/mysema/query/support/AbstractProjectable.java b/querydsl-core/src/test/java/com/mysema/query/support/AbstractProjectable.java new file mode 100644 index 000000000..876aa2752 --- /dev/null +++ b/querydsl-core/src/test/java/com/mysema/query/support/AbstractProjectable.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2010 Mysema Ltd. + * All rights reserved. + * + */ +package com.mysema.query.support; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.mysema.commons.lang.CloseableIterator; +import com.mysema.commons.lang.EmptyCloseableIterator; +import com.mysema.query.Projectable; +import com.mysema.query.SearchResults; +import com.mysema.query.types.Expression; + +public class AbstractProjectable implements Projectable { + + @Override + public long count() { + return 0; + } + + @Override + public long countDistinct() { + return 0; + } + + @Override + public boolean exists() { + return 0 < count(); + } + + @Override + public boolean notExists() { + return !exists(); + } + + @Override + public CloseableIterator iterate(Expression first, + Expression second, Expression... rest) { + return null; + } + + @Override + public CloseableIterator iterate(Expression[] args) { + return new EmptyCloseableIterator(); + } + + @Override + public CloseableIterator iterate(Expression projection) { + return new EmptyCloseableIterator(); + } + + @Override + public CloseableIterator iterateDistinct(Expression first, + Expression second, Expression... rest) { + return new EmptyCloseableIterator(); + } + + @Override + public CloseableIterator iterateDistinct(Expression[] args) { + return new EmptyCloseableIterator(); + } + + @Override + public CloseableIterator iterateDistinct(Expression projection) { + return new EmptyCloseableIterator(); + } + + @Override + public List list(Expression first, Expression second, + Expression... rest) { + return Collections.emptyList(); + } + + @Override + public List list(Expression[] args) { + return Collections.emptyList(); + } + + @Override + public List list(Expression projection) { + return Collections.emptyList(); + } + + @Override + public List listDistinct(Expression first, + Expression second, Expression... rest) { + return Collections.emptyList(); + } + + @Override + public List listDistinct(Expression[] args) { + return Collections.emptyList(); + } + + @Override + public List listDistinct(Expression projection) { + return Collections.emptyList(); + } + + @Override + public SearchResults listResults(Expression projection) { + return new SearchResults(Collections.emptyList(), null, null, 0l); + } + + @Override + public SearchResults listDistinctResults(Expression projection) { + return new SearchResults(Collections.emptyList(), null, null, 0l); + } + + @Override + public Map map(Expression key, Expression value) { + return Collections.emptyMap(); + } + + @Override + public Object[] singleResult(Expression first, Expression second, + Expression... rest) { + return new Object[0]; + } + + @Override + public Object[] singleResult(Expression[] args) { + return null; + } + + @Override + public RT singleResult(Expression projection) { + return null; + } + + @Override + public Object[] uniqueResult(Expression first, Expression second, + Expression... rest) { + return null; + } + + @Override + public Object[] uniqueResult(Expression[] args) { + return null; + } + + @Override + public RT uniqueResult(Expression projection) { + return null; + } + +} diff --git a/querydsl-core/src/test/java/com/mysema/query/support/GroupByTest.java b/querydsl-core/src/test/java/com/mysema/query/support/GroupByTest.java new file mode 100644 index 000000000..5e2e48fac --- /dev/null +++ b/querydsl-core/src/test/java/com/mysema/query/support/GroupByTest.java @@ -0,0 +1,104 @@ +package com.mysema.query.support; + + +import static junit.framework.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.junit.Test; + +import com.mysema.commons.lang.CloseableIterator; +import com.mysema.commons.lang.IteratorAdapter; +import com.mysema.query.types.Expression; +import com.mysema.query.types.expr.NumberExpression; +import com.mysema.query.types.expr.StringExpression; +import com.mysema.query.types.path.NumberPath; +import com.mysema.query.types.path.StringPath; + +public class GroupByTest { + + private final NumberExpression postId = new NumberPath(Integer.class, null, "postId"); + + private final StringExpression postName = new StringPath(null, "postName"); + + private final NumberExpression commentId = new NumberPath(Integer.class, null, "commentId"); + + /** + *
    + *
  1. Order of groups by first row of a group + *
  2. Rows belonging to a group may appear in any order + *
  3. Group of null is handled correctly + *
+ */ + @Test + public void Group_By() { + Collection results = new GroupBy(postId, postName, commentId).transform(new AbstractProjectable(){ + public CloseableIterator iterate(Expression[] args) { + assertEquals(postId, args[0]); + assertEquals(postName, args[1]); + assertEquals(commentId, args[2]); + + return iterator( + row(1, "post 1", 1), + row(2, "post 2", 4), + row(1, "post 1", 2), + row(2, "post 2", 5), + row(3, "post 3", 6), + row(null, "null post", 7), + row(null, "null post", 8), + row(1, "post 1", 3) + ); + } + }); + assertEquals(4, results.size()); + Iterator iter = results.iterator(); + + Group g = iter.next(); + assertEquals(toInt(1), g.get(postId)); + assertEquals("post 1", g.get(postName)); + assertEquals(3, g.size()); + List comments = g.getList(commentId); + assertEquals(toInt(1), comments.get(0)); + assertEquals(toInt(2), comments.get(1)); + assertEquals(toInt(3), comments.get(2)); + + g = iter.next(); + assertEquals(toInt(2), g.get(postId)); + assertEquals("post 2", g.get(postName)); + assertEquals(2, g.size()); + comments = g.getList(commentId); + assertEquals(toInt(4), comments.get(0)); + assertEquals(toInt(5), comments.get(1)); + + g = iter.next(); + assertEquals(toInt(3), g.get(postId)); + assertEquals("post 3", g.get(postName)); + assertEquals(1, g.size()); + comments = g.getList(commentId); + assertEquals(toInt(6), comments.get(0)); + + // Group by null value + g = iter.next(); + assertEquals(null, g.get(postId)); + assertEquals("null post", g.get(postName)); + assertEquals(2, g.size()); + comments = g.getList(commentId); + assertEquals(toInt(7), comments.get(0)); + assertEquals(toInt(8), comments.get(1)); + } + + private Integer toInt(int i) { + return Integer.valueOf(i); + } + + private static Object[] row(Object... row) { + return row; + } + + private static CloseableIterator iterator(Object[]... rows) { + return new IteratorAdapter(Arrays.asList(rows).iterator()); + } +}