Your browser (Internet Explorer 7 or lower) is out of date. It has known security flaws and may not display all features of this and other websites. Learn how to update your browser.

X

Sorting Spring Data MongoDB collections using @OrderBy

This is already third post about tuning and enhancing Spring Data MongoDB capabilities. This time I found that I miss one JPA feature – @OrderBy annotation. @OrderBy specifies the ordering of the elements of a collection valued association at the point when the association is retrieved.

In this article I will show how to implement sorting with @OrderBy annotation with Spring Data MongoDB.

Use case

Just a short example of what is it all about for those who did not use JPA @OrderBy before. We’ve got here two classes and one to many relation:

package pl.maciejwalkowiak.springdata.mongodb.domain;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document
public class Backpack {
	@Id
	private ObjectId id;

	private List<Item> items;

	...
}
public class Item {
	private String name;
	private int price;

	...
}

Backpack is here a main class and contains list of embedded items. When Backpack is loaded from database its items are loaded in order close to insertion order. What if we want to change that and order items by one of its fields? We need to implement sorting by our own and again we will extend AbstractMongoEventListener.

Sorting details: introducing @OrderBy

In opposite to JPA – sorting in this case sorting cannot be done on database level. We need to take care about it on application side – that can be done in two places:

  • before object is converted into MongoDB data structure – if we want to make sure that objects are sorted properly inside MongoDB collection
  • after object is converted from MongoDB data structure into Java object – if we just want to make sure that inside our application List is sorted properly

In order to specify in which place sorting should take place I have created SortPhase enumeration:

public enum SortPhase {
	AFTER_CONVERT,
	BEFORE_CONVERT;
}

Finally – @OrderBy annotation will contain three almost self describing properties:

package pl.maciejwalkowiak.springdata.mongodb;

import org.springframework.data.mongodb.core.query.Order;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface OrderBy {
	/**
	 * Field name
	 */
	String value();
	Order order() default Order.ASCENDING;
	SortPhase[] phase() default SortPhase.AFTER_CONVERT;
}

Implementing SortingMongoEventListener

Declarative sorting has to use reflection. To keep code readable I used commons-beanutils but it could have been done manually without using it. Add following dependencies to your project:

<dependency>
	<groupId>commons-beanutils</groupId>
	<artifactId>commons-beanutils</artifactId>
	<version>1.8.3</version>
</dependency>

<dependency>
	<groupId>commons-collections</groupId>
	<artifactId>commons-collections</artifactId>
	<version>3.2.1</version>
</dependency>

The final part is SortingMongoEventListener implementation:

package pl.maciejwalkowiak.springdata.mongodb;

import com.mongodb.DBObject;
import org.apache.commons.beanutils.BeanComparator;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.query.Order;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * MongoEventListener that intercepts object before its converted to BasicDBObject (before it is saved into MongoDB)
 * and after its loaded from MongoDB.
 *
 * @author Maciej Walkowiak
 */
public class SortingMongoEventListener extends AbstractMongoEventListener {
	@Override
	public void onAfterConvert(DBObject dbo, final Object source) {
		ReflectionUtils.doWithFields(source.getClass(), new SortingFieldCallback(source, SortPhase.AFTER_CONVERT));
	}

	@Override
	public void onBeforeConvert(Object source) {
		ReflectionUtils.doWithFields(source.getClass(), new SortingFieldCallback(source, SortPhase.BEFORE_CONVERT));
	}

	/**
	 * Performs sorting with field if:
	 * <ul>
	 * <li>field is an instance of list</li>
	 * <li>is annotated with OrderBy annotation</li>
	 * <li>OrderBy annotation is set to run in same phase as SortingFieldCallback</li>
	 * </ul>
	 */
	private static class SortingFieldCallback implements ReflectionUtils.FieldCallback {
		private Object source;
		private SortPhase phase;

		private SortingFieldCallback(Object source, SortPhase phase) {
			this.source = source;
			this.phase = phase;
		}

		public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
			if (field.isAnnotationPresent(OrderBy.class)) {
				OrderBy orderBy = field.getAnnotation(OrderBy.class);

				if (Arrays.asList(orderBy.phase()).contains(phase)) {
					ReflectionUtils.makeAccessible(field);
					Object fieldValue = field.get(source);

					sort(fieldValue, orderBy);
				}
			}
		}

		private void sort(Object fieldValue, OrderBy orderBy) {
			if (ClassUtils.isAssignable(List.class, fieldValue.getClass())) {
				final List list = (List) fieldValue;

				if (orderBy.order() == Order.ASCENDING) {
					Collections.sort(list, new BeanComparator(orderBy.value()));
				} else {
					Collections.sort(list, new BeanComparator(orderBy.value(), Collections.reverseOrder()));
				}
			}

		}
	}
}

In order to use it you just need to declare this class as a Spring bean in application context:

<bean class="pl.maciejwalkowiak.springdata.mongodb.SortingMongoEventListener" />

Example

Now its time to add created OrderBy annotation to Backpack class from beginning of this post. Lets say we want to order items by price in descending order:

@Document
public class Backpack {
	@Id
	private ObjectId id;

	@OrderBy(value = "price", order = Order.DESCENDING)
	private List<Item> items;

	...
}

Thats it. Now every time you load Backpack objects – does not matter if its findAll, findOne or your custom method – items in backpack will be ordered.

Summary

SortingMongoEventListener is another example how Spring Data MongoDB event system is powerful. You are welcome to comment and let me know if you think this feature could be a part of Spring Data MongoDB.

If you liked this post or you are interested in Spring Data MongoDB I encourage you to read my two other articles about this project:

If you enjoyed this post, then make sure you subscribe to my RSS feed

  • Oliver Gierke

    Yet another great one, Maciej! Would be cool to see a pull request for this as well. Some suggestions:

    1. Does it make sense to rename the enum values in SortPhase to ON_READ and ON_WRITE. These are not as close to the actual event phases but capture the essence of the two different approaches slightly better, I think.
    2. I’d vote to default to ON_WRITE then as most people will usually read data more often than they write and this would move the cost of the reflection operation to the side less frequently invoked.
    3. Might be an idea to also allow defining a Class attribute at the annotation to allow a manual implementation of Comparator to avoid the reflection overhead introduced by the BeanComparator for performance critical scenarios.

    Keep the ideas coming! :)

    • http://maciejwalkowiak.pl/ Maciej Walkowiak

      Thanks Oliver. As soon as I find some free time I will finish pull request with cascading (it’s tricky to apply changes that fullfil your suggestions) I will also put one with @OrderBy