JPA Annotations: @Entity, @Column, @Id and More in Hibernate

A reference of the JPA annotations used on almost every Hibernate entity, from @Entity and @Column to @Embedded, @Version and @Convert, each with the DDL Hibernate 7.4 generates for it.

logo

JPA annotations are the jakarta.persistence annotations that tell Hibernate how a Java class maps to a database table. They say which class is an entity, which field is the primary key, what each column is called and how long it is, and which fields are not stored at all. Hibernate reads them at startup to build the mapping, and it can generate the tables from them.

We put JPA annotations on every entity class, for example to map the Play of a theater booking app to the play table with a unique title and an index on the genre.

The following example maps a Play entity with the most common annotations, and the second block shows the DDL that Hibernate 7.4 generated for it on H2.

@Entity
@Table(name = "play", indexes = @Index(name = "idx_play_genre", columnList = "genre"))
public class Play {

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

  @Column(nullable = false, length = 120, unique = true)
  private String title;

  @Enumerated(EnumType.STRING)
  private Genre genre;

  @Transient
  private boolean featured;
}
create table play (
    ...
    id bigint not null,
    title varchar(120) not null unique,              -- @Column
    genre enum ('COMEDY','DRAMA','MUSICAL'),         -- @Enumerated(STRING)
    primary key (id)                                 -- @Id
);                                                   -- no column for featured
create index idx_play_genre on play (genre);

Notice that each annotation changes one line of the DDL, and the @Transient field featured gets no column at all.

Next, we go through the annotations we use on almost every entity, each with a short snippet and the DDL or SQL that Hibernate 7.4 generated for it.

1. How JPA Annotations Map a Class to a Table

An entity is a Java class whose objects Hibernate stores as rows of a table. Without any annotation except @Entity and @Id, Hibernate already maps every field to a column with the field’s name, and a String becomes varchar(255), so the other annotations only change these defaults.

Left: the Play class with one annotation per field, from @Entity @Table and @Id @GeneratedValue down to @Transient; right: the matching line of the generated play table, such as title varchar(120) not null unique, ticket_price numeric(6,2), synopsis clob and version integer not null; the @Transient field featured maps to no column
Each annotation changes one line of the CREATE TABLE. A @Transient field never reaches the table.

Since Hibernate 6, all standard annotations are in the jakarta.persistence package. Older code imports javax.persistence, which Hibernate 6 and 7 no longer read.

Each annotation in the table maps one part of the class, and section 2 shows each one with its DDL.

AnnotationPurposeKey attributes
@EntityMarks a class as an entityname (entity name used in JPQL)
@TableSets the table for the entityname, schema, uniqueConstraints, indexes, check, comment
@IdMarks the primary key fieldnone
@GeneratedValueLets Hibernate or the database create the keystrategy (IDENTITY, SEQUENCE, UUID, TABLE, AUTO), generator
@SequenceGeneratorConfigures the database sequencename, sequenceName, allocationSize, initialValue
@ColumnSets the column detailsname, nullable, length, unique, precision, scale, insertable, updatable, columnDefinition, check, comment
@TransientExcludes a field from the tablenone
@EnumeratedStores an enum as a number or a namevalue (ORDINAL, STRING)
@EnumeratedValueStores a fixed code for each enum constantplaced on a field of the enum
@LobMaps large text or binary datanone
@BasicSets fetching and optionality of a simple fieldfetch, optional
@TemporalDeprecated; only for java.util.Date and Calendarvalue
@Embeddable / @EmbeddedStores a value object in the owner’s tablenone
@AttributeOverrideRenames a column of an embedded objectname, column
@VersionOptimistic locking counternone
@Convert / @ConverterConverts a field with an AttributeConverterconverter, disableConversion
@AccessChooses field or property (getter) accessvalue (FIELD, PROPERTY)
@MappedSuperclassShares mapped fields with subclasses, no own tablenone
@InheritanceMaps a class hierarchystrategy (SINGLE_TABLE, JOINED, TABLE_PER_CLASS)
@ElementCollectionStores a collection of values in a separate tablefetch, targetClass

2. JPA Annotations Example

We map a small theater app with Hibernate 7.4.11, Java 25 and an in-memory H2 database, and Hibernate generates the schema from the annotations at startup.

2.1. Domain Model

A Theater has an address and box office contacts, a Play such as “Hamlet” has a genre, a duration and a synopsis, and a Performance is one show of a play in a theater hall at a given time. The complete project on GitHub bootstraps Hibernate with HibernatePersistenceConfiguration and checks every DDL line shown here in 16 JUnit tests.

EntityTableKey strategyAnnotations it shows
TheaterTheaterIDENTITY@Embedded, @AttributeOverride, columnDefinition
PlayplaySEQUENCE@Column, @Enumerated, @Lob, @Version, @Convert, @Access, @Transient
PerformanceperformanceUUID@Entity(name), uniqueConstraints, ORDINAL, insertable/updatable

2.2. @Entity and @Table

The @Entity annotation makes the class persistent. Without @Table, the table gets the entity name, which is the class name, so Theater is stored in the table Theater. The @Table annotation sets another table name and adds constraints and indexes to the generated DDL. For example, the unique constraint uk_hall_time stops the box office from booking two performances in the same hall at the same time.

@Entity(name = "Show")
@Table(name = "performance",
    uniqueConstraints = @UniqueConstraint(name = "uk_hall_time",
        columnNames = {"theater_id", "hall", "starts_at"}),
    indexes = @Index(name = "idx_starts_at", columnList = "starts_at"))
public class Performance { ... }
create table performance (
    ...
    constraint uk_hall_time unique (theater_id, hall, starts_at)
);
create index idx_starts_at on performance (starts_at);

The uniqueConstraints attribute is for a rule over several columns, and a single unique column is simpler with @Column(unique = true). The name in @Entity is a different thing, namely the name we write in queries, as section 3.1 shows.

2.3. @Id and @GeneratedValue Strategies

The @Id annotation marks the primary key, and @GeneratedValue tells Hibernate who creates its value. We use a different strategy on each entity.

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

// Play
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "play_seq")
@SequenceGenerator(name = "play_seq", sequenceName = "play_seq", allocationSize = 10)
private Long id;

// Performance
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Each strategy produces different DDL and a different INSERT.

StrategyDDLSQL on em.persist()Result
IDENTITYid bigint generated by default as identityinsert into Theater (…,id) values (…,default)id = 1, assigned by the database
SEQUENCEcreate sequence play_seq start with 1 increment by 10select next value for play_seq, then insert into play (…,id)id = 1; the next play gets 2 from the same block of 10
UUIDid uuid not nullinsert into performance (…,id)id = b7fdacd3-e829-…, created in Java

With IDENTITY, Hibernate must run the INSERT at persist() to learn the id, which also turns off JDBC batching for that entity. So on databases that support sequences, we pick SEQUENCE with an allocationSize. The TABLE strategy keeps counters in a separate table and needs extra queries and locks, whereas AUTO, the default, lets Hibernate pick a strategy for the database.

2.4. @Column Attributes

The @Column annotation changes one column, and because every attribute has a default, we set only what differs.

@Column(nullable = false, length = 120, unique = true)
private String title;

@Column(name = "ticket_price", precision = 6, scale = 2)
private BigDecimal ticketPrice;

@Column(columnDefinition = "smallint default 100 not null")
private int seats;
AttributeDefaultOur valueGenerated column
namefield name“ticket_price”ticket_price
length255120title varchar(120)
nullabletruefalsenot null
uniquefalsetrueunique
precision, scaledatabase default6, 2numeric(6,2), stores 40.00
columnDefinitiongenerated“smallint default 100 not null”seats smallint default 100 not null
insertable, updatabletruefalsecolumn left out of INSERT and UPDATE

Hibernate copies columnDefinition into the DDL as is, so it can tie the mapping to one database. We use it only when no other attribute produces the column we need.

The pair insertable = false, updatable = false makes a field read-only. The common case is a second field on a column that a relationship already writes, such as the plain playId next to the play association.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "play_id")
private Play play;                        // writes play_id

@Column(name = "play_id", insertable = false, updatable = false)
private Long playId;                      // reads play_id: 1

2.5. @Transient

A field marked @Transient has no column, and Hibernate never reads or writes it. We use @Transient for values that exist only in memory, for example a featured flag that the box office screen shows but never saves.

@Transient
private boolean featured;

play.setFeatured(true);
em.persist(play);                       // insert into play (...) - no featured column
boolean featured = em.find(Play.class, id).isFeatured();   // false, in a new persistence context

2.6. @Enumerated With ORDINAL and STRING

By default, an enum field without annotations, or with @Enumerated alone, stores the constant’s position (ORDINAL), whereas EnumType.STRING stores the constant’s name. Jakarta Persistence 3.2 adds @EnumeratedValue to store a code we choose.

@Enumerated                         // Performance.status
private Status status;              // SCHEDULED stored as 0

@Enumerated(EnumType.STRING)        // Play.genre
private Genre genre;                // DRAMA stored as 'DRAMA'

@Enumerated(EnumType.STRING)        // Performance.language
private Language language;          // ENGLISH stored as 'en'

enum Language {
  ENGLISH("en"), HINDI("hi");

  @EnumeratedValue
  private final String code;
  ...
}
Comparison grid of three enum mappings: ORDINAL on Status creates tinyint check 0 to 2 and stores SCHEDULED as 0, not safe to reorder but safe to rename; STRING on Genre creates an enum column and stores DRAMA as 'DRAMA', safe to reorder but not to rename; @EnumeratedValue on Language creates varchar(2) with check in hi, en and stores ENGLISH as 'en', safe for both; a red note explains that adding DRAFT before SCHEDULED makes old rows with 0 load as DRAFT
ORDINAL breaks when constants move, STRING breaks when they are renamed. @EnumeratedValue keeps a fixed code.
status tinyint check ((status between 0 and 2))
genre enum ('COMEDY','DRAMA','MUSICAL')
language varchar(2) check ((language in ('hi','en')))

We prefer EnumType.STRING or @EnumeratedValue over ORDINAL. The number 0 has no meaning when we read the table, and it points to a different constant as soon as someone adds one in front.

2.7. @Lob and @Basic(fetch = LAZY)

The @Lob annotation maps text or bytes that do not fit a normal column. On a String, Hibernate creates a CLOB (character large object), and on a byte[], it creates a BLOB.

@Lob
@Basic(fetch = FetchType.LAZY)
private String synopsis;            // synopsis clob

The setting @Basic(fetch = LAZY) asks Hibernate to load the column only when it is read. Without bytecode enhancement, Hibernate ignores LAZY on a basic field. Our em.find() still selected p1_0.synopsis, and Hibernate.isPropertyInitialized(play, “synopsis”) returned true. Lazy basic fields need the Hibernate Maven or Gradle enhancement plugin, whereas lazy loading of associations works without it.

2.8. Dates Without @Temporal

The @Temporal annotation told older JPA versions whether a java.util.Date was a date, a time or a timestamp. Jakarta Persistence 3.2 deprecates @Temporal (@Deprecated(since = “3.2”)), together with java.util.Date and Calendar mappings. The java.time types carry the date or time precision themselves and need no annotation.

@Column(name = "starts_at", nullable = false)
private LocalDateTime startsAt;     // starts_at timestamp(6) not null

Every java.time class, including Instant, maps to its own date and time column type.

2.9. @Embeddable, @Embedded and @AttributeOverride

An embeddable is a value object without its own table or id, such as an address, and its fields become columns of the entity that holds it.

@Embeddable
public class Address {
  @Column(length = 100) private String street;
  @Column(length = 50)  private String city;
  @Column(length = 10)  private String zip;
}

@Embeddable
public record BoxOffice(String phone, String email) { }

The entity marks the field with @Embedded, and @AttributeOverride renames one of the embedded columns for that entity only.

@Embedded
@AttributeOverride(name = "zip", column = @Column(name = "postal_code", length = 10))
private Address address;

@Embedded
private BoxOffice boxOffice;
The Theater entity with fields id, name, seats, address and boxOffice; the Address class with street, city and zip and the BoxOffice record with phone and email both point into one Theater table whose columns are id, name, seats, street, city, postal_code, phone and email; postal_code is marked as zip renamed by @AttributeOverride
Embedded fields are flattened into the owner’s table. @AttributeOverride changes the column name, here zip to postal_code.
create table Theater (
    seats smallint default 100 not null,
    id bigint generated by default as identity,
    postal_code varchar(10),
    city varchar(50),
    name varchar(80) not null unique,
    street varchar(100),
    email varchar(255),
    phone varchar(255),
    primary key (id)
);

When an entity has two fields of the same embeddable type, such as a billing and a shipping address, @AttributeOverride is required, otherwise both fields map to the same column names. Since Jakarta Persistence 3.2, a Java record works as an embeddable, as BoxOffice shows. When we use an embeddable as a composite primary key, it goes on an @EmbeddedId field instead.

2.10. @Version

The @Version annotation adds a counter that Hibernate increments on every update, which protects against lost updates when two users edit the same row (optimistic locking).

@Version
private Integer version;                        // version integer not null

em.persist(play);                               // version = 0
play.setTicketPrice(new BigDecimal("45.00"));   // update play set ...,version=? where id=? and version=?
                                                // version = 1

If another transaction changed the row in between, the where version=? matches no row and the commit fails.

jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.howtodoinjava.hibernate.annotations.Play with id '1']

2.11. @Convert With an AttributeConverter

An AttributeConverter converts a field to a column value and back, so we can store the intermission flag as “Y” or “N”.

@Converter
public class YesNoConverter implements AttributeConverter<Boolean, String> {
  public String convertToDatabaseColumn(Boolean value) { return value != null && value ? "Y" : "N"; }
  public Boolean convertToEntityAttribute(String column) { return "Y".equals(column); }
}
@Convert(converter = YesNoConverter.class)
@Column(length = 1)
private boolean intermission;       // intermission varchar(1), stores 'Y'

The converter decides the stored value, so the column holds ‘Y’ while the Java field stays a boolean. With @Converter(autoApply = true), the converter applies to every Boolean field of all entities without @Convert.

2.12. @Access

The position of @Id sets the access type of the entity. When @Id is on a field, Hibernate reads and writes the fields (field access), and when it is on a getter, Hibernate calls getters and setters (property access). The @Access annotation changes the access type for one attribute.

@Access(AccessType.PROPERTY)
@Column(length = 120)
public String getSlug() {
  return title == null ? null : title.toLowerCase().replace(' ', '-');
}

public void setSlug(String slug) { }    // needed by property access

Play keeps field access for everything else, and the slug column gets “hamlet”. After setTitle(“Hamlet Live”), the next UPDATE stores “hamlet-live”.

2.13. Relationship Annotations

Associations between entities have their own annotations, and the foreign key column decides which table holds the link.

AnnotationRelationshipForeign key
@OneToOneA play and its single posterIn one of the two tables
@ManyToOneMany performances of one playplay_id in performance
@ManyToManyPlays and actorsIn a join table
@JoinColumnNames the foreign key column@JoinColumn(name = “play_id”)

The side that holds the foreign key is the owning side, and the other side uses mappedBy. Cascade types decide what happens to related entities on save and delete.

2.14. What Jakarta Persistence 3.2 Added

Version 3.2 (Hibernate 7) adds a few mapping options, and each row shows the DDL that Hibernate 7.4 generated for one of them.

AdditionSnippetGenerated DDL
check on @Column and @Tablecheck = @CheckConstraint(name = “ck_duration”, constraint = “duration_minutes > 0”)duration_minutes integer constraint ck_duration check (duration_minutes > 0)
comment on @Column and @Table@Table(name = “play”, comment = “Plays in the repertoire”)comment on table play is ‘Plays in the repertoire’
@EnumeratedValueOn a field of the enumlanguage varchar(2) check ((language in (‘hi’,’en’)))
Records as embeddables@Embeddable public record BoxOffice(…)phone varchar(255), email varchar(255)
@Temporal deprecatedUse java.time typesstarts_at timestamp(6)

3. JPA Annotations FAQs

3.1. What Is the Difference Between @Entity(name) and @Table(name)?

The @Entity(name) attribute is the name of the entity in JPQL queries, whereas @Table(name) is the name of the table in SQL. The Performance entity sets both.

@Entity(name = "Show")
@Table(name = "performance")

TypedQuery<String> halls = em.createQuery("select s.hall from Show s", String.class);          // select p1_0.hall from performance p1_0
TypedQuery<String> wrong = em.createQuery("select p.hall from Performance p", String.class);   // error: Could not resolve root entity 'Performance'

Without @Table, the entity name is also the table name, so in most entities we change only @Table and leave the entity name as the class name. Every HQL query uses the entity name.

3.2. Why Does Hibernate Say a Column Is Duplicated in Mapping?

Hibernate reports a duplicated column when two fields write the same column, and it stops at startup because it does not know which value to insert.

@ManyToOne
@JoinColumn(name = "play_id")
private Play play;

@Column(name = "play_id")
private Long playId;
org.hibernate.MappingException: Column 'play_id' is duplicated in mapping for entity 'com.howtodoinjava.hibernate.annotations.DuplicateColumnShow' (use '@Column(insertable=false, updatable=false)' when mapping multiple properties to the same column)

The message names the fix, so we add insertable = false, updatable = false to the plain field, as in section 2.4.

3.3. Does @Column(nullable = false) Validate the Value?

Partly. It adds not null to the DDL, and Hibernate also checks it before the INSERT, so a Play without a title never reaches the database.

org.hibernate.PropertyValueException: not-null property references a null or transient value for entity com.howtodoinjava.hibernate.annotations.Play.title

Only the database checks unique = true, at flush or commit (Unique index or primary key violation … PLAY(TITLE …) VALUES (‘Hamlet’)). If we want messages a user can read, we add Bean Validation annotations such as @NotNull and @Size next to @Column.

3.4. Should I Import javax.persistence or jakarta.persistence?

We import jakarta.persistence. Jakarta EE 9 renamed the package, and Hibernate 6 and 7 support only the new one. To move old code, we replace the imports, because the annotation names and attributes stayed the same. Projects that still use hbm.xml files can move from XML mappings to annotations as well.

3.5. Which Hibernate-Specific Annotations Are Worth Knowing?

Hibernate adds annotations in org.hibernate.annotations for cases the standard does not cover, and they work only with Hibernate.

AnnotationPurpose
@NaturalIdMarks a business key, such as an ISBN, and loads by it
@ImmutableEntity is never updated
@SoftDeleteMarks rows as deleted instead of deleting them
@CreationTimestamp, @UpdateTimestampFills a timestamp on insert or update

4. Conclusion

A handful of JPA annotations covers most entities. We use @Entity and @Id to make a class persistent, @Table and @Column to shape the DDL, @Enumerated(EnumType.STRING) for enums, @Embedded for value objects, and @Version to protect updates.

With Hibernate 7 we import the annotations from jakarta.persistence and use java.time instead of @Temporal. Jakarta Persistence 3.2 also lets us add check constraints and comments in the mapping itself, and the other Hibernate tutorials build on the same entity mappings.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. could you please post a sample from the usage of @NameQuerie()

    I mean it associates a query to an Entity(), but how we use it? with Repository class?
    It’s still unclear for me.

  2. Hi Lokesh,

    Thanks much for putting all the annotations at one place. Can you please provide a better insight for the following annotations @SelectBeforeUpdate, @DynamicInsert, @DynamicUpdate, @Polymorphism (type=PolymorphismType.EXPLICIT),@OptimisticLocking (type = OptimisticLockType.ALL). Thanks very much!

  3. Woah, fantastic article Lokesh, All in one go to page for preparing JPA annotations.. very well done.
    Keep up the good work.!!

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.