如何使用 mongo 搜索集合和 return 子文档列表 (Spring-data-mongo)

How to search a collection and return a list of sub document with mongo (Sping-data-mongo)

鉴于此文档集(工作流):

[
{ 
 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'}
 ]
}
]

我已经有一个搜索功能,return 我有一个工作流列表(页面),该列表(页面)匹配使用查询和 mongoTemplate.find();

的一系列条件

我需要做的是将这个结果转换成这样: (假设查询 return 所有元素

[

 { 
 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
]

换句话说,我想 return 我的工作流的扁平化版本,每个工作流只有 1 个任务。尽可能分页!!

我可以使用的另一个版本是 return 带有聚合工作流对象(父级)的任务列表到添加的字段中,例如:

[
 {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-数据聚合请求中的内容...我很难处理群组部分。谢谢

所以我会尝试使用示例代码来回答。我正在使用 SpringTemplates 而不是 SpringRepositories。虽然存储库可以进行聚合,但对于模板具有更多控制权的大多数企业应用程序而言,它们从根本上来说过于基础。在我看来,我只会使用模板而不会使用存储库 - 但这只是我的意见。

请记住 - SpringData 想要将 POJO 映射到 MongoDB 集合中的数据。查询的响应很容易,因为两者相互同步——POJO 与数据库中找到的预期结构相匹配。执行聚合时,结果通常会因各种原因而改变形状。

在您的用例中,您似乎想要展开字段“任务”并且每个更高级别的父对象只有一个任务。这意味着父字段将重复 - 很像原始 post 中显示的预期输出。执行展开时,数组不再存在,但有一个文档取而代之。由于这个原因,输出的形状略有不同。对于 Spring,这意味着不同的 class(继承在这里可以提供帮助)。出于这个原因,在我的示例代码中,我有两个 POJO - 一个名为 Workflow 代表原始保存的文档形状,包括字段 tasks 的数组,另一个名为 Workflow2 的 POJO 代表重塑聚合结果。唯一的区别是字段 tasks。一个有一个 List<Task> 而另一个有一个 Task 子对象。

所以,实际上我有 3 个 POJO:

  • 工作流程
  • 工作流 2
  • 任务

任务是 class 定义字段 task 中的子文档。无论它是否是一个数组 - 它仍然需要一个 class 来保存两个子文档字段 taskIdvalue.

我正在使用 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=replSet&w=majority&readConcernLevel=majority&readPreference=primary&authSource=admin&retryWrites=true&maxPoolSize=10&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();

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();