Another alternative for group by ResultTransformer

This commit is contained in:
Samppa Saarela 2011-09-15 15:52:06 +03:00
parent 2a138217b4
commit e281332656
3 changed files with 413 additions and 4 deletions

View File

@ -0,0 +1,256 @@
/*
* Copyright (c) 2010 Mysema Ltd.
* All rights reserved.
*
*/
package com.mysema.query.support;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import com.mysema.commons.lang.CloseableIterator;
import com.mysema.query.Projectable;
import com.mysema.query.ResultTransformer;
import com.mysema.query.Tuple;
import com.mysema.query.types.Expression;
import com.mysema.query.types.Visitor;
/**
* Groups results by the first expression.
* <ol>
* <li>Order of groups by position of the first row of a group
* <li>Rows belonging to a group may appear in any order
* <li>Group of null is handled correctly
* </ol>
*
* @author sasa
*/
//@SuppressWarnings("unchecked")
public class GroupBy2 implements ResultTransformer<Collection<Tuple>> {
private final Expression<?>[] expressions;
private static interface GroupFactory<T, R> {
public void add(Object o);
public R get();
}
/**
* NOTE: This expression only applies to GroupBy.transform
*
* @param <T>
* @param <R>
*/
public static abstract class GroupAsExpression<T, R> implements Expression<R> {
private static final long serialVersionUID = -8164758792405567077L;
private final Expression<T> expr;
public GroupAsExpression(Expression<T> expr) {
this.expr = expr;
}
@Override
public <S, C> S accept(Visitor<S, C> v, C context) {
throw new UnsupportedOperationException();
}
@Override
public Class<? extends R> getType() {
throw new UnsupportedOperationException();
}
public int hashCode() {
return expr.hashCode();
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof GroupAsExpression) {
GroupAsExpression<?, ?> other = (GroupAsExpression<?, ?>) o;
return this.expr.equals(other.expr);
} else {
return false;
}
}
public abstract GroupFactory<T, R> createGroupFactory();
}
public static <T> GroupAsExpression<T, Set<T>> set(Expression<T> expr) {
return new GroupAsExpression<T, Set<T>>(expr) {
private static final long serialVersionUID = -2507144565843468159L;
@Override
public GroupFactory<T, Set<T>> createGroupFactory() {
return new GroupFactory<T, Set<T>>() {
private final Set<T> set = new HashSet<T>();
@Override
public void add(Object o) {
set.add((T) o);
}
@Override
public Set<T> get() {
return set;
}
};
}
};
}
public static <T> GroupAsExpression<T, List<T>> list(Expression<T> expr) {
return new GroupAsExpression<T, List<T>>(expr) {
private static final long serialVersionUID = -6941324182786049824L;
@Override
public GroupFactory<T, List<T>> createGroupFactory() {
return new GroupFactory<T, List<T>>() {
private final List<T> list = new ArrayList<T>();
@Override
public void add(Object o) {
list.add((T) o);
}
@Override
public List<T> get() {
return list;
}
};
}
};
}
private static class ValueGroupFactory<T> implements GroupFactory<T, T> {
private T val;
private boolean first = true;
@Override
public void add(Object o) {
if (first) {
val = (T) o;
first = false;
}
}
@Override
public T get() {
return val;
}
}
public GroupBy2(Expression<?> groupBy, Expression<?>... args) {
expressions = new Expression<?>[args.length + 1];
expressions[0] = groupBy;
System.arraycopy(args, 0, expressions, 1, args.length);
}
@Override
public Collection<Tuple> transform(Projectable projectable) {
final LinkedHashMap<Object, Tuple> groups = new LinkedHashMap<Object, Tuple>();
CloseableIterator<Object[]> iter = projectable.iterate(unwrap(expressions));
try {
while (iter.hasNext()) {
Object[] row = iter.next();
Object groupBy = row[0];
// groups.values() should return Collection<GTuple> instead of Collection<? extends GTuple>
GroupTuple group = (GroupTuple) groups.get(groupBy);
if (group == null) {
group = new GroupTuple();
groups.put(groupBy, group);
}
group.add(row);
}
} finally {
iter.close();
}
return groups.values();
}
private static Expression<?>[] unwrap(Expression<?>... expressions) {
Expression<?>[] unwrapped = new Expression<?>[expressions.length];
for (int i=0; i < expressions.length; i++) {
if (expressions[i] instanceof GroupAsExpression) {
unwrapped[i] = ((GroupAsExpression<?, ?>) expressions[i]).expr;
} else {
unwrapped[i] = expressions[i];
}
}
return unwrapped;
}
private int indexOf(Expression<?> expr) {
for (int i=0; i < expressions.length; i++) {
if (expressions[i].equals(expr)) {
return i;
}
}
return -1;
}
private class GroupTuple implements Tuple {
private final GroupFactory<?, ?>[] groupFactories;
public GroupTuple() {
groupFactories = new GroupFactory<?, ?>[expressions.length];
for (int i=0; i < expressions.length; i++) {
if (expressions[i] instanceof GroupAsExpression<?, ?>) {
groupFactories[i] = ((GroupAsExpression<?, ?>) expressions[i]).createGroupFactory();
} else {
groupFactories[i] = new ValueGroupFactory<Object>();
}
}
}
@Override
public <T> T get(int index, Class<T> type) {
return (T) groupFactories[index].get();
}
@Override
public <T> T get(Expression<T> expr) {
int index = indexOf(expr);
return (T) groupFactories[index].get();
}
@Override
public Object[] toArray() {
Object[] row = new Object[groupFactories.length];
for (int i=0; i < groupFactories.length; i++) {
row[i] = groupFactories[i].get();
}
return row;
}
public void add(Object[] row) {
for (int i=0; i < groupFactories.length; i++) {
groupFactories[i].add(row[i]);
}
}
}
}

View File

@ -0,0 +1,154 @@
package com.mysema.query.support;
import static com.mysema.query.support.GroupBy2.list;
import static com.mysema.query.support.GroupBy2.set;
import static junit.framework.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import com.mysema.commons.lang.CloseableIterator;
import com.mysema.commons.lang.EmptyCloseableIterator;
import com.mysema.commons.lang.IteratorAdapter;
import com.mysema.query.Projectable;
import com.mysema.query.Tuple;
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 GroupBy2Test {
private final NumberExpression<Integer> postId = new NumberPath<Integer>(Integer.class, "postId");
private final StringExpression postName = new StringPath("postName");
private final NumberExpression<Integer> commentId = new NumberPath<Integer>(Integer.class, "commentId");
static class PostWithComments {
public Integer id;
public String name;
public Set<Integer> comments;
public PostWithComments(Integer id, String name, Set<Integer> comments) {
this.id = id;
this.name = name;
this.comments = comments;
}
}
@Test
public void Expression_Order_And_Type() {
new GroupBy2(postId, postName, set(commentId)).transform(new AbstractProjectable(){
public CloseableIterator<Object[]> iterate(Expression<?>[] args) {
assertEquals(postId, args[0]);
assertEquals(postName, args[1]);
assertEquals(commentId, args[2]);
return new EmptyCloseableIterator<Object[]>();
}
});
}
/**
* <ol>
* <li>Order of groups by first row of a group
* <li>Rows belonging to a group may appear in any order
* <li>Group of null is handled correctly
* </ol>
*/
@Test
public void Multiple_Groups() {
Collection<Tuple> results = new GroupBy2(postId, postName, list(commentId)).transform(
projectable(
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<Tuple> iter = results.iterator();
Tuple g = iter.next();
assertEquals(toInt(1), g.get(postId));
assertEquals("post 1", g.get(postName));
List<Integer> comments = g.get(list(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));
comments = g.get(list(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));
comments = g.get(list(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));
comments = g.get(list(commentId));
assertEquals(toInt(7), comments.get(0));
assertEquals(toInt(8), comments.get(1));
}
private static <T> Set<T> toSet(T... o) {
return new HashSet<T>(Arrays.asList(o));
}
@Test
public void Group_As_Set() {
Collection<Tuple> results = new GroupBy2(postId, postName, set(commentId)).transform(projectable(
row(1, "post 1", 1),
row(null, "null post", 2)
));
assertEquals(2, results.size());
Iterator<Tuple> iter = results.iterator();
Tuple group = iter.next();
assertEquals(toInt(1), group.get(postId));
assertEquals("post 1", group.get(postName));
assertEquals(toSet(1), group.get(set(commentId)));
group = iter.next();
assertEquals(null, group.get(postId));
}
private Projectable projectable(final Object[]... rows) {
return new AbstractProjectable(){
public CloseableIterator<Object[]> iterate(Expression<?>[] args) {
return iterator(rows);
}
};
}
private Integer toInt(int i) {
return Integer.valueOf(i);
}
private static Object[] row(Object... row) {
return row;
}
private static CloseableIterator<Object[]> iterator(Object[]... rows) {
return new IteratorAdapter<Object[]>(Arrays.asList(rows).iterator());
}
}

View File

@ -143,7 +143,7 @@ public class GroupByTest {
@Test
public void Nested_Transformers() {
// NOTE: This kind of bean projecting transformer can be generalized...
// Maybe this kind of bean projecting transformer can be generalized...
// A transformer that uses results of GroupBy transformer to instantiate beans with collections
List<PostWithComments> posts = new ResultTransformer<List<PostWithComments>>() {
@ -192,8 +192,7 @@ public class GroupByTest {
}
@SuppressWarnings("unchecked")
private static <T> CloseableIterator<T> iterator(Object[]... rows) {
return new IteratorAdapter(Arrays.asList(rows).iterator());
private static CloseableIterator<Object[]> iterator(Object[]... rows) {
return new IteratorAdapter<Object[]>(Arrays.asList(rows).iterator());
}
}