Persistence top

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE

1. Introduction

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.

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

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.

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

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.

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:

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:

@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:

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

3.1. Testing Our Model

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

@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

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:

@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.

4. Caveat

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": {
    "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:

{
  "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.

5. Conclusion

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.

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

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
guest
0 Comments
Inline Feedbacks
View all comments