Learn Computing from the Experts | The Rheinwerk Computing Blog

What Is the Jakarta Persistence Entity Bean?

Written by Rheinwerk Computing | Apr 4, 2025 1:00:00 PM

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.

  • Code-first approach: In software development, we start with the entity beans. The database is determined based on the OOP models. This approach is unusual in practice, as there are usually predefined databases with tables and schemas. In addition, performance on the database side would not necessarily be optimal.
  • Database-first approach: We start with the database and write (or automatically generate) the entity beans; the database modeling comes first. The disadvantage could be that the entity beans don’t feel like full-fledged objects, but they are just blunt data containers; this antipattern is also called anemic domain model (martinfowler.com/bliki/Anemic-DomainModel.html).

Neither approach is optimal. In practice, there are mixed forms that work well—the database schema is always in the foreground:

  • The entity beans try as best as possible to be “good objects,” and all the Jakarta Persistence possibilities for mapping the tables to objects are exhausted.
  • Without even trying to make the entity beans “good” objects, this method takes an almost automatic 1:1 mapping of the tables to the entity beans. However, these entity beans remain internally in the infrastructure layer and are mapped to “real domain objects” with the perfect data types—then no more technological annotations or details come through. If you want to achieve the best possible domain-driven design, you’ll go this route, but the effort is high, and the practical benefit is rather low.

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.

 

Develop an Entity Bean Class

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.

 

Data Type Mapping

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:

  • With id, there is the peculiarity that Long or long would fit depending on the use case. A wrapper Long can be null, a primitive long of course can’t, and 0 isn’t equal to null. In our program, Long will be necessary because there is a short time when the ID isn’t assigned, and that is when a Profile is built with new, and the database saves the row and writes back an ID. If we were to just read from the database, then we would always get finished and built Profile objects and the variable would always be assigned and never null. In this case, long would be fine. In addition, long would be fine if we assigned IDs ourselves, but we don’t do that.
  • birthdate is a date, and besides LocalDate, the older data types such as java.util.Date or java.sql.Date would also be conceivable. Fortunately, Jakarta Persistence has been able to handle the temporal data types such as LocalDate, LocalTime, and LocalDateTime without any problems for some time now.
  • The data type for gender is more challenging. On the database side, the column is defined as TINYINT, which corresponds to a byte/Byte on the Java side. However, there is an issue with TINYINT being an unsigned byte on the database side, which means it can range from 0 to 255. On the other hand, Java doesn’t have unsigned data types, and its byte values range from -128 to +127. Despite this disparity, because gender only involves small numbers, it still falls within the acceptable range.
  • lastseen is a TIMESTAMP, and LocalDateTime is a good choice. In principle, the data types java.util.Date and java.sql.Timestamp are also possible.

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.

 

O/R Metadata

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:

  • Annotations: This is the usual way.
  • XML file: An XML file in the META-INF directory with the file name xml contains all descriptions.

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:

  • Annotations for logical mapping: These annotations describe the object model, associations, and so on.
  • Annotations for the physical mapping: These annotations describe the database schema such as table name, index, and so on.

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;

}

  1. The primary key is called Id-Property and is annotated with the Jakarta annotation @Id. This is a logical annotation.
  2. Auto-generated keys get @GeneratedValue as well as a strategy.
  3. In many places, metadata for the physical mapping can be omitted because the Jakarta Persistence provider performs an implicit mapping. For example, if an entity class is called Profile, the Jakarta Persistence provider assumes that the table is also called Profile. It’s the same with persistent attributes: if a persistent attribute is called id, the Jakarta Persistence provider assumes that the column is called the same. In this context, it’s convenient that SQL is case-insensitive. If you want the name of the column to be different from the persistent attribute name, you use @Column. In the code, attractedToGender is mapped to the attracted_to_gender column. Spring Boot has a setting that does this automatically.

 

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:

  • String getNickname()
  • void setNickname( String nickname )
  • LocalDate getBirthdate()
  • void setBirthdate( LocalDate birthdate )
  • String getDescription()
  • void setDescription( String description )
  • LocalDateTime getLastseen()
  • void setLastseen( LocalDateTime lastseen )
  • Long getId()

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.

 

Tool Support

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:

  • During database migration
  • When generating repositories
  • When creating repository methods via the GUI

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.