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.

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.
| Annotation | Purpose | Key attributes |
|---|---|---|
| @Entity | Marks a class as an entity | name (entity name used in JPQL) |
| @Table | Sets the table for the entity | name, schema, uniqueConstraints, indexes, check, comment |
| @Id | Marks the primary key field | none |
| @GeneratedValue | Lets Hibernate or the database create the key | strategy (IDENTITY, SEQUENCE, UUID, TABLE, AUTO), generator |
| @SequenceGenerator | Configures the database sequence | name, sequenceName, allocationSize, initialValue |
| @Column | Sets the column details | name, nullable, length, unique, precision, scale, insertable, updatable, columnDefinition, check, comment |
| @Transient | Excludes a field from the table | none |
| @Enumerated | Stores an enum as a number or a name | value (ORDINAL, STRING) |
| @EnumeratedValue | Stores a fixed code for each enum constant | placed on a field of the enum |
| @Lob | Maps large text or binary data | none |
| @Basic | Sets fetching and optionality of a simple field | fetch, optional |
| @Temporal | Deprecated; only for java.util.Date and Calendar | value |
| @Embeddable / @Embedded | Stores a value object in the owner’s table | none |
| @AttributeOverride | Renames a column of an embedded object | name, column |
| @Version | Optimistic locking counter | none |
| @Convert / @Converter | Converts a field with an AttributeConverter | converter, disableConversion |
| @Access | Chooses field or property (getter) access | value (FIELD, PROPERTY) |
| @MappedSuperclass | Shares mapped fields with subclasses, no own table | none |
| @Inheritance | Maps a class hierarchy | strategy (SINGLE_TABLE, JOINED, TABLE_PER_CLASS) |
| @ElementCollection | Stores a collection of values in a separate table | fetch, 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.
| Entity | Table | Key strategy | Annotations it shows |
|---|---|---|---|
| Theater | Theater | IDENTITY | @Embedded, @AttributeOverride, columnDefinition |
| Play | play | SEQUENCE | @Column, @Enumerated, @Lob, @Version, @Convert, @Access, @Transient |
| Performance | performance | UUID | @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.
| Strategy | DDL | SQL on em.persist() | Result |
|---|---|---|---|
| IDENTITY | id bigint generated by default as identity | insert into Theater (…,id) values (…,default) | id = 1, assigned by the database |
| SEQUENCE | create sequence play_seq start with 1 increment by 10 | select next value for play_seq, then insert into play (…,id) | id = 1; the next play gets 2 from the same block of 10 |
| UUID | id uuid not null | insert 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;
| Attribute | Default | Our value | Generated column |
|---|---|---|---|
| name | field name | “ticket_price” | ticket_price |
| length | 255 | 120 | title varchar(120) |
| nullable | true | false | not null |
| unique | false | true | unique |
| precision, scale | database default | 6, 2 | numeric(6,2), stores 40.00 |
| columnDefinition | generated | “smallint default 100 not null” | seats smallint default 100 not null |
| insertable, updatable | true | false | column 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;
...
}

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;

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.
| Annotation | Relationship | Foreign key |
|---|---|---|
| @OneToOne | A play and its single poster | In one of the two tables |
| @ManyToOne | Many performances of one play | play_id in performance |
| @ManyToMany | Plays and actors | In a join table |
| @JoinColumn | Names 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.
| Addition | Snippet | Generated DDL |
|---|---|---|
| check on @Column and @Table | check = @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’ |
| @EnumeratedValue | On a field of the enum | language varchar(2) check ((language in (‘hi’,’en’))) |
| Records as embeddables | @Embeddable public record BoxOffice(…) | phone varchar(255), email varchar(255) |
| @Temporal deprecated | Use java.time types | starts_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.
| Annotation | Purpose |
|---|---|
| @NaturalId | Marks a business key, such as an ISBN, and loads by it |
| @Immutable | Entity is never updated |
| @SoftDelete | Marks rows as deleted instead of deleting them |
| @CreationTimestamp, @UpdateTimestamp | Fills 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
- Jakarta Persistence 3.2 Specification
- jakarta.persistence package JavaDoc (Jakarta Persistence 3.2)
- Column JavaDoc (Jakarta Persistence 3.2)
- GenerationType JavaDoc (Jakarta Persistence 3.2)
- EnumeratedValue JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Embeddable values
- Hibernate ORM 7.4 User Guide: Optimistic locking
Happy Learning !!
very good information and explained in detailed..
Thank you very much..
This is called Crystal Clear explanation. Superb Bro.
Short, Simple and Fantastic. Thanks a lot.
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.
thanks a lot for your helpful post.
Very nice…explanation…thnkx yr
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!
I will try to find time for them. Thanks for suggestion. I appreciate it.
Woah, fantastic article Lokesh, All in one go to page for preparing JPA annotations.. very well done.
Keep up the good work.!!