Статьи

Сортировка коллекций Spring Data MongoDB с использованием @OrderBy

Это уже третий пост о настройке и расширении возможностей Spring Data MongoDB . На этот раз я обнаружил, что мне не хватает одной функции JPA — аннотации @OrderBy . @OrderBy указывает порядок элементов ассоциации с ценностью коллекции в точке, когда эта ассоциация извлекается.

В этой статье я покажу, как реализовать сортировку с аннотацией @OrderBy с Spring Data MongoDB .  

Случай использования

Просто краткий пример того, что это такое для тех, кто раньше не использовал JPA @OrderBy. У нас есть два класса и отношение один ко многим:

01
02
03
04
05
06
07
08
09
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;
 
 ...
}

Рюкзак здесь является основным классом и содержит список встроенных предметов. Когда рюкзак загружается из базы данных, его элементы загружаются в порядке, близком к порядку вставки. Что если мы хотим изменить это и упорядочить элементы по одному из его полей? Нам нужно реализовать сортировку самостоятельно, и мы снова расширим AbstractMongoEventListener .

Сортировка деталей: введение @OrderBy

В отличие от JPA — сортировка в этом случае не может быть выполнена на уровне базы данных. Мы должны позаботиться об этом на стороне приложения — это можно сделать в двух местах:

  • перед преобразованием объекта в структуру данных MongoDB — если мы хотим убедиться, что объекты правильно отсортированы в коллекции MongoDB
  • после того, как объект преобразован из структуры данных MongoDB в объект Java — если мы просто хотим убедиться, что внутри нашего приложения список отсортирован правильно

Чтобы указать, в каком месте должна происходить сортировка, я создал перечисление SortPhase:

1
2
3
4
public enum SortPhase {
 AFTER_CONVERT,
 BEFORE_CONVERT;
}

Наконец, аннотация @OrderBy будет содержать три почти самоописываемых свойства:

01
02
03
04
05
06
07
08
09
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;
}

Реализация SortingMongoEventListener

Декларативная сортировка должна использовать отражение. Чтобы код читался, я использовал commons-beanutils, но это можно было сделать вручную, не используя его. Добавьте следующие зависимости в ваш проект:

01
02
03
04
05
06
07
08
09
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>

Последняя часть — реализация SortingMongoEventListener :

01
02
03
04
05
06
07
08
09
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
75
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()));
    }
   }
 
  }
 }
}

Чтобы использовать его, вам просто нужно объявить этот класс как bean-компонент Spring в контексте приложения:

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

пример

Теперь пришло время добавить созданную аннотацию OrderBy в класс Backpack с начала этого поста. Допустим, мы хотим упорядочить товары по цене в порядке убывания:

01
02
03
04
05
06
07
08
09
10
@Document
public class Backpack {
 @Id
 private ObjectId id;
 
 @OrderBy(value = "price", order = Order.DESCENDING)
 private List<Item> items;
 
 ...
}

Это оно. Теперь каждый раз, когда вы загружаете объекты рюкзака — не имеет значения, если его findAll, findOne или ваш собственный метод — элементы в рюкзаке будут упорядочены.  

Резюме

SortingMongoEventListener — еще один пример мощной системы событий Spring Data MongoDB. Пожалуйста, прокомментируйте и дайте мне знать, если вы считаете, что эта функция может быть частью Spring Data MongoDB.

Ссылка: Сортировка коллекций Spring Data MongoDB с использованием @OrderBy от нашего партнера по JCG Мачея Уоковяка в блоге « Путь к разработке программного обеспечения» .