| Lesson 7 | Jakarta Persistence Entities |
| Objective | Explain how Jakarta Persistence entities represent and manage durable domain data in Jakarta EE 11. |
Older enterprise Java courses used entity beans to represent persistent business information such as customers, bank accounts, and products. That component model is now historical. Entity beans have been removed from the Jakarta EE 11 Platform, and applications should not design new persistence layers around remote persistent components.
Jakarta Persistence 3.2 is the Jakarta EE 11 standard for persistence and object-relational mapping. Its central programming artifact is the entity: a lightweight Java domain object whose state can be stored in a relational database. An entity is not an Enterprise Bean and is not a remotely shared server object. CDI managed beans, Jakarta REST resources, servlets, and session beans can all call an application service that works with entities.
An entity class models durable application data. A Product entity, for example, can represent a row in a product table while presenting the application with fields and domain methods. The persistence provider maps entity state to database columns and coordinates loading and storing that state.
An entity is declared with @Entity or in mapping XML. Every entity has a primary key. A portable entity class is a top-level class or static inner class, has a public or protected no-argument constructor, and is not final. Its persistent fields or properties are also non-final. The location of mapping annotations determines whether the provider uses field access or property access, so an entity should use one access strategy consistently.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import java.math.BigDecimal;
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private BigDecimal price;
@Version
private long version;
protected Product() {
}
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
public void changePrice(BigDecimal price) {
if (price.signum() < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.price = price;
}
}
The @Id field holds the persistent identity. @GeneratedValue asks the provider to obtain a key using its configured generation strategy. The @Version field supports optimistic locking, which detects conflicting updates. The protected constructor is available to the persistence provider, while the public constructor and changePrice method express application rules.
Entity classes can contain associations and richer behavior, but they should remain focused on the domain. They do not need remote interfaces, network stubs, or container-specific base classes.
The value of an entity primary key identifies persistent data for EntityManager operations. Within one persistence context, a given persistent identity corresponds to one managed entity instance. This identity guarantee is local to that context. Loading product 42 in another request, transaction, or server can produce a different Java object representing the same database row.
A persistence context is a set of managed entity instances associated with an EntityManager. It acts as a unit of work between application objects and the database. The provider tracks changes made to managed entities and synchronizes those changes with the database, usually when the context is flushed as part of transaction completion.
This behavior is called dirty checking. Calling changePrice does not necessarily issue an immediate SQL statement. If the product is managed, the provider detects the changed field and writes it at the appropriate synchronization point. This allows several related changes to participate in one transaction.
An entity instance moves through lifecycle states according to how the application uses it:
| State | Meaning | Typical transition |
|---|---|---|
| New or transient | The object exists in memory but has no persistent identity managed by the current context. | Create it with a constructor, then call persist. |
| Managed | The object belongs to a persistence context, and tracked changes can be synchronized with the database. | Use find, run a query, or persist a new object. |
| Detached | The object still has entity identity, but its changes are no longer tracked by that persistence context. | The context closes, is cleared, or the object is detached. |
| Removed | The managed object is scheduled for deletion when the transaction is synchronized. | Call remove on a managed entity. |
EntityManager.persist makes a new entity managed and schedules its insertion. find retrieves an entity by primary key. Queries locate entities by other criteria. remove schedules a managed entity for deletion. merge copies state from a detached object into a managed instance and returns that managed instance; it does not reattach the argument itself. For ordinary request-based work, it is often clearer to load a managed entity and invoke a domain method than to treat merge as a universal update operation.
Persistence operations that change durable state need a transaction boundary. In a Jakarta EE runtime, an application service can define that boundary and receive a container-managed EntityManager. The following stateless session bean uses container-managed transactions. By default, its business methods execute with a required transaction context.
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.math.BigDecimal;
@Stateless
public class ProductService {
@PersistenceContext
private EntityManager entityManager;
public Product create(String name, BigDecimal price) {
Product product = new Product(name, price);
entityManager.persist(product);
return product;
}
public Product find(long id) {
return entityManager.find(Product.class, id);
}
public void changePrice(long id, BigDecimal price) {
Product product = entityManager.find(Product.class, id);
if (product == null) {
throw new IllegalArgumentException("Unknown product: " + id);
}
product.changePrice(price);
}
public void remove(long id) {
Product product = entityManager.find(Product.class, id);
if (product != null) {
entityManager.remove(product);
}
}
}
The service does not call an explicit update method after changePrice. The product returned by find is managed in the current persistence context, so dirty checking detects the new price. If the transaction commits successfully, the change is synchronized with the database. If the transaction rolls back, its database changes are not committed.
A Jakarta REST resource or UI controller can call this service, but the service should own the business operation and transaction boundary. Keeping persistence work behind an application service makes validation, authorization, error handling, and multi-entity updates easier to reason about.
Several users can work with the same product data at the same time, but they normally do so through different persistence contexts and different Java objects. They do not share one entity instance across client connections. The database transaction isolation level and the persistence provider's locking behavior determine how concurrent operations interact.
The @Version field enables optimistic locking. When two transactions read the same entity version and both attempt an update, the first commit advances the version. The second update can no longer match the expected version, so the provider reports an optimistic-lock conflict instead of silently overwriting the first change. The application can then ask the user to retry, reload current data, or resolve the conflict according to business rules.
Pessimistic locking is also available when an operation genuinely needs a database lock before making a decision. It can reduce concurrency and increase deadlock risk, so it should be chosen deliberately. Transactions and locking tools support consistency, but they do not automatically solve every business race. Rules such as inventory reservation still require a carefully designed operation and database constraints.
| Concern | Legacy entity bean | Jakarta Persistence entity |
|---|---|---|
| Role | Persistent server component in an older distributed-component model | Lightweight persistent domain object mapped to durable data |
| Component type | Enterprise component managed through the old entity-bean contract | Java class managed by a persistence provider when associated with a persistence context |
| Identity | Component identity tied to the old container model | Primary key declared with @Id or @EmbeddedId |
| Lifecycle manager | Enterprise component container | EntityManager and its persistence context |
| Access pattern | Could be exposed as a distributed persistent object | Used locally behind an application service; external APIs normally exchange DTOs |
| Persistence operations | Creation, finder, and removal contracts on component interfaces | persist, find, queries, remove, and merge |
| Transactions and concurrency | Container-centered component rules | Jakarta Transactions, database isolation, and optimistic or pessimistic locking |
| Jakarta EE 11 status | Removed from the platform | Jakarta Persistence 3.2 is the current platform persistence specification |
The modern model separates concerns. An entity represents data and domain behavior. An application service coordinates a use case and its transaction. A REST resource, servlet, or UI adapter handles the external interaction. This separation avoids making the persistent object itself a network endpoint.
Persistence means that committed business data is stored durably and can be loaded in a later transaction, including after an application restart. It does not mean that the same Java object remains alive. A process crash destroys the in-memory object and its persistence context; a later operation creates or loads another entity instance for the same database identity.
This differs from stateful-session-bean passivation, which temporarily preserves conversational component state, and from HTTP session replication or a distributed cache, which addresses web-session state. Service failover is an availability capability of the deployment architecture. None of these should be described as entity persistence.
A practical request path is: HTTP or UI layer, application service, EntityManager and persistence context, then database. The service loads and changes entities within a transaction. The external layer sends command data inward and returns an explicit response model or DTO.
Returning entity graphs directly from a remote API creates avoidable coupling between database mappings and the public contract. It can also trigger problems with lazy associations after the persistence context ends, expose fields unintentionally, and make serialization cycles difficult to control. DTOs let the API choose exactly what data crosses the boundary while entities remain focused on persistence and domain rules.
Jakarta Persistence 3.2 is part of the Jakarta EE 11 Platform, and a compatible full-platform runtime supplies the required integration with persistence and transaction services. Tomcat 11 is a Servlet container rather than a full Jakarta EE Platform implementation. A plain Tomcat deployment therefore needs an added persistence provider, a database driver, a configured data source, and suitable transaction support. An alternative is to deploy the application to a Jakarta EE 11 compatible runtime that supplies those platform services.
Jakarta Persistence entities are the modern way to model durable relational data in Jakarta EE 11. Each entity has persistent identity, and an EntityManager manages entity instances inside a persistence context. Application services define transaction boundaries, managed changes are synchronized through dirty checking, and version fields can detect conflicting updates. Entities are local domain objects, not shared remote components. Keeping entities behind service and DTO boundaries produces a clearer persistence design and a more stable external API. This architecture also keeps persistence decisions separate from client communication protocols.