Appearance
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:
| Value | Creates | Updates | Deletes | What it does |
|---|---|---|---|---|
none | no | no | no | Nothing. Use this in production. |
update | yes | yes | no | Adds what is missing (a new column, a new table) but never deletes existing data or schema. Handy in development. |
validate | no | no | no | At startup, checks that entities and schema match; throws an exception on a mismatch. |
create | yes | yes | yes | Drops and recreates the schema at startup. |
create-drop | yes | yes | yes | Creates 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:
- It must be a public class.
- It must implement
Serializable. - It must have a no-argument constructor.
- It must override
equals()andhashCode().
Why:
equalsandhashCode: 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 aStringorLong, 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:
- Custom logic: start value, increment and so on.
- 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.
- 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 eachINSERTimmediately atpersist()and cannot batch inserts. - 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
SELECTand anUPDATEon 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
- Which
ddl-autovalue should production use? What doesvalidatedo? - What is the difference between
createandcreate-drop? - Without
@Table, what is the table forOrderDetailscalled? - How do you make the pair (
name,email) unique? How do you indexphone? - Without
@Column, is aStringcolumn nullable? How long is it? - Give the four rules for a composite key class, and the reason for
equals/hashCode. - What is the difference between
@IdClassand@EmbeddedId? - With
@SequenceGenerator(initialValue = 100, allocationSize = 5), what does the database sequence look like, and why do most inserts not query it? - Why can Hibernate not batch inserts with
IDENTITY? - Why is
GenerationType.TABLErarely used?
Answers
none. It checks entities against the schema at startup and fails on a mismatch.createdrops and recreates at startup;create-dropcreates at startup and drops at shutdown.order_details.@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"name", "email"}));@Table(indexes = @Index(name = "index_phone", columnList = "phone")).- Yes, nullable;
varchar(255). - Public,
Serializable, no-arg constructor,equals()andhashCode(). The caches store entities in maps keyed by the id. @IdClass: the key fields stay in the entity, each with@Id.@EmbeddedId: the entity holds one field of an@Embeddablekey class.start with 100 increment by 5. Each call reserves 5 ids, which Hibernate hands out from memory.- The id only exists after the row is inserted, so each insert must run immediately.
- It needs an extra table, a select and update per id, and locking under concurrency.