Software Develoment Journey
Java, Spring Framework, How-to

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

  ...
}
1
2
3
4
5
6
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:
1
2
3
4
public enum SortPhase {
  AFTER_CONVERT,
  BEFORE_CONVERT;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1
2
3
4
5
6
7
8
9
10
11
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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:
  * - field is an instance of list
  * - is annotated with OrderBy annotation
  *
  * OrderBy annotation is set to run in same phase as SortingFieldCallback
  */
  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:

1
<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:

1
2
3
4
5
6
7
8
9
10
@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