Как выполнить поиск в коллекции и вернуть список вложенных документов с помощью mongo (Sping-data-mongo)

#mongodb #spring-data-mongodb

#mongodb #spring-data-mongodb

Вопрос:

Учитывая эту коллекцию документов (рабочий процесс):

 [
{ 
 id: 1,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task1', value:'new'}
  {taskId: 'task2', value:'started'}
  {taskId: 'task3', value:'completed'}
 ]
},
{ 
 id: 2,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task1', value:'new'}
  {taskId: 'task2', value:'started'}
  {taskId: 'task3', value:'completed'}
 ]
},
{ 
 id: 3,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task1', value:'new'}
  {taskId: 'task2', value:'started'}
  {taskId: 'task3', value:'completed'}
 ]
}
]
 

У меня уже есть функция поиска, которая возвращает мне список (страницу) рабочих процессов, соответствующих набору критериев, используя Query и MongoTemplate.find();

Что мне нужно сделать, так это преобразовать этот результат во что-то вроде этого: (давайте представим, что запрос возвращает все элементы

 [

 { 
 id: 1,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task1', value:'new'}
 ]
},
 { 
 id: 1,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task2', value:'started'}
 ]
},
 { 
 id: 1,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task3', value:'completed'}
 ]
},
{ 
 id: 2,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task1', value:'new'}
 ]
},
{ 
 id: 2,
 name: 'workflow',
 status: 'started',
 createdDate: '2021-02-10'
 tasks: [
  {taskId: 'task2', value:'started'}
 ]
},
.... etc
]
 

Другими словами, я хотел бы вернуть упрощенную версию моих рабочих процессов только с 1 задачей на рабочий процесс. Постраничный, если это возможно!!

другой версией, с которой я мог бы работать, было бы вернуть список задач с агрегированным объектом рабочего процесса (родительским) в добавленное поле, например:

 [
 {taskId: 'task1', value:'new', workflow: {the workflow object}},
 {taskId: 'task2', value:'started', workflow: {the workflow object}},
]
 

Я немного поиграл с агрегацией и размоткой и т. Д., Но я новичок в mongodb, и я не нахожу примеров, которые мне помогают.

Заранее спасибо!

Обновить:

На основе ответов здесь и других. Я придумал этот запрос, который работает и делает именно то, что я хочу. :

 db.Workflow.aggregate([
  {
    $match: {}
  },
  {
    $unwind: "$tasks"
  },
  {
    $facet: {
      data: [
        {
          $skip: 0
        },
        {
          $limit: 30
        },
        
      ],
      count: [
        {
          $group: {
            _id: null,
            count: {
              $sum: 1
            }
          }
        },
        
      ],
      
    }
  }
])
 

Итак, если кто-нибудь может помочь мне перевести это в spring-запрос на агрегацию данных … мне трудно с разделом группы. Спасибо

Ответ №1:

Агрегация MongoDB — это то, что вам нужно:

 db.Workflow.aggregate([
  {
    $match: {} // put here your search criteria
  },
  {
    $unwind: "$tasks"
  },
  {
    $addFields: {
      tasks: [
        "$tasks"
      ]
    }
  },
  //pageable
  {
    $skip: 0
  },
  {
    $limit: 100
  }
])
 

MongoPlayground

Способ SpringBoot:

 @Autowired
private MongoTemplate mongoTemplate;

...

List<AggregationOperation> pipeline = new ArrayList<>();

//$match (put here your filter)
pipeline.add(Aggregation.match(Criteria.where("status").is("started")));

//$unwind
pipeline.add(Aggregation.unwind("tasks"));

//$addFields
pipeline.add(Aggregation.addFields().addFieldWithValue("tasks", Arrays.asList("$tasks")).build());

//$skip
pipeline.add(Aggregation.skip(0L));
    
//$limit
pipeline.add(Aggregation.limit(100L));

Aggregation agg = Aggregation.newAggregation(pipeline)
    .withOptions(Aggregation
        .newAggregationOptions().allowDiskUse(Boolean.TRUE).build());

return mongoTemplate.aggregate(agg, Workflow.class, Workflow.class).getMappedResults();
 

Комментарии:

1. Очень приятно. MongoPlayground теперь в закладках!! ;-). очень полезно. Я добавлю дополнительный вопрос к своему вопросу. Если у вас есть идея, как преобразовать мой запрос в spring-data!! Заранее спасибо

Ответ №2:

Поэтому я постараюсь ответить, используя пример кода. Я использую SpringTemplates, а не SpringRepositories. Хотя репозитории могут выполнять агрегацию, они в основном слишком просты для большинства корпоративных приложений, где шаблоны имеют гораздо больший контроль. На мой взгляд, я буду использовать только шаблоны и никогда не буду использовать репозитории — но это только мое мнение.

Имейте в виду — SpringData хочет сопоставить POJO с данными в коллекции MongoDB. Ответ на запрос прост, потому что они синхронизированы друг с другом — POJO соответствует ожидаемым структурам, найденным в базе данных. При выполнении агрегирования результаты часто изменяются по целому ряду причин.

В вашем случае использования кажется, что вы хотите развернуть поле «задачи» и иметь только одну задачу на родительский объект более высокого уровня. Это означает, что родительские поля будут повторяться — так же, как ваш ожидаемый результат, показанный в вашем исходном сообщении. При выполнении unwind массив больше не существует, но на его месте находится один документ. По этой причине выходные данные имеют несколько иную форму. Для Spring это означает другой класс (здесь может помочь наследование). По этой причине в моем примере кода у меня есть два POJO — один вызывается Workflow , который представляет исходные сохраненные формы документа, включая массив для поля tasks , а другой вызывается POJO Workflow2 , который представляет измененные результаты агрегирования. Единственное отличие — это поле tasks . У одного есть a List<Task> , тогда как у другого есть Task вложенный объект.

Итак, на самом деле у меня есть 3 POJO:

  • Рабочий процесс
  • Workflow2
  • Задача

Task — это класс для определения вложенных документов в поле task . Является ли это массивом или нет — ему все равно нужен класс для хранения двух полей вложенного документа taskId и value .

Я использую maven для управления зависимостями. Для дополнительной ясности я полностью квалифицирую каждый объект без операторов импорта.

Итак, без дальнейших церемоний вот код.

Файл pom.xml

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>test.barry</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <start-class>test.barry.Main</start-class>
        <mongodb.version>4.3.4</mongodb.version> <!-- BARRY NOTE: FORCE SPRING-BOOT TO USE THE MONGODB DRIVER VERSION 4.4.0 INSTEAD OF 4.0.5 -->
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
            <version>4.3.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
    </dependencies>
</project>
 

Файл src/main/resources/application.properties

 spring.data.mongodb.uri=mongodb://testuser:mysecret@localhost:50011,localhost:50012,localhost:50013/?replicaSet=replSetamp;w=majorityamp;readConcernLevel=majorityamp;readPreference=primaryamp;authSource=adminamp;retryWrites=trueamp;maxPoolSize=10amp;waitQueueTimeoutMS=1000
spring.data.mongodb.database=javaspringtestX
spring.data.mongodb.socketconnecttimeout=60
 

Файл src/main/java/test.barry/Main.java

 package test.barry;

@org.springframework.boot.autoconfigure.SpringBootApplication
public class Main {
    public static void main(String[] args) {
        org.springframework.boot.SpringApplication.run(Main.class, args);
    }
}
 

Файл src/main/java/test.barry/MySpringBootApplication.java

 package test.barry;

@org.springframework.boot.autoconfigure.SpringBootApplication
public class MySpringBootApplication implements org.springframework.boot.CommandLineRunner {

  @org.springframework.beans.factory.annotation.Autowired
  org.springframework.data.mongodb.core.MongoTemplate mongoTemplate;

  public static void main(String[] args) {
    org.springframework.boot.SpringApplication.run(org.springframework.boot.autoconfigure.SpringBootApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {

    System.out.println("Drop collections for automatic cleanup during test:");
    System.out.println("-------------------------------");
    this.mongoTemplate.dropCollection(test.barry.models.Workflow.class);

    java.util.Calendar calendar = java.util.Calendar.getInstance();
    calendar.set(2021, 2, 10);

    test.barry.models.Workflow workflow1 = new test.barry.models.Workflow();
    workflow1.id = 1;
    workflow1.name  = "workflow";
    workflow1.status = "started";
    workflow1.createdDate = calendar.getTime();
    workflow1.tasks.add(new test.barry.models.Task ("task1", "new"));
    workflow1.tasks.add(new test.barry.models.Task ("task2", "started"));
    workflow1.tasks.add(new test.barry.models.Task ("task3", "completed"));

    this.mongoTemplate.save(workflow1);

    test.barry.models.Workflow workflow2 = new test.barry.models.Workflow();
    workflow2.id = 2;
    workflow2.name  = "workflow";
    workflow2.status = "started";
    workflow2.createdDate = calendar.getTime();
    workflow2.tasks.add(new test.barry.models.Task ("task1", "new"));
    workflow2.tasks.add(new test.barry.models.Task ("task2", "started"));
    workflow2.tasks.add(new test.barry.models.Task ("task3", "completed"));

    this.mongoTemplate.save(workflow2);

    test.barry.models.Workflow workflow3 = new test.barry.models.Workflow();
    workflow3.id = 3;
    workflow3.name  = "workflow";
    workflow3.status = "started";
    workflow3.createdDate = calendar.getTime();
    workflow3.tasks.add(new test.barry.models.Task ("task1", "new"));
    workflow3.tasks.add(new test.barry.models.Task ("task2", "started"));
    workflow3.tasks.add(new test.barry.models.Task ("task3", "completed"));

    this.mongoTemplate.save(workflow3);

    org.springframework.data.mongodb.core.aggregation.Aggregation pipeline = org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation (
            org.springframework.data.mongodb.core.aggregation.Aggregation.unwind("tasks")
    );

    org.springframework.data.mongodb.core.aggregation.AggregationResults<test.barry.models.Workflow2> aggregationResults = this.mongoTemplate.aggregate(pipeline, test.barry.models.Workflow.class, test.barry.models.Workflow2.class);
    java.util.List<test.barry.models.Workflow2> listResults = aggregationResults.getMappedResults();
    System.out.println(listResults.size());
  }
}
 

Файл src/main/java/test.barry/SpringConfiguration.java

 package test.barry;

@org.springframework.context.annotation.Configuration
@org.springframework.context.annotation.PropertySource("classpath:/application.properties")
public class SpringConfiguration {

    @org.springframework.beans.factory.annotation.Autowired
    org.springframework.core.env.Environment env;

    @org.springframework.context.annotation.Bean
     public com.mongodb.client.MongoClient mongoClient() {
         String uri = env.getProperty("spring.data.mongodb.uri");
         return com.mongodb.client.MongoClients.create(uri);
     }
    @org.springframework.context.annotation.Bean
    public org.springframework.data.mongodb.MongoDatabaseFactory mongoDatabaseFactory() {
        String uri = env.getProperty("spring.data.mongodb.uri");
        String database = env.getProperty("spring.data.mongodb.database");
        return new org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory(com.mongodb.client.MongoClients.create(uri), database);
    }

    @org.springframework.context.annotation.Bean
    public org.springframework.data.mongodb.core.MongoTemplate mongoTemplate() throws Exception {
        return new org.springframework.data.mongodb.core.MongoTemplate(mongoClient(), env.getProperty("spring.data.mongodb.database"));
    }
}
 

Файл src/main/java/test.barry/models/Workflow.java

 package test.barry.models;

@org.springframework.data.mongodb.core.mapping.Document(collection = "Workflow")
public class Workflow
{
    @org.springframework.data.annotation.Id
    public int id;

    public String name;
    public String status;
    public java.util.Date createdDate;
    public java.util.List<Task> tasks;

    public Workflow() {
        this.tasks = new java.util.ArrayList<Task>();
    }

    public Workflow(String name, String status, java.util.Date createdDate) {
        this();
        this.name = name;
        this.status = status;
        this.createdDate = createdDate;
    }

    @Override
    public String toString() {
        return String.format("Workflow[id=%s, name='%s', status='%s', createdDate='%s']", id, name, status, createdDate);
    }
}
 

Файл src/main/java/test.barry/models/Workflow2.java

 package test.barry.models;

@org.springframework.data.mongodb.core.mapping.Document(collection = "Workflow")
public class Workflow2
{
    @org.springframework.data.annotation.Id
    public int id;

    public String name;
    public String status;
    public java.util.Date createdDate;
    public Task tasks;

    public Workflow2() {
        this.tasks = new Task();
    }

    public Workflow2(String name, String status, java.util.Date createdDate) {
        this();
        this.name = name;
        this.status = status;
        this.createdDate = createdDate;
    }

    @Override
    public String toString() {
        return String.format("Workflow[id=%s, name='%s', status='%s', createdDate='%s']", id, name, status, createdDate);
    }
}
 

Файл src/main/java/test.barry/models/Task.java

 package test.barry.models;

public class Task
{
    public Task() {}

    public Task(String taskId, String value) {
        this.taskId = taskId;
        this.value = value;
    }

    public String taskId;
    public String value;
}
 

Заключение

При использовании MongoShell мы видим, что создаются следующие записи:

 Enterprise replSet [primary] javaspringtestX> db.Workflow.find()
[
  {
    _id: 1,
    name: 'workflow',
    status: 'started',
    createdDate: ISODate("2021-03-10T23:49:46.704Z"),
    tasks: [
      { taskId: 'task1', value: 'new' },
      { taskId: 'task2', value: 'started' },
      { taskId: 'task3', value: 'completed' }
    ],
    _class: 'test.barry.models.Workflow'
  },
  {
    _id: 2,
    name: 'workflow',
    status: 'started',
    createdDate: ISODate("2021-03-10T23:49:46.704Z"),
    tasks: [
      { taskId: 'task1', value: 'new' },
      { taskId: 'task2', value: 'started' },
      { taskId: 'task3', value: 'completed' }
    ],
    _class: 'test.barry.models.Workflow'
  },
  {
    _id: 3,
    name: 'workflow',
    status: 'started',
    createdDate: ISODate("2021-03-10T23:49:46.704Z"),
    tasks: [
      { taskId: 'task1', value: 'new' },
      { taskId: 'task2', value: 'started' },
      { taskId: 'task3', value: 'completed' }
    ],
    _class: 'test.barry.models.Workflow'
  }
]
 

Для просмотра результатов агрегирования мы должны использовать отладчик. Я использую IntelliJ IDEA для отладки и показываю результаты в виде списка типов Workflow2 . Не уверен, как их показать здесь. Мое тестирование показало, что это работает, насколько я понимаю. Пожалуйста, оцените и дайте мне знать, нуждается ли это в настройке…

Кстати, концепция разбивки на страницы лучше всего подходит для управления вашим приложением, а не базой данных. На практике вы можете обнаружить использование skip() и limit(), но для больших наборов данных, содержащих много страниц, вы можете обнаружить, что запросы на следующие страницы вызывают проблемы с производительностью, поскольку каждый раз они должны идентифицировать все документы, а затем определять, какие из них пропустить. Лучше отслеживать диапазон, показанный на предыдущей странице, а затем запрашивать только записи на следующей странице. Т.Е. Ограничить результирующий набор для повышения производительности.

РЕДАКТИРОВАТЬ — 2021-12-09 При просмотре сохраненных данных отображаются странные даты. По-видимому, устаревшее использование java.util.Date myDate = java.util.Date(2021, 2, 10); создает недопустимые даты. По этой причине я добавил java.util.Calendar calendar = java.util.Calendar.getInstance();

Комментарии:

1. Вау, спасибо за этот очень подробный ответ. Я попробую это завтра. Думаю, я был близок, поскольку ваш код агрегации — это почти то, что я пробовал. Еще раз спасибо!