Статьи

Агрегирование данных с помощью Spring Data MongoDB и Spring Boot

Платформа агрегации MongoDB предназначена для группировки документов и их преобразования в агрегированный результат. Запрос агрегации состоит в определении нескольких этапов, которые будут выполняться в конвейере. Если вы заинтересованы в более подробной информации о структуре, то
Документация mongodb — это хорошая отправная точка.

Цель этого поста — написать веб-приложение для запросов к mongodb, чтобы получить агрегированные результаты из базы данных. Мы сделаем это очень просто благодаря Spring Boot и Spring Data. На самом деле это приложение очень быстро внедряется, поскольку Spring Boot позаботится обо всех необходимых настройках, а Spring Data поможет нам настроить репозитории.

Исходный код можно найти в моем репозитории Github .

1 Приложение

Прежде чем перейти к коду, давайте посмотрим, что мы хотим сделать с нашим приложением.

Наш домен представляет собой набор продуктов, которые мы распределили по нескольким складам:

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
@Document
public class Product {
     
    @Id
    private final String id;
    private final String warehouse;
    private final float price;
     
    public Product(String id, String warehouse, float price) {
        this.id = id;
        this.warehouse = warehouse;
        this.price = price;
    }
     
    public String getId() {
        return id;
    }
     
    public String getWarehouse() {
        return warehouse;
    }
     
    public float getPrice() {
        return price;
    }
}

Наша цель — собрать все продукты в ценовом диапазоне, сгруппированные по складу и собрать общий доход и среднюю цену каждой группы.

В этом примере на наших складах хранятся следующие товары:

1
2
3
4
5
6
7
8
new Product("NW1", "Norwich", 3.0f);
new Product("LN1", "London", 25.0f);
new Product("LN2", "London", 35.0f);
new Product("LV1", "Liverpool", 15.2f);
new Product("MN1", "Manchester", 45.5f);
new Product("LV2", "Liverpool", 23.9f);
new Product("LN3", "London", 55.5f);
new Product("LD1", "Leeds", 87.0f);

Приложение будет запрашивать товары по цене от 5,0 до 70,0. Необходимые шаги конвейера агрегации будут следующими:

aggregationPipeline (1)

В итоге мы получим агрегированные результаты, сгруппированные по складу. Каждая группа будет содержать список товаров каждого склада, среднюю цену товара и общую выручку, которая фактически является суммой цен.

2 зависимости Maven

Как видите, у нас есть короткий pom.xml с зависимостями Spring Boot:

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
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.3.RELEASE</version>
    <relativePath/>
</parent>
 
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
     
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Определяя spring-boot-starter-parent в качестве родительского pom, мы устанавливаем настройки Spring Boot по умолчанию. В основном он устанавливает версии набора библиотек, которые он может использовать, например Spring или Apache Commons. Например, Spring Boot 1.3.3, который мы используем, устанавливает 4.2.5.RELEASE в качестве версии среды Spring. Как указано в предыдущих статьях, он не добавляет библиотеки в наше приложение, он только устанавливает версии.

После определения родителя нам нужно добавить только три зависимости:

  • spring-boot-starter-web: в основном включает библиотеки Spring MVC и встроенный сервер Tomcat.
  • spring-boot-starter-test: включает в себя библиотеки тестирования, такие как JUnit, Mockito, Hamcrest и Spring Test.
  • spring-boot-starter-data-mongodb: эта зависимость включает Java-драйвер MongoDB и библиотеки Spring Data Mongo.

3 Настройка приложения

Благодаря Spring Boot, настройка приложения так же проста, как установка зависимостей:

1
2
3
4
5
6
7
@SpringBootApplication
public class AggregationApplication {
     
    public static void main(String[] args) {
        SpringApplication.run(AggregationApplication.class, args);
    }
}

При запуске основного метода мы запустим наше веб-приложение, прослушивающее порт 8080.

4 хранилище

Теперь, когда у нас правильно настроено приложение, мы реализуем репозиторий. Это не сложно, так как Spring Data позаботится обо всей проводке.

1
2
3
4
@Repository
public interface ProductRepository extends MongoRepository<Product, String> {
     
}

Следующий тест подтверждает, что наше приложение правильно настроено.

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
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AggregationApplication.class)
@WebAppConfiguration
public class AggregationApplicationTests {
     
    @Autowired
    private ProductRepository productRepository;
     
    @Before
    public void setUp() {
        productRepository.deleteAll();
    }
     
    @Test
    public void contextLoads() {
    }
     
    @Test
    public void findById() {
        Product product = new Product("LN1", "London", 5.0f);
        productRepository.save(product);
         
        Product foundProduct = productRepository.findOne("LN1");
         
        assertNotNull(foundProduct);
    }
}

Мы не реализовали методы save и findOne. Они уже определены, поскольку наш репозиторий расширяет MongoRepository.

5 запрос агрегации

Наконец, мы настроили приложение и объяснили все шаги. Теперь мы можем сосредоточиться на запросе агрегации.

Поскольку наш запрос агрегации не является базовым запросом, нам необходимо реализовать собственный репозиторий. Шаги:

Создайте собственный репозиторий с помощью метода, который нам нужен:

1
2
3
4
public interface ProductRepositoryCustom {
     
    List<WarehouseSummary> aggregate(float minPrice, float maxPrice);
}

Измените первый репозиторий, чтобы также расширить наш пользовательский репозиторий:

1
2
3
4
@Repository
public interface ProductRepository extends MongoRepository<Product, String>, ProductRepositoryCustom {
     
}

Создайте реализацию, чтобы написать запрос агрегации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class ProductRepositoryImpl implements ProductRepositoryCustom {
     
    private final MongoTemplate mongoTemplate;
     
    @Autowired
    public ProductRepositoryImpl(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }
     
    @Override
    public List<WarehouseSummary> aggregate(float minPrice, float maxPrice) {
        ...
    }
}

Теперь мы собираемся реализовать этапы трубопровода mongodb, как описано в начале поста.

Наша первая операция — операция сопоставления . Мы отфильтруем все товарные документы, которые находятся за пределами нашего ценового диапазона:

1
2
3
4
private MatchOperation getMatchOperation(float minPrice, float maxPrice) {
    Criteria priceCriteria = where("price").gt(minPrice).andOperator(where("price").lt(maxPrice));
    return match(priceCriteria);
}

Следующим этапом конвейера является групповая операция. Помимо группировки документов по складу, на этом этапе мы также делаем следующие расчеты:

  • last: возвращает склад последнего документа в группе.
  • addToSet: собирает все уникальные идентификаторы продуктов всех сгруппированных документов, в результате чего получается массив.
  • avg: Рассчитывает среднее значение всех цен в группе.
  • сумма: Суммирует все цены в группе.
1
2
3
4
5
6
7
private GroupOperation getGroupOperation() {
    return group("warehouse")
        .last("warehouse").as("warehouse")
        .addToSet("id").as("productIds")
        .avg("price").as("averagePrice")
        .sum("price").as("totalRevenue");
}

Последним этапом трубопровода является эксплуатация проекта . Здесь мы указываем результирующие поля агрегации:

1
2
3
4
private ProjectionOperation getProjectOperation() {
    return project("productIds", "averagePrice", "totalRevenue")
        .and("warehouse").previousOperation();
}

Запрос строится следующим образом:

01
02
03
04
05
06
07
08
09
10
11
public List<WarehouseSummary> aggregate(float minPrice, float maxPrice) {
    MatchOperation matchOperation = getMatchOperation(minPrice, maxPrice);
    GroupOperation groupOperation = getGroupOperation();
    ProjectionOperation projectionOperation = getProjectOperation();
     
    return mongoTemplate.aggregate(Aggregation.newAggregation(
        matchOperation,
        groupOperation,
        projectionOperation
    ), Product.class, WarehouseSummary.class).getMappedResults();
}

В агрегатном методе мы указываем входной класс, который является нашим документом Product. Следующий аргумент — это выходной класс, который представляет собой DTO для хранения результирующей агрегации:

1
2
3
4
5
public class WarehouseSummary {
    private String warehouse;
    private List<String> productIds;
    private float averagePrice;
    private float totalRevenue;

Мы должны закончить пост тестом, доказывающим, что результаты — это то, чего мы ожидаем:

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
@Test
public void aggregateProducts() {
    saveProducts();
     
    List<WarehouseSummary> warehouseSummaries = productRepository.aggregate(5.0f, 70.0f);
     
    assertEquals(3, warehouseSummaries.size());
    WarehouseSummary liverpoolProducts = getLiverpoolProducts(warehouseSummaries);
    assertEquals(39.1, liverpoolProducts.getTotalRevenue(), 0.01);
    assertEquals(19.55, liverpoolProducts.getAveragePrice(), 0.01);
}
 
private void saveProducts() {
    productRepository.save(new Product("NW1", "Norwich", 3.0f));
    productRepository.save(new Product("LN1", "London", 25.0f));
    productRepository.save(new Product("LN2", "London", 35.0f));
    productRepository.save(new Product("LV1", "Liverpool", 15.2f));
    productRepository.save(new Product("MN1", "Manchester", 45.5f));
    productRepository.save(new Product("LV2", "Liverpool", 23.9f));
    productRepository.save(new Product("LN3", "London", 55.5f));
    productRepository.save(new Product("LD1", "Leeds", 87.0f));
}
 
private WarehouseSummary getLiverpoolProducts(List<WarehouseSummary> warehouseSummaries) {
    return warehouseSummaries.stream().filter(product -> "Liverpool".equals(product.getWarehouse())).findAny().get();
}

6. Заключение

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

Я публикую свои новые сообщения в Google Plus и Twitter. Следуйте за мной, если вы хотите быть в курсе нового контента.