MongoDB Composite Key With Spring Data – 使用Spring Data的MongoDB复合键

最后修改: 2022年 6月 18日

1. Introduction

1.绪论

In this tutorial, we’ll create composite keys in our Spring Data MongoDB application. We’ll learn about different strategies and how to configure them.

在本教程中,我们将在Spring Data MongoDB应用程序中创建复合键。我们将了解不同的策略以及如何配置它们。

2. What Is a Composite Key and When to Use It

2.什么是复合钥匙以及何时使用它

A composite key is a combination of properties in a document that uniquely identifies it. Using a composite primary key is not better or worse than using a single automatically generated property. We can even combine these approaches with unique indexes.

复合主键是一个文档中的属性组合,可以唯一地识别它。使用复合主键并不比使用单个自动生成的属性更好或更差。我们甚至可以将这些方法与唯一索引结合起来。

Often, there’s no single property that’s able to uniquely identify a document. In those cases, we can leave it blank and MongoDB will generate a unique value for its “_id” property. Alternatively, we can choose multiple properties that, when combined, serve that purpose. In that case, we have to create a custom class for our ID property to hold all these properties. Let’s see how this works.

通常,没有一个属性能够唯一地识别一个文档。在这些情况下,我们可以将其留空,然后MongoDB 将为其”_id“属性生成一个唯一值。另外,我们也可以选择多个属性,这些属性组合起来就可以达到这个目的。在这种情况下,我们必须为我们的 ID 属性创建一个自定义类,以容纳所有这些属性。让我们看看这是如何实现的。

3. Using the @Id Annotation to Create a Composite Key

3.使用@Id注解来创建一个复合键

The @Id annotation can be used to annotate a property of a custom type, giving full control of its generation. The only requirements for our ID classes are that we override equals() and hashCode() and have a default no-arg constructor.

@Id注解可用于注解自定义类型的属性,对其生成进行完全控制。我们对ID类的唯一要求是覆盖equals()hashCode(),并且有一个默认的no-arg构造函数

In our first example, we’ll create a document for event tickets. Its ID will be a combination of the venue and date properties. Let’s start with our ID class:

在我们的第一个例子中,我们将为活动门票创建一个文档。它的ID将是venuedate属性的组合。让我们从我们的ID类开始。

public class TicketId {
    private String venue;
    private String date;

    // getters and setters

    // override hashCode() and equals()
}

Since the no-arg constructor is implicit and we don’t need other constructors, we don’t need to write it. Also, we’ll use String dates to make examples simpler. Next, let’s create our Ticket class, and annotate our TicketId property with @Id:

由于无参数构造函数是隐式的,而且我们不需要其他构造函数,所以我们不需要写它。另外,我们将使用String日期来使例子更简单。接下来,让我们创建Ticket类,并用@Id注释我们的TicketId属性。

@Document
public class Ticket {
    @Id
    private TicketId id;

    private String event;

    // getters and setters
}

For our MongoRepository, we can specify our TicketId as the ID type, and that’s all the setup needed:

对于我们的MongoRepository,我们可以指定我们的TicketId作为ID类型,这就是所有需要的设置:

public interface TicketRepository extends MongoRepository<Ticket, TicketId> {
}

3.1. Testing Our Model

3.1.测试我们的模型

Consequently, trying to insert a ticket with the same ID twice, will throw a DuplicateKeyException. We can check this with a test:

因此,试图插入一个具有相同ID的票据两次,将抛出一个DuplicateKeyException我们可以通过一个测试来检查。

@Test
public void givenCompositeId_whenDupeInsert_thenExceptionIsThrown() {
    TicketId ticketId = new TicketId();
    ticketId.setDate("2020-01-01");
    ticketId.setVenue("V");
    
    Ticket ticket = new Ticket(ticketId, "Event C");
    service.insert(ticket);

    assertThrows(DuplicateKeyException.class, () -> {        
        service.insert(ticket);
    });
}

And this ensures our key is working.

而这就确保了我们的钥匙在工作。

3.2. Finding by ID

3.2.按身份证查找

Since we’re defining our TicketId as the ID class in our repository, we can still use the default findById() method. Let’s write a test to see it in action:

由于我们将TicketId定义为仓库中的ID类,我们仍然可以使用默认的findById()方法。让我们写一个测试来看看它的运行情况。

@Test
public void givenCompositeId_whenSearchingByIdObject_thenFound() {
    TicketId ticketId = new TicketId();
    ticketId.setDate("2020-01-01");
    ticketId.setVenue("Venue B");

    service.insert(new Ticket(ticketId, "Event B"));

    Optional<Ticket> optionalTicket = ticketRepository.findById(ticketId);

    assertThat(optionalTicket.isPresent());
    Ticket savedTicket = optionalTicket.get();

    assertEquals(savedTicket.getId(), ticketId);
}

We should use this approach when we want absolute control of our ID property. Similarly, this will make sure the properties in our ID object cannot be modified. One downside is that we lose the ID generated by MongoDB, which is less readable. But, easier to use in links, for example.

当我们想要绝对控制我们的 ID 属性时,我们应该使用这种方法。类似地,这将确保我们的ID对象中的属性不能被修改。一个缺点是,我们失去了由MongoDB生成的ID,它的可读性较差。但是,在链接中更容易使用,比如说。

4. Caveat

4.注意事项

When using a nested object as an ID, the order of the properties matter. This is usually not a problem when using our repository, as Java objects are always constructed in the same order. But, if we change the order of the fields in our TicketId class, we can insert another document with the same values. For instance, these objects are considered different:

当使用一个嵌套对象作为ID时,属性的顺序很重要。在使用我们的资源库时,这通常不是问题,因为Java对象总是以相同的顺序构建。但是,如果我们改变TicketId类中字段的顺序,我们可以插入另一个具有相同值的文件。例如,这些对象被认为是不同的。

{
  "id": {
    "venue":"Venue A",
    "date": "2023-05-27"
  },
  "event": "Event 1"
}

After that, if we change the field order in TicketId, we’ll be able to insert the same values. No exceptions will be thrown:

之后,如果我们改变TicketId中的字段顺序,我们就能插入相同的值。不会抛出任何异常:

{
  "id": {
    "date": "2023-05-27",
    "venue":"Venue A"
  },
  "event": "Event 1"
}

This doesn’t happen if, instead of an ID class, we use unique indexes on properties of our Ticket class. In other words, it only happens with nested objects.

如果我们不使用ID类,而是在Ticket类的属性上使用唯一索引,这种情况就不会发生。换句话说,这只发生在嵌套对象上。

5. Conclusion

5.总结

In this article, we saw the pros and cons of creating composite keys for our MongoDB documents. And the required configuration to implement them with a simple use case. But, we also learned about an important caveat to be aware of.

在这篇文章中,我们看到了为我们的MongoDB文档创建复合键的优点和缺点。以及用一个简单的用例来实现它们所需的配置。但是,我们也了解到了一个需要注意的重要事项。

And as always, the source code is available over on GitHub.

一如既往,源代码可在GitHub上获得。