JPA Complete Guide
Master JPA with visual diagrams covering EntityManager, persistence units, entity lifecycle, Spring Data JPA, and complete implementation examples
Java Persistence API (JPA) is a specification for Object-Relational Mapping (ORM) in Java. It provides a standard way to map Java objects to database tables and manage persistence operations. Understanding JPA architecture and components is essential for modern Java development.
JPA Architecture Overview
graph TB
A[Application Layer] --> B[JPA API]
B --> C[EntityManagerFactory]
C --> D[EntityManager]
D --> E[Persistence Context]
B --> F[JPA Provider]
F --> G[Hibernate]
F --> H[EclipseLink]
F --> I[OpenJPA]
D --> J[JPQL Queries]
D --> K[Criteria API]
D --> L[Native SQL]
E --> M[Entity Objects]
M --> N[Database]
style B fill:#4CAF50
style C fill:#FF9800
style D fill:#2196F3
style E fill:#9C27B0
Key Points:
- JPA API: Standard specification (javax.persistence.*)
- Provider: Implementation (Hibernate, EclipseLink, OpenJPA)
- EntityManagerFactory: Creates EntityManager instances, thread-safe
- EntityManager: Manages entity lifecycle, not thread-safe
- Persistence Context: Cache of managed entities
JPA Components Code
// 1. Entity - Domain object mapped to database table
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true)
private String email;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
// Getters and setters
}
// 2. EntityManagerFactory - Created from persistence unit
public class JPAExample {
public static void main(String[] args) {
// Create EntityManagerFactory from persistence.xml
EntityManagerFactory emf = Persistence
.createEntityManagerFactory("myPersistenceUnit");
// Create EntityManager
EntityManager em = emf.createEntityManager();
// Begin transaction
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// Create and persist entity
Customer customer = new Customer();
customer.setName("John Doe");
customer.setEmail("[email protected]");
customer.setCreatedAt(new Date());
em.persist(customer);
// Commit transaction
tx.commit();
} catch (Exception e) {
if (tx.isActive()) {
tx.rollback();
}
throw e;
} finally {
em.close();
}
emf.close();
}
}
Persistence Unit Configuration
flowchart TB
A[persistence.xml] --> B[Persistence Unit]
B --> C[Provider Configuration]
B --> D[Entity Classes]
B --> E[Database Properties]
C --> F[Hibernate]
D --> G[Customer.class]
D --> H[Order.class]
E --> I[JDBC URL]
E --> J[Credentials]
B --> K[EntityManagerFactory]
K --> L[EntityManager]
style A fill:#FF9800
style B fill:#4CAF50
style K fill:#2196F3
Key Points:
- persistence.xml: Configuration file in META-INF directory
- Persistence Unit: Named configuration for EntityManagerFactory
- Provider: JPA implementation (Hibernate, EclipseLink)
- Entity Classes: Domain objects to be managed
- Properties: Database connection, dialect, DDL settings
Persistence.xml Configuration
<!-- META-INF/persistence.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<!-- Persistence Unit Definition -->
<persistence-unit name="myPersistenceUnit" transaction-type="RESOURCE_LOCAL">
<!-- JPA Provider -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!-- Entity Classes -->
<class>com.example.model.Customer</class>
<class>com.example.model.Order</class>
<class>com.example.model.Product</class>
<!-- Exclude unlisted classes -->
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<!-- Properties -->
<properties>
<!-- Database Connection -->
<property name="javax.persistence.jdbc.driver"
value="com.mysql.cj.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url"
value="jdbc:mysql://localhost:3306/mydb"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="password"/>
<!-- Hibernate Properties -->
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQL8Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<!-- Connection Pool -->
<property name="hibernate.c3p0.min_size" value="5"/>
<property name="hibernate.c3p0.max_size" value="20"/>
<property name="hibernate.c3p0.timeout" value="300"/>
<!-- Cache -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
value="org.hibernate.cache.jcache.JCacheRegionFactory"/>
</properties>
</persistence-unit>
</persistence>
Spring Boot Configuration (No persistence.xml)
// Spring Boot auto-configuration replaces persistence.xml
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EnableTransactionManagement
public class JpaConfig {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.model");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "update");
properties.setProperty("hibernate.dialect",
"org.hibernate.dialect.MySQL8Dialect");
properties.setProperty("hibernate.show_sql", "true");
properties.setProperty("hibernate.format_sql", "true");
em.setJpaProperties(properties);
return em;
}
@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
// application.properties alternative
// spring.datasource.url=jdbc:mysql://localhost:3306/mydb
// spring.datasource.username=root
// spring.datasource.password=password
// spring.jpa.hibernate.ddl-auto=update
// spring.jpa.show-sql=true
// spring.jpa.properties.hibernate.format_sql=true
EntityManager Lifecycle
sequenceDiagram
participant App as Application
participant EMF as EntityManagerFactory
participant EM as EntityManager
participant PC as Persistence Context
participant DB as Database
App->>EMF: createEntityManagerFactory()
Note over EMF: Created once, thread-safe
App->>EMF: createEntityManager()
EMF->>EM: Create EntityManager
EM->>PC: Create Persistence Context
App->>EM: persist(entity)
EM->>PC: Add to context (managed)
App->>EM: flush()
PC->>DB: INSERT/UPDATE
App->>EM: find(id)
EM->>PC: Check cache
PC-->>EM: Return if cached
EM->>DB: SELECT if not cached
DB-->>PC: Store in cache
App->>EM: close()
Note over PC: Entities become detached
App->>EMF: close()
Key Points:
- EntityManagerFactory: Created once per persistence unit, expensive
- EntityManager: Created per request/transaction, lightweight
- Persistence Context: First-level cache, cleared when EM closed
- Managed Entities: Tracked by persistence context, changes auto-synced
- Detached Entities: No longer tracked, need merge() to reattach
EntityManager Operations
@Service
public class CustomerService {
@PersistenceContext
private EntityManager entityManager;
// CREATE - persist()
@Transactional
public Customer createCustomer(Customer customer) {
entityManager.persist(customer);
// Entity becomes managed, ID generated
return customer;
}
// READ - find()
@Transactional(readOnly = true)
public Customer findCustomer(Long id) {
// Returns managed entity or null
return entityManager.find(Customer.class, id);
}
// READ - getReference() - lazy loading
@Transactional(readOnly = true)
public Customer getCustomerReference(Long id) {
// Returns proxy, throws exception if not found
return entityManager.getReference(Customer.class, id);
}
// UPDATE - merge()
@Transactional
public Customer updateCustomer(Customer detachedCustomer) {
// Merge detached entity back to managed state
Customer managedCustomer = entityManager.merge(detachedCustomer);
return managedCustomer;
}
// UPDATE - automatic dirty checking
@Transactional
public void updateCustomerEmail(Long id, String newEmail) {
Customer customer = entityManager.find(Customer.class, id);
// Entity is managed, changes auto-detected
customer.setEmail(newEmail);
// No need to call update() - automatic on commit
}
// DELETE - remove()
@Transactional
public void deleteCustomer(Long id) {
Customer customer = entityManager.find(Customer.class, id);
if (customer != null) {
entityManager.remove(customer);
}
}
// JPQL Query
@Transactional(readOnly = true)
public List<Customer> findByName(String name) {
return entityManager.createQuery(
"SELECT c FROM Customer c WHERE c.name LIKE :name",
Customer.class)
.setParameter("name", "%" + name + "%")
.getResultList();
}
// Native SQL Query
@Transactional(readOnly = true)
public List<Customer> findActiveCustomers() {
return entityManager.createNativeQuery(
"SELECT * FROM customers WHERE active = true",
Customer.class)
.getResultList();
}
// Criteria API
@Transactional(readOnly = true)
public List<Customer> findCustomersByCriteria(String email) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> query = cb.createQuery(Customer.class);
Root<Customer> root = query.from(Customer.class);
query.select(root)
.where(cb.equal(root.get("email"), email));
return entityManager.createQuery(query).getResultList();
}
// Flush and Clear
@Transactional
public void batchInsert(List<Customer> customers) {
int batchSize = 50;
for (int i = 0; i < customers.size(); i++) {
entityManager.persist(customers.get(i));
if (i % batchSize == 0 && i > 0) {
// Flush to database and clear context
entityManager.flush();
entityManager.clear();
}
}
}
}
Spring Data JPA
flowchart TD
A[Repository] --> B[CrudRepository]
B --> C[PagingAndSortingRepository]
C --> D[JpaRepository]
B --> E["save()<br/>findById()<br/>findAll()<br/>delete()<br/>count()"]
C --> F["findAll(Pageable)<br/>findAll(Sort)"]
D --> G["flush()<br/>saveAndFlush()<br/>deleteInBatch()"]
D --> H[Query Methods]
H --> I["findByName()<br/>findByEmailAndActive()"]
D --> J["@Query Annotation"]
J --> K["JPQL Queries<br/>Native SQL"]
style D fill:#4CAF50
style H fill:#FF9800
style J fill:#2196F3
Key Points:
- CrudRepository: Basic CRUD operations (save, findById, delete, count)
- PagingAndSortingRepository: Adds pagination and sorting capabilities
- JpaRepository: Adds JPA-specific methods (flush, batch operations)
- Query Methods: Auto-generate queries from method names
- @Query: Custom JPQL or native SQL queries
Spring Data JPA Implementation
// 1. Entity
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private Boolean active;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
// Getters and setters
}
// 2. Repository Interface
public interface CustomerRepository extends JpaRepository<Customer, Long> {
// Query method - auto-generated from method name
List<Customer> findByName(String name);
List<Customer> findByActiveTrue();
List<Customer> findByEmailContaining(String emailPart);
List<Customer> findByNameAndActive(String name, Boolean active);
// Custom JPQL query
@Query("SELECT c FROM Customer c WHERE c.email = :email")
Optional<Customer> findByEmail(@Param("email") String email);
// Native SQL query
@Query(value = "SELECT * FROM customers WHERE created_at > :date",
nativeQuery = true)
List<Customer> findRecentCustomers(@Param("date") Date date);
// Pagination
Page<Customer> findByActive(Boolean active, Pageable pageable);
// Modifying query
@Modifying
@Query("UPDATE Customer c SET c.active = :active WHERE c.id = :id")
int updateActiveStatus(@Param("id") Long id,
@Param("active") Boolean active);
}
// 3. Service Layer
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
// Create
@Transactional
public Customer createCustomer(Customer customer) {
customer.setCreatedAt(new Date());
customer.setActive(true);
return customerRepository.save(customer);
}
// Read
public Optional<Customer> findById(Long id) {
return customerRepository.findById(id);
}
public List<Customer> findAll() {
return customerRepository.findAll();
}
// Update
@Transactional
public Customer updateCustomer(Long id, Customer updates) {
Customer customer = customerRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Customer not found"));
customer.setName(updates.getName());
customer.setEmail(updates.getEmail());
return customerRepository.save(customer);
}
// Delete
@Transactional
public void deleteCustomer(Long id) {
customerRepository.deleteById(id);
}
// Pagination
public Page<Customer> findActiveCustomers(int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdAt").descending());
return customerRepository.findByActive(true, pageable);
}
}
// 4. Controller
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
@PostMapping
public ResponseEntity<Customer> create(@RequestBody Customer customer) {
return ResponseEntity.ok(customerService.createCustomer(customer));
}
@GetMapping("/{id}")
public ResponseEntity<Customer> findById(@PathVariable Long id) {
return customerService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<Page<Customer>> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(
customerService.findActiveCustomers(page, size));
}
}
Best Practices
- Use JPA Standard APIs: Prefer EntityManager over Hibernate Session
- Transaction Management: Always use @Transactional for write operations
- Lazy Loading: Keep EntityManager open for lazy associations
- Batch Operations: Use flush() and clear() for large batches
- Query Optimization: Use JPQL for portability, native SQL for performance
- Spring Data JPA: Leverage repository pattern for cleaner code
- Pagination: Use Pageable for large result sets
- Connection Pooling: Configure proper pool size for production