1. 引言

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

2. 复合键是什么,何时使用

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

通常,没有一个单一的属性能够唯一标识一个文档。在这种情况下,我们可以让它空缺,而MongoDB会为“_id”属性自动生成一个唯一的值。另一种选择是选择多个属性,当它们组合在一起时,就能达到这个目的。在这种情况下,我们需要为我们的ID属性创建一个自定义类来容纳所有这些属性。让我们看看它是如何工作的。

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

@Id注解可以用于注解自定义类型的属性,从而完全控制其生成。我们的ID类仅需覆盖equals()hashCode(),并具有默认的无参构造函数即可。

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

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

    // getters and setters

    // override hashCode() and equals()
}

由于默认的无参构造函数是隐式的,我们不需要写它。此外,为了使示例更简单,我们将使用String日期。接下来,让我们创建Ticket类,并在TicketId属性上添加@Id注解:

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

    private String event;

    // getters and setters
}

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

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

3.1. 测试我们的模型

因此,尝试插入具有相同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);
    });
}

这确保了我们的键正常工作。

3.2. 通过ID查找

由于我们在repository中定义了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);
}

当我们想要完全控制我们的ID属性时,应该使用这种方法。同样,这也会确保我们ID对象中的属性不可修改。缺点是,我们失去了MongoDB生成的ID,这可能不太易读。但在链接等场景中,使用起来更方便。

4. 注意事项

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

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

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

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

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

5. 总结

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

如往常一样,源代码可在GitHub上找到。