The JPA access type decides whether the persistence provider reads and writes an entity’s state through its fields (field access) or through its getter and setter methods (property access), and by default the position of the @Id annotation picks it. With @Id on a field, Hibernate uses field access for the whole entity, and with @Id on a getter, it uses property access. The @Access annotation sets the access type explicitly, for a whole entity or for a single attribute, which is called mixed access.
The access type matters whenever an entity has getters or setters with logic, or getter names that differ from the field names, because it decides which column names Hibernate creates and whether our setter code runs when an entity is loaded.
The following example shows the three ways to choose the access type in JPA, with the effect of each annotation as a comment.
// 1. Field access: @Id on a field
@Id @GeneratedValue
private Long id; // Hibernate reads and writes the fields, getters are ignored
// 2. Property access: @Id on a getter
@Id @GeneratedValue
public Long getId() { return id; } // Hibernate calls the getters and setters
public String getStageName() { return name; } // column "stageName", not "name"
// 3. Mixed access: a field-access entity with one property-access attribute
@Transient
private int seconds; // 225, not mapped
@Access(AccessType.PROPERTY)
@Column(name = "duration", length = 5)
protected String getDuration() { return "%d:%02d".formatted(seconds / 60, seconds % 60); } // stored as "3:45"
Notice that the column name in the second case comes from the getter getStageName(), not from the field name. Also notice the @Transient annotation in the third case, without which Hibernate would map the value twice.
Next, we compare the two access types and see how Hibernate 7 applies the default. After that, we map one entity with each access type in a small music app, look at the generated SQL, and cover the mistakes that Hibernate does not report.
1. Field Access vs Property Access
An entity has state, such as a song title, and Hibernate must copy that state into the table on insert and back into a new object on load. The access type says how Hibernate reaches the state. With field access, Hibernate reads and writes the fields with reflection. With property access, Hibernate calls the getter to read a value and the setter to write it.
For example, a music app has an Artist entity whose setter strips the spaces from the stage name. With property access, the setter also runs when Hibernate loads the artist from the database, so the code cleans data that came from the table too. With field access, Hibernate writes the field and never calls the setter.

The two access types differ in more places than the column names. The table sums up the differences that we see in the example.
| Field access | Property access | |
|---|---|---|
| Chosen by default when @Id is on | a field | a getter |
| Mapping annotations go on | the fields | the getters |
| Hibernate reads and writes | the fields, with reflection | the getters and setters |
| Column name comes from | the field name | the property name (getStageName() -> stageName) |
| Getters and setters required | no | yes, for every persistent property |
| Setter logic runs on load | no | yes |
| Helper getters, such as getDisplayName() | ignored | treated as properties, unless marked @Transient |
Field access is the better default for most entities, because the getters and setters stay free for the rest of the app. We can leave out a setter for a value that must not change, such as the ID, and add helper getters without mapping them by accident. Property access fits when the mapped value differs from what the class stores, but section 4 shows a more focused way to do that for a single attribute.
Both access types share some rules from the Jakarta Persistence specification. The instance variables of an entity must be private, protected or package-private, never public, and the getters and setters of persistent properties must be public or protected.
2. Field Access Example
The following example uses Hibernate 7.4.12.Final (Jakarta Persistence 3.2) with Java 25 and an H2 in-memory database, created through HibernatePersistenceConfiguration. The Playlist entity has the @Id annotation on its id field, so Hibernate uses field access for every attribute of the entity.
@Entity
public class Playlist {
@Id
@GeneratedValue
private Long id;
private String name;
protected Playlist() {
}
public Playlist(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void rename(String newName) {
if (newName == null || newName.isBlank()) {
throw new IllegalArgumentException("name must not be blank");
}
this.name = newName.strip();
}
}
Notice that Playlist has no setters at all. The ID never changes after the insert, so it gets only a getter, and the name changes through rename(), which checks its input. The protected no-argument constructor is for Hibernate, which needs it to create the object when it loads a row.
We persist a playlist and load it again. The column names in the SQL are the field names id and name.
Long playlistId = emf.callInTransaction(em -> {
Playlist playlist = new Playlist("Road trip");
em.persist(playlist);
return playlist.getId();
});
Playlist playlist = emf.callInTransaction(em -> em.find(Playlist.class, playlistId)); // name = "Road trip"
create table Playlist (id bigint not null, name varchar(255), primary key (id))
insert into Playlist (name,id) values (?,?)
select p1_0.id,p1_0.name from Playlist p1_0 where p1_0.id=?
To make the choice visible in the code, we can add @Access(AccessType.FIELD) to the class. It does not change the mapping here, but it documents the choice, so Hibernate no longer derives it from the position of @Id.
3. Property Access Example
The Artist entity has the @Id annotation on the getter getId(), so Hibernate uses property access. Every persistent property needs a getter and a setter, and the mapping annotations go on the getters. In our class, the field is called name, but the property is stageName, and the setter strips the spaces around the value.
@Entity
public class Artist {
private Long id;
private String name;
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStageName() {
return name;
}
public void setStageName(String stageName) {
this.name = stageName == null ? null : stageName.strip();
}
}
We save an artist with the name ” Nina “ and load it in a new transaction. The repository version of the class counts the calls of setStageName(), so we can see that Hibernate calls the setter while it loads the entity.
Long artistId = emf.callInTransaction(em -> {
Artist artist = new Artist();
artist.setStageName(" Nina ");
em.persist(artist);
return artist.getId();
});
Artist artist = emf.callInTransaction(em -> em.find(Artist.class, artistId));
String stageName = artist.getStageName(); // "Nina", setStageName() was called once during the load
create table Artist (id bigint not null, stageName varchar(255), primary key (id))
insert into Artist (stageName,id) values (?,?)
select a1_0.id,a1_0.stageName from Artist a1_0 where a1_0.id=?
We can see that the column is stageName, the name of the property, and that the field name appears nowhere in the SQL. With property access, Hibernate treats every getter as a persistent property, so a helper getter needs @Transient. For example, the class ArtistWithDisplayName in the example code is Artist plus a getDisplayName() getter without a setter, and Hibernate fails at startup.
org.hibernate.PropertyNotFoundException: Could not locate setter method for property 'displayName' of class 'com.howtodoinjava.hibernate.access.ArtistWithDisplayName'
With a setter, the same getter becomes a displayName column instead, which is worse, because nothing fails.
Setter logic also runs on every load, which is useful for cleanup such as strip() but surprising for logic with side effects, such as sending an event. And when a property uses lazy fetching, the specification warns not to read the field behind the property before Hibernate has loaded it, because the field still holds its initial value. The lazy loading guide explains when Hibernate loads such values.
4. Mixed Access With @Access
The @Access annotation sets the access type explicitly. On a class, it sets the access type of the whole entity. On a single field or getter, it overrides the access type for that one attribute, so an entity can use field access for most attributes and property access for one of them.
Mixed access fits when the database stores a value in a different form than the Java code uses. For example, the music app moves to an old database where the Song table keeps the duration as the text “3:45”. The Java code wants the duration in seconds, because it adds up the length of a playlist.
4.1. Mapping One Attribute Through Its Getter
The Song entity uses field access through @Access(AccessType.FIELD) and the @Id field. The seconds field holds the value in Java and is marked @Transient, so Hibernate does not map it. The protected getter getDuration() has @Access(AccessType.PROPERTY), so Hibernate reads and writes the duration column through getDuration() and setDuration().
@Entity
@Access(AccessType.FIELD)
public class Song {
@Id
@GeneratedValue
private Long id;
private String title;
@Transient
private int seconds;
protected Song() {
}
public Song(String title, int seconds) {
this.title = title;
this.seconds = seconds;
}
@Access(AccessType.PROPERTY)
@Column(name = "duration", length = 5)
protected String getDuration() {
return "%d:%02d".formatted(seconds / 60, seconds % 60);
}
protected void setDuration(String duration) {
String[] parts = duration.split(":");
this.seconds = Integer.parseInt(parts[0]) * 60 + Integer.parseInt(parts[1]);
}
public int getSeconds() {
return seconds;
}
}
We save a song of 225 seconds. Hibernate calls getDuration() for the insert and setDuration() for the load, so the table holds the text and the loaded entity holds the number.
Long songId = emf.callInTransaction(em -> {
Song song = new Song("Morning walk", 225);
em.persist(song);
return song.getId();
});
Song song = emf.callInTransaction(em -> em.find(Song.class, songId));
int seconds = song.getSeconds(); // 225
// select duration from Song where id = ? -> "3:45"
create table Song (duration varchar(5), id bigint not null, title varchar(255), primary key (id))
insert into Song (duration,title,id) values (?,?,?)
select s1_0.id,s1_0.duration,s1_0.title from Song s1_0 where s1_0.id=?
The getter and the setter are protected, so the rest of the app works with getSeconds() and never sees the text form. For a plain conversion like this one, a JPA AttributeConverter also works, but mixed access keeps the conversion inside the entity and needs no extra class.
4.2. Forgetting @Transient
In mixed access, the field behind a property-access attribute must be marked @Transient. The class still uses field access, so without @Transient Hibernate maps the seconds field as its own column, next to the duration column of the property.
create table SongWithoutTransient (seconds integer not null, duration varchar(5), id bigint not null, primary key (id))
insert into SongWithoutTransient (duration,seconds,id) values (?,?,?)
Hibernate gives no warning here. The table gets an extra seconds column that holds the same value in another form, and against an existing database without that column, every insert fails. The @Transient annotation is the JPA annotation from jakarta.persistence; the Java transient keyword also excludes a field from the mapping, but it excludes it from Java serialization too.
4.3. Annotations on the Wrong Member
The specification lists the placements where the behavior is undefined, and Hibernate does not report all of them.
- @Access(AccessType.PROPERTY) on a field.
- @Access(AccessType.FIELD) on a getter.
- @Access on a setter.
A related mistake is a mapping annotation on a getter in an entity that uses field access, without @Access(AccessType.PROPERTY). In the following entity, the @Id field selects field access, so Hibernate ignores the @Column annotation on getTitle() without any error.
@Entity
public class MisplacedColumn {
@Id
@GeneratedValue
private Long id;
private String title;
@Column(name = "song_title", nullable = false) // ignored: the entity uses field access
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
create table MisplacedColumn (id bigint not null, title varchar(255), primary key (id))
The column is title and accepts null, so neither the name nor the NOT NULL rule from @Column made it into the table. We keep all mapping annotations on the same kind of member as @Id, and use @Access only on the attributes that need the other kind.
5. JPA Access Type FAQs
5.1. Does an Embeddable Use the Access Type of Its Entity?
Yes. An @Embeddable class, such as an Address inside a Customer, gets the access type of the entity that contains it. If the embeddable needs its own access type, we put @Access on the embeddable class.
5.2. Does a Subclass Inherit the Access Type?
Yes. The default access type applies to the whole entity hierarchy, so a subclass of a field-access entity also uses field access, and its mapping annotations must be on fields too. To use another access type in the subclass, we annotate the subclass with @Access.
5.3. Which Access Type Does Spring Data JPA Use?
The same one as plain Hibernate, because Spring Data JPA does not change the mapping. The @Id position or the @Access annotation decides it, so the examples in this article work the same way in a Spring Boot app.
6. Conclusion
The access type decides how Hibernate reaches the state of an entity. With @Id on a field, Hibernate uses field access, so it reads and writes the fields and the column names come from the field names. With @Id on a getter, it uses property access, calls the getters and setters, and takes the column names from the property names.
Field access is the better default, because it leaves the getters and setters free for our own code. Property access runs setter logic on every load, and it maps every getter with a setter unless we mark it @Transient.
The @Access annotation makes the choice explicit and lets one attribute use the other access type. In that mixed case, we mark the backing field @Transient, and we keep all other mapping annotations on the same kind of member as @Id, because Hibernate ignores misplaced annotations without any error.
7. References
- Jakarta Persistence 3.2 specification
- Access annotation JavaDoc
- AccessType JavaDoc
- Transient annotation JavaDoc
- Hibernate ORM 7.4 User Guide, Access strategies
Happy Learning !!
Thanks, nice tip