diff --git a/mongodb/aggregation/README.md b/mongodb/aggregation/README.md new file mode 100644 index 00000000..b1ecb016 --- /dev/null +++ b/mongodb/aggregation/README.md @@ -0,0 +1,53 @@ +# Spring Data MongoDB - Aggregations + +This project contains usage samples for using MongoDB [Aggregations](https://docs.mongodb.com/manual/aggregation/) +showing both the programmatic and declarative approach for integrating an Aggregation Pipeline into the repository lay. + +## Programmatic + +The programmatic approach uses a [custom repository](https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#repositories.custom-implementations) implementation along with the [Aggregation Framework](https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo.aggregation). + +```java +class OrderRepositoryImpl implements OrderRepositoryCustom { + + private MongoOperations operations; + + // ... + + @Override + public Invoice getInvoiceFor(Order order) { + + AggregationResults results = operations.aggregate(newAggregation(Order.class, + match(where("id").is(order.getId())), + unwind("items"), + project("id", "customerId", "items") + .andExpression("'$items.price' * '$items.quantity'").as("lineTotal"), + group("id") + .sum("lineTotal").as("netAmount") + .addToSet("items").as("items"), + project("id", "items", "netAmount") + .and("orderId").previousOperation() + .andExpression("netAmount * [0]", taxRate).as("taxAmount") + .andExpression("netAmount * (1 + [0])", taxRate).as("totalAmount") + ), Invoice.class); + + return results.getUniqueMappedResult(); + } +} +``` + +## Declarative + +The [declarative approach](https://docs.spring.io/spring-data/mongodb/docs/2.2.0.RC2/reference/html/#mongodb.repositories.queries.aggregation) allows to define an Aggregation Pipeline via the `@Aggregation` annotation. + +```java +public interface OrderRepository extends CrudRepository, OrderRepositoryCustom { + + @Aggregation("{ $group : { _id : $customerId, total : { $sum : 1 } } }") + List totalOrdersPerCustomer(Sort sort); + + @Aggregation(pipeline = { "{ $match : { customerId : ?0 } }", "{ $count : total }" }) + Long totalOrdersForCustomer(String customerId); +} +``` + diff --git a/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrderRepository.java b/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrderRepository.java index 5d8eb665..91a89506 100644 --- a/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrderRepository.java +++ b/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrderRepository.java @@ -15,6 +15,10 @@ */ package example.springdata.mongodb.aggregation; +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.repository.CrudRepository; /** @@ -22,7 +26,13 @@ import org.springframework.data.repository.CrudRepository; * * @author Thomas Darimont * @author Oliver Gierke + * @author Christoph Strobl */ public interface OrderRepository extends CrudRepository, OrderRepositoryCustom { + @Aggregation("{ $group : { _id : $customerId, total : { $sum : 1 } } }") + List totalOrdersPerCustomer(Sort sort); + + @Aggregation(pipeline = { "{ $match : { customerId : ?0 } }", "{ $count : total }" }) + Long totalOrdersForCustomer(String customerId); } diff --git a/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrdersPerCustomer.java b/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrdersPerCustomer.java new file mode 100644 index 00000000..2044d543 --- /dev/null +++ b/mongodb/aggregation/src/main/java/example/springdata/mongodb/aggregation/OrdersPerCustomer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.mongodb.aggregation; + +import lombok.Value; + +import org.springframework.data.annotation.Id; + +/** + * @author Christoph Strobl + */ +@Value +public class OrdersPerCustomer { + + @Id // + private String customerId; + private Long total; +} diff --git a/mongodb/aggregation/src/test/java/example/springdata/mongodb/aggregation/OrderRepositoryIntegrationTests.java b/mongodb/aggregation/src/test/java/example/springdata/mongodb/aggregation/OrderRepositoryIntegrationTests.java index e5346967..5f2c300d 100644 --- a/mongodb/aggregation/src/test/java/example/springdata/mongodb/aggregation/OrderRepositoryIntegrationTests.java +++ b/mongodb/aggregation/src/test/java/example/springdata/mongodb/aggregation/OrderRepositoryIntegrationTests.java @@ -21,11 +21,13 @@ import static org.junit.Assert.*; import java.util.Date; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Sort; import org.springframework.test.context.junit4.SpringRunner; /** @@ -33,6 +35,7 @@ import org.springframework.test.context.junit4.SpringRunner; * * @author Thomas Darimont * @author Oliver Gierke + * @author Christoph Strobl */ @RunWith(SpringRunner.class) @SpringBootTest @@ -64,4 +67,33 @@ public class OrderRepositoryIntegrationTests { assertThat(invoice.getTaxAmount(), is(closeTo(1.577, 000001))); assertThat(invoice.getTotalAmount(), is(closeTo(9.877, 000001))); } + + @Test + public void declarativeAggregationWithSort() { + + repository.save(new Order("c42", new Date()).addItem(product1)); + repository.save(new Order("c42", new Date()).addItem(product2)); + repository.save(new Order("c42", new Date()).addItem(product3)); + + repository.save(new Order("b12", new Date()).addItem(product1)); + repository.save(new Order("b12", new Date()).addItem(product1)); + + Assertions.assertThat(repository.totalOrdersPerCustomer(Sort.by(Sort.Order.desc("total")))) // + .containsExactly( // + new OrdersPerCustomer("c42", 3L), new OrdersPerCustomer("b12", 2L) // + ); + } + + @Test + public void multiStageDeclarativeAggregation() { + + repository.save(new Order("c42", new Date()).addItem(product1)); + repository.save(new Order("c42", new Date()).addItem(product2)); + repository.save(new Order("c42", new Date()).addItem(product3)); + + repository.save(new Order("b12", new Date()).addItem(product1)); + repository.save(new Order("b12", new Date()).addItem(product1)); + + Assertions.assertThat(repository.totalOrdersForCustomer("c42")).isEqualTo(3); + } }