Entity beans, also known as persistent objects, have a key property that distinguishes
them from regular Java objects: a primary key uniquely identifies them.
Mapping data from tables to the states of entity beans can pose a challenge due to the impedance mismatch problem between the object and table worlds.
To address this problem, two approaches are typically taken: the code-first approach or the database-first approach.
Neither approach is optimal. In practice, there are mixed forms that work well—the database schema is always in the foreground:
With our database schema, this is simple: we can transfer the tables directly to entity beans. There is no case where the data of an entity bean is distributed to multiple tables or a table is distributed to multiple entity beans.
Persistent objects in Java are implemented using entity beans, with each entity bean being a basic Java object. To represent an object, a class is required to act as a container for its states. In this case, let’s create a new class called Profile that can later hold the details of the Profile table:
class Profile {
}
You don’t have to implement interfaces or extend a specific superclass. The class names usually don’t have special prefixes or suffixes; we can put the type in a package entity and separate it from other types. That the class is named like the table is common, as long as the table follows the Java naming conventions.
For the Jakarta Persistence provider to map rows from the Profile table to an entity bean instance, the entity bean must have the ability to hold the data. It’s important to determine which data from the table should be held by the entity beans and which Java data types are appropriate.
For this discussion, let’s focus on the Profile table. Because we’re using the databasefirst approach, we need to examine all the columns that we want to transfer to the entity bean. In our case, we’ll transfer all the columns, as listed in this table.
The transfer of the types happens via two dimensions: How big are the data types, and can they be NULL? If they can be NULL (called nullable types), we must use the wrapper types (Long, Short, Byte) instead of the primitive data types because references can be null.
In some cases, there are several options:
Our data types are thus fixed. We can set instance variables with the selected data types in the Profile class:
public class Profile {
Long id;
String nickname;
LocalDate birthdate;
short manelength;
byte gender;
Byte attractedToGender;
String description;
LocalDateTime lastseen;
}
For some O/R mappers, this is sufficient. However, Jakarta Persistence has further requirements.
With Jakarta Persistence, more O/R metadata is needed because the instance variables alone aren’t enough. The metadata determines, for example, that the class is an entity bean or what the name of the join column is.
The Jakarta Persistence specification provides two ways to specify the metadata:
Both ways are equally expressive. The advantage (and disadvantage) of the annotations is the direct connection to the code, but the settings are also directly exposed this way. The Jakarta Persistence specification documents both variants.
We’ll later set metadata via annotations, and these can be roughly divided into two groups:
O/R mappers require the metadata for correct mapping. The entity bean class carries a logical annotation:
@Entity
public class Profile {
…
}
A Jakarta Persistence entity class is always annotated with @Entity; the annotation is allowed on concrete or abstract classes, but not on interfaces.
The Jakarta Persistence provider must take the data from the JavaBean and write it back. In doing so, it can fall back on the instance variables or on setters/getters:
@Entity
@Access( AccessType.FIELD )
public class Profile {
…
}
By using the @Access(AccessType.FIELD) annotation, the Jakarta Persistence provider is informed that the data is stored within the object’s variables instead of being accessed through setter/getter methods.
Let’s add the instance variables:
@Entity
@Access( AccessType.FIELD )
public class Profile {
@Id (1)
@GeneratedValue(strategy = GenerationType.IDENTITY) (2)
private Long id;
private String nickname;
private LocalDate birthdate;
private short manelength;
private byte gender;
@Column(name = "attracted_to_gender") (3)
private Byte attractedToGender;
private String description;
private LocalDateTime lastseen;
}
Each entity bean class must have at least one public or protected parameterless constructor; the persistence provider will use reflection to automatically build the object later.
@Entity
@Access( AccessType.FIELD )
public class Profile {
…
protected Profile() { }
public Profile( String nickname, LocalDate birthdate,
int manelength, int gender,
Integer attractedToGender, String description,
LocalDateTime lastseen ) {
setNickname( nickname );
setBirthdate( birthdate );
setManelength( manelength );
setGender( gender );
setAttractedToGender( attractedToGender );
setDescription( description );
setLastseen( lastseen );
}
}
Parameterized constructors are handy when our code builds new profiles, so the ID is also missing because we don’t determine it.
The constructor calls setters and initializes the object. In addition, the client can later change the states with the setters and read the states with getters. The implementation of the setters/getters is as usual, and the following are as expected:
For ID, there is only a getter, but no setter.
For three persistent attributes, the setters/getters perform a type conversion:
public int getManelength() {
return manelength;
}
public void setManelength( int manelength ) {
this.manelength = (short) manelength;
}
public int getGender() {
return gender;
}
public void setGender( int gender ) {
this.gender = (byte) gender;
}
public @Nullable Integer getAttractedToGender() {
return attractedToGender == null ? null : attractedToGender.intValue();
}
public void setAttractedToGender( @Nullable Integer attractedToGender ) {
this.attractedToGender = attractedToGender == null ?
null : attractedToGender.byteValue();
}
This is because using the data types byte and Byte in Java can be somewhat cumbersome, whereas int and Integer are more user-friendly. Converting between data types can be helpful, and it could potentially enable the use of Optional* for nullable types. Instead of annotating with @Nullable Integer, it would be just as appropriate to use the API’s OptionalInt.
We’re almost done with the entity bean class. In the last step, we add methods from java.lang.Object:
@Override public boolean equals( Object o ) {
return o instanceof Profile profile
&& Objects.equals( nickname, profile.nickname );
}
@Override public int hashCode() {
return Objects.hashCode( nickname );
}
@Override public String toString() {
return "%s[id=%d]".formatted( getClass().getSimpleName(), getId() );
}
}
In the implementation of methods equals(…) and hashCode(), only the unique nickname or business key is used. The Objects methods can be helpful in cases where equals(…) or hashCode() uses the nickname early, even before initialization, and the instance variable is null.
When it comes to method toString(), it only accesses the ID to avoid unnecessary reloading and cycles. Cycles can occur when, for example, a profile has a photo and includes the toString representation, but the photo references back to the profile, creating an endless loop. To avoid such situations, only the ID is used in the toString() method.
Development environments such as IntelliJ help map tables to objects. For example, IntelliJ can automatically generate the entities from the database tables. However, this is often only a start, and manual work is required.
For IntelliJ, there is also a plugin called JPA Buddy (www.jpa-buddy.com) with useful features that aren’t yet integrated in IntelliJ. JPA Buddy helps in the following situations:
JPA Buddy also doesn’t require an Ultimate edition. In this section, we’ve introduced the first entity bean, with additional beans for photos and unicorns to follow.
Editor’s note: This post has been adapted from a section of the book Spring Boot 3 and Spring Framework 6 by Christian Ullenboom. Christian is an Oracle-certified Java programmer and has been a trainer and consultant for Java technologies and object-oriented analysis and design since 1997.
This post was originally published 4/2025.