liveBook · Manning

Composite key

As explained in Java Persistence with Spring Data and Hibernate epub

A key formed from multiple attributes or columns that jointly identify a persistent entity or relational row.

A composite key uses more than one column to identify a row. Each column may repeat, but the combination must be unique. In an IMAGE table, (ITEM_ID, FILENAME) identifies an image attached to an item: the same filename cannot be attached twice to that item. section 8.1.2 section 8.1.4

The key follows the relationship being stored:

  • For a Set of image filenames, ITEM_ID and FILENAME form the collection table’s primary key. The table stores no image order. section 8.1.4
  • For a Map keyed by an embeddable Filename, ITEM_ID and NAME form the primary key. NAME cannot be null; Filename also needs value-based equals() and hashCode() for Java map-key uniqueness. section 8.2.5
  • For a many-to-many association, CATEGORY_ID and ITEM_ID identify a link. The pair prevents duplicate links while allowing either entity to participate in other links. section 9.3.1

A link with its own identifier

When a link must also record who created it and when, map the link table as an entity. Its embeddable identifier holds categoryId and itemId; an @EmbeddedId maps that pair to CATEGORY_ID and ITEM_ID. Unlike a collection element, each link entity is saved or deleted independently, though cascading persistence can reduce save calls. section 9.3.2

Listing 9.4 Mapping a many-to-many relationship with CategorizedItem
Path: manytomany-linkentity/src/main/java/com/manning/javapersistence
 /ch09/manytomany/linkentity/CategorizedItem.java
 
@Entity
@Table(name = "CATEGORY_ITEM")
@org.hibernate.annotations.Immutable                        #1
public class CategorizedItem {
    @Embeddable
    public static class Id implements Serializable {        #2
        @Column(name = "CATEGORY_ID")
        private Long categoryId;
        @Column(name = "ITEM_ID")
        private Long itemId;
 
        public Id() {
        }
 
        public Id(Long categoryId, Long itemId) {
            this.categoryId = categoryId;
            this.itemId = itemId;
        }
        //implementing equals and hashCode
    }
    @EmbeddedId                                             #3
    private Id id = new Id();
    @Column(updatable = false)
    @NotNull
    private String addedBy;                                 #4
    @Column(updatable = false)
    @NotNull
    @CreationTimestamp
    private LocalDateTime addedOn;                          #5
    @ManyToOne
    @JoinColumn(
        name = "CATEGORY_ID",
        insertable = false, updatable = false)
    private Category category;                              #6
    @ManyToOne
    @JoinColumn(
        name = "ITEM_ID",
        insertable = false, updatable = false)
    private Item item;                                      #7
    public CategorizedItem(
        String addedByUsername,                             #8
        Category category,
        Item item) {
        this.addedBy = addedByUsername;                     #9
        this.category = category;
        this.item = item;
        this.id.categoryId = category.getId();              #10
        this.id.itemId = item.getId();
        category.addCategorizedItem(this);                  #9
        item.addCategorizedItem(this);
    }
    //  . . .
}
#1 The class is immutable, as annotated with <kbd>@org.hibernate.annotations .Immutable</kbd>.
#2 An entity class needs an identifier property. The primary key of the link table is the composite of <kbd>CATEGORY_ID</kbd> and <kbd>ITEM_ID</kbd>. We can externalize this <kbd>Id</kbd> class into its own file, of course.
#3 The new <kbd>@EmbeddedId</kbd> annotation maps the identifier property and its composite key columns to the entity’s table.
#4 The basic property mapping the <kbd>addedBy</kbd> username to a column of the join table.
#5 The basic property mapping the <kbd>addedOn</kbd> timestamp to a column of the join table. This is the “additional information about the link” that interests us.
#6 The <kbd>@ManyToOne</kbd> property <kbd>category</kbd> is already mapped in the identifier.
#7 The <kbd>@ManyToOne</kbd> property <kbd>item</kbd> is already mapped in the identifier. The trick here is to make them read-only, with the <kbd>updatable=false</kbd>, <kbd>insertable=false</kbd> settings. This means Hibernate or Spring Data JPA using Hibernate writes the values of these columns by taking the identifier value of <kbd>CategorizedItem</kbd>. At the same time, we can read and browse the associated instances through <kbd>categorizedItem .getItem()</kbd> and <kbd>getCategory()</kbd>. (If we map the same column twice without making one mapping read-only, Hibernate or Spring Data JPA using Hibernate will complain on startup about a duplicate column mapping.)
#8 We can also see that constructing a <kbd>CategorizedItem</kbd> involves setting the values of the identifier. The application always assigns composite key values; Hibernate doesn’t generate them.
#9 The constructor sets the <kbd>addedBy</kbd> field value and guarantees referential integrity by managing collections on both sides of the association.
#10 The constructor sets the <kbd>categoryId</kbd> field value. We’ll map these collections next to enable bidirectional navigation. This is a unidirectional mapping and enough to support the many-to-many relationship between <kbd>Category</kbd> and <kbd>Item</kbd>. To create a link, we instantiate and persist a <kbd>CategorizedItem</kbd>. If we want to break a link, we remove the <kbd>CategorizedItem</kbd>. The constructor of <kbd>CategorizedItem</kbd> requires that we provide already persistent <kbd>Category</kbd> and <kbd>Item</kbd> instances.

The mapping shows the two identifier fields inside an @Embeddable Id class and the entity’s @EmbeddedId property. section 9.3.2

Check nullability when generating schemas. Hibernate includes an embeddable collection element’s non-nullable columns in its composite primary key. Use @Column(nullable=false) for a property that must join that key; @NotNull alone may validate the object without generating the required database constraint. For a ternary link, this distinction can keep USER_ID and ADDEDON outside the intended (CATEGORY_ID, ITEM_ID) key, but their missing SQL NOT NULL constraints must be fixed manually. An embeddable link has a simpler collection-managed lifecycle, at the cost of bidirectional navigation. section 8.2.1 section 9.3.3

How Java Persistence with Spring Data and Hibernate epub covers it

How deep it goes
Works through collection-table keys, map-key constraints, and an @EmbeddedId link entity, including nullability and lifecycle consequences.
Examples
Uses image filenames keyed by item and category–item link tables, including a link entity that records creator and timestamp.
What it assumes
Defines column-combination uniqueness from zero, then assumes familiarity with Java collections and JPA entity and association mappings.
What you can do after
Build it from scratch with JPA collection mappings or @EmbeddedId, and check whether generated keys and constraints match the intended relationship.
How it connects
Connects composite keys to Set and Map equality, embeddables, many-to-many associations, and database nullability constraints.

Also explained in

Compare how 8 books explain composite key →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Java Persistence with Spring Data and Hibernate epub. The section links point to the parts of the book it draws on.