Skip to content
Chapter 27 · Spring Boot

JPA Part 5: Mapping Entities to Tables ​

So far entities used JPA's defaults: a table named after the class, a column per field. This part takes control of the mapping, starting with who creates the schema (ddl-auto), then @Table, @Column, primary keys and how ids are generated.

ddl-auto: who manages the schema ​

spring.jpa.hibernate.ddl-auto tells Hibernate what to do with the database schema (tables, columns, constraints) at startup:

ValueCreatesUpdatesDeletesWhat it does
nonenononoNothing. Use this in production.
updateyesyesnoAdds what is missing (a new column, a new table) but never deletes existing data or schema. Handy in development.
validatenononoAt startup, checks that entities and schema match; throws an exception on a mismatch.
createyesyesyesDrops and recreates the schema at startup.
create-dropyesyesyesCreates the schema at startup and drops it at shutdown. Spring Boot's default for in-memory databases such as H2.
properties
spring.jpa.hibernate.ddl-auto=update

@Table ​

Optional. Without it the table is named after the entity, converted from CamelCase to snake_case: UserDetails becomes user_details. (H2 shows it as USER_DETAILS because it stores unquoted names in upper case.)

java
public @interface Table {
    String name() default "";
    String schema() default "";
    UniqueConstraint[] uniqueConstraints() default {};
    Index[] indexes() default {};
}
java
@Table(name = "USER_DETAILS", schema = "ONBOARDING")
@Entity
public class UserDetails { ... }

schema puts the table in a particular schema (a namespace inside the database).

Unique constraints and indexes are defined at table level:

java
@Table(
    name = "USER_DETAILS",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = "phone"),            // single column
        @UniqueConstraint(columnNames = {"name", "email"})   // composite: the pair must be unique
    },
    indexes = {
        @Index(name = "index_phone", columnList = "phone"),              // single column
        @Index(name = "index_name_email", columnList = "name, email")    // composite, comma separated
    }
)
@Entity
public class UserDetails {

    @Id
    private Long id;
    private String name;
    private String email;
    private String phone;

    public UserDetails() {
    }
    // getters and setters
}

Hibernate generates:

sql
create table user_details (id bigint not null, email varchar(255), name varchar(255), phone varchar(255),
    primary key (id), unique (phone), unique (name, email))
create index index_phone on user_details (phone)
create index index_name_email on user_details (name, email)

@Column ​

Also optional. Every field becomes a column with default settings: named after the field (snake_case), nullable, not unique, length 255 for strings. To change them:

java
@Table(name = "user_details")
@Entity
public class UserDetails {

    @Id
    private Long id;

    @Column(name = "full_name", unique = true, nullable = false, length = 255)
    private String name;

    private String email;
    private String phone;
    // ...
}

The column is now full_name varchar(255) not null unique. unique = true is the single-column shortcut for a unique constraint.

Primary key: @Id ​

A primary key must be unique and not null, and identifies each row. Mark it with @Id. An entity has one primary key, and normally one @Id field.

Composite keys ​

When two columns together identify a row (say name + address), you create a separate key class. Its rules:

  1. It must be a public class.
  2. It must implement Serializable.
  3. It must have a no-argument constructor.
  4. It must override equals() and hashCode().

Why:

  • equals and hashCode: the first- and second-level caches store entities in maps keyed by the id. With a composite key, the key is your class, so two keys with the same values must be equal and hash the same.
  • Serializable: a single key is a String or Long, already serializable. A composite key is a custom class, and JPA requires that it can be serialized, for example to send it over the network in a distributed cache.

Way 1: @IdClass. The entity keeps the fields, each marked @Id, and names the key class:

java
@Table(name = "user_details")
@IdClass(UserDetailsCK.class)
@Entity
public class UserDetails {

    @Id
    private String name;

    @Id
    private String address;

    private String phone;

    public UserDetails() {
    }
    // getters and setters
}
java
public class UserDetailsCK implements Serializable {

    private String name;
    private String address;

    public UserDetailsCK() {
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof UserDetailsCK)) return false;
        UserDetailsCK userCK = (UserDetailsCK) obj;
        return this.name.equals(userCK.name) && this.address.equals(userCK.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }
}

Way 2: @Embeddable and @EmbeddedId. The key class is marked @Embeddable and the entity holds one field of it:

java
@Table(name = "user_details")
@Entity
public class UserDetails {

    @EmbeddedId
    private UserDetailsCK userDetailsCK;

    private String phone;

    public UserDetails() {
    }
    // getters and setters
}
java
@Embeddable
public class UserDetailsCK implements Serializable {

    private String name;
    private String address;

    public UserDetailsCK() {
    }

    // same equals() and hashCode() as above
}

Either way, the table gets primary key (address, name).

@GeneratedValue: generating the id ​

By default a primary key is not filled in: you must set it yourself before saving. @GeneratedValue lets it be generated. It works with a single @Id, not with composite keys.

1. GenerationType.IDENTITY ​

The database's auto-increment column generates a new id on each insert.

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

2. GenerationType.SEQUENCE ​

A sequence is a database object that hands out unique numbers, independent of any table. In SQL you control it fully:

sql
CREATE SEQUENCE user_seq INCREMENT BY 25 START WITH 100 MAXVALUE 9999;

In JPA:

java
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "unique_user_seq")
@SequenceGenerator(
    name = "unique_user_seq",
    sequenceName = "db_seq_name",
    initialValue = 100,
    allocationSize = 5
)
private Long id;
  • name: the generator's name inside JPA; @GeneratedValue(generator = ...) refers to it.
  • sequenceName: the sequence's name in the database. If a sequence with that name already exists, it is used; otherwise Hibernate creates it.
  • initialValue: the first number.
  • allocationSize: how many ids Hibernate reserves per trip to the database.

Hibernate creates create sequence db_seq_name start with 100 increment by 5. The increment equals allocationSize on purpose: each call to the sequence reserves a block of 5 ids, which Hibernate then hands out from memory as 100, 101, 102, .... Saving 7 users gave ids 100 to 106 with only three select next value for db_seq_name queries (Hibernate fetches twice at the start, then once per new block).

Advantages of SEQUENCE over IDENTITY:

  1. Custom logic: start value, increment and so on.
  2. Independent of a table: several tables can share one sequence. Some companies even run a small service that hands out unique ids to many microservices.
  3. Ids can be cached in blocks, so saving does not need the database to produce each id. With IDENTITY, the id only exists once the row is inserted, so Hibernate must run each INSERT immediately at persist() and cannot batch inserts.
  4. Portability: auto-increment works differently across databases; sequences behave more consistently.

3. GenerationType.TABLE ​

Uses @TableGenerator and a separate table just to hand out ids. Rarely used, because it is inefficient:

  • an extra table only for ids
  • every new id needs a SELECT and an UPDATE on that table
  • parallel inserts need locking on that table, which becomes a bottleneck

A sequence does the same job inside the database with an atomic counter, far more efficiently.

Next: One entity maps to one table, but real data is related: a user has an address, orders, products. Part 6 starts the association mappings with one-to-one.

Check yourself ​

  1. Which ddl-auto value should production use? What does validate do?
  2. What is the difference between create and create-drop?
  3. Without @Table, what is the table for OrderDetails called?
  4. How do you make the pair (name, email) unique? How do you index phone?
  5. Without @Column, is a String column nullable? How long is it?
  6. Give the four rules for a composite key class, and the reason for equals/hashCode.
  7. What is the difference between @IdClass and @EmbeddedId?
  8. With @SequenceGenerator(initialValue = 100, allocationSize = 5), what does the database sequence look like, and why do most inserts not query it?
  9. Why can Hibernate not batch inserts with IDENTITY?
  10. Why is GenerationType.TABLE rarely used?
Answers
  1. none. It checks entities against the schema at startup and fails on a mismatch.
  2. create drops and recreates at startup; create-drop creates at startup and drops at shutdown.
  3. order_details.
  4. @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"name", "email"})); @Table(indexes = @Index(name = "index_phone", columnList = "phone")).
  5. Yes, nullable; varchar(255).
  6. Public, Serializable, no-arg constructor, equals() and hashCode(). The caches store entities in maps keyed by the id.
  7. @IdClass: the key fields stay in the entity, each with @Id. @EmbeddedId: the entity holds one field of an @Embeddable key class.
  8. start with 100 increment by 5. Each call reserves 5 ids, which Hibernate hands out from memory.
  9. The id only exists after the row is inserted, so each insert must run immediately.
  10. It needs an extra table, a select and update per id, and locking under concurrency.