Primary Key

Author      Ter-Petrosyan Hakob

In a relational database, a primary key is one column—or a set of columns—that uniquely identifies each row. A primary key must always have a value (no NULLs) and no two rows can share the same key. Examples include a customer ID, a phone number, an order reference, or an ISBN.

In JPA, every entity needs an identifier mapped to a primary key. This identifier can be a single field or a combination of fields (a composite key). Once you assign a primary key value to an entity, you should never change it.

A simple primary key in JPA means you use just one field in your entity class. You mark that field with the @Id annotation to tell JPA it is the unique identifier. Once set, this value must not change.

Allowed types for a simple primary key include:

Use one of these types for your @Id field so that each entity instance has a single, unchangeable key.

When you make an entity, you can set its ID value yourself or let JPA generate it automatically. To use automatic generation, add the @GeneratedValue annotation and pick one of these four strategies:

@Entity
public class Address {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...
}    

Composite Primary Keys

When you map an entity, it’s best to use a single column as its primary key. But sometimes you need a composite key—for example, when you work with a legacy database or when business rules require two values (like a date and an order number, or a country code and a timestamp).

To use a composite key:

Both approaches produce the same database tables. The only difference is in how you write your code to load or query the entity.

A class used for a composite primary key must follow these rules:

By following these rules, JPA and your code will handle composite keys correctly and safely.

@EmbeddedId

Let’s create the NewsId composite primary key class:


@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor

@Embeddable
public class NewsId implements Serializable {

    private String title;
    private String language;
}


As you can see, I’m using Lombok annotations to generate the getters, setters, constructors, equals(), and hashCode() methods.

In the next example, the News entity uses @EmbeddedId to include the NewsId key class. You do not need the @Id annotation here. Remember, whatever you use with @EmbeddedId must be a class marked with @Embeddable.

@Getter
@Setter
@Accessors(chain = true)

@Entity
public class News {

    @EmbeddedId
    private NewsId id;
    private String content;
}

JPA generated the following table:

CREATE TABLE news (
  content  VARCHAR(255),
  language VARCHAR(255) NOT NULL,
  title    VARCHAR(255) NOT NULL,
  PRIMARY KEY (language, title)
);

Here, language and title together form the primary key.

Now let’s write a simple unit test to check that our News entity with the composite key works correctly:


class NewsTest {
    private static EntityManager em;
    private static EntityTransaction tx;
    private static EntityManagerFactory emf;

    @BeforeAll
    static void init() {
        emf = Persistence.createEntityManagerFactory("testDB");
        em = emf.createEntityManager();
        tx = em.getTransaction();
    }

    @AfterAll
    static void close() {
        if (em != null) {
            em.close();
        }

        if (emf != null) {
            emf.close();
        }
    }

    @Test
    void test() {
        NewsId newsId = new NewsId("Humboldt penguin marks birthday", "EN");
        tx.begin();
        em.persist(new News().setContent("content").setId(newsId));
        tx.commit();

        tx.begin();
        News news = em.find(News.class, newsId);
        tx.commit();

        assertEquals("content", news.getContent());
        assertEquals(newsId, news.getId());
    }
}

@IdClass

Another way to define a composite key is with the @IdClass annotation. With this method, you:

In this example, the NewsId class is just a plain Java object (POJO) and needs no JPA annotations.

@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class NewsId implements Serializable {

    private String title;
    private String language;
}

Next, the News entity defines its composite key with @IdClass(NewsId.class). Each key field in the class (for example, title and language) is marked with @Id.

@Getter
@Setter
@Accessors(chain = true)

@Entity
@IdClass(NewsId.class)
public class News {

    @Id
    private String title;
    @Id
    private String language;
    private String content;
}

Both @EmbeddedId and @IdClass produce the same table layout:

CREATE TABLE news (
  content  VARCHAR(255),
  language VARCHAR(255) NOT NULL,
  title    VARCHAR(255) NOT NULL,
  PRIMARY KEY (language, title)
);

Next, we write a simple unit test to make sure our News entity with its composite key works:

NOTE: Before calling em.persist(news), set both the title and language on your News object.

class NewsTest {

    ....

    @Test
    void test() {
        var language = "EN";
        var title = "Humboldt penguin marks birthday";

        var news = new News()
                .setTitle(title)
                .setContent("content")
                .setLanguage(language);

        tx.begin();
        em.persist(news);
        tx.commit();

        NewsId newsId = new NewsId(title, language);

        tx.begin();
        News foundNews = em.find(News.class, newsId);
        tx.commit();

        assertEquals("content", foundNews.getContent());
        assertEquals(newsId.getTitle(), foundNews.getTitle());
        assertEquals(newsId.getLanguage(), foundNews.getLanguage());
    }
}

Using @IdClass can lead to mistakes because you must repeat each key field in both the key class and the entity, and the names and types must match exactly.

One clear difference appears in JPQL queries: