Full Stack • Java • System Design • Cloud • AI Engineering

JPA2026-06-17

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

  1. Use JPA Standard APIs: Prefer EntityManager over Hibernate Session
  2. Transaction Management: Always use @Transactional for write operations
  3. Lazy Loading: Keep EntityManager open for lazy associations
  4. Batch Operations: Use flush() and clear() for large batches
  5. Query Optimization: Use JPQL for portability, native SQL for performance
  6. Spring Data JPA: Leverage repository pattern for cleaner code
  7. Pagination: Use Pageable for large result sets
  8. Connection Pooling: Configure proper pool size for production