1. java
  2. /enterprise
  3. /jpa-hibernate

JPA and Hibernate Object-Relational Mapping

JPA & Hibernate

JPA (Java Persistence API) is a specification for object-relational mapping (ORM) in Java, while Hibernate is the most popular JPA implementation. Together, they provide a powerful way to manage relational data in Java applications by mapping Java objects to database tables.

Table of Contents

JPA Fundamentals

Basic Configuration

<!-- persistence.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.1">
    <persistence-unit name="myPU">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <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"/>
            
            <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"/>
        </properties>
    </persistence-unit>
</persistence>

EntityManagerFactory and EntityManager

import javax.persistence.*;
import java.util.List;

public class JPAUtil {
    private static EntityManagerFactory emf;
    
    static {
        try {
            emf = Persistence.createEntityManagerFactory("myPU");
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static EntityManager getEntityManager() {
        return emf.createEntityManager();
    }
    
    public static void close() {
        if (emf != null) {
            emf.close();
        }
    }
}

// Basic CRUD operations
public class UserDAO {
    
    public void save(User user) {
        EntityManager em = JPAUtil.getEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        try {
            tx.begin();
            em.persist(user);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw e;
        } finally {
            em.close();
        }
    }
    
    public User findById(Long id) {
        EntityManager em = JPAUtil.getEntityManager();
        try {
            return em.find(User.class, id);
        } finally {
            em.close();
        }
    }
    
    public List<User> findAll() {
        EntityManager em = JPAUtil.getEntityManager();
        try {
            return em.createQuery("SELECT u FROM User u", User.class)
                    .getResultList();
        } finally {
            em.close();
        }
    }
    
    public void update(User user) {
        EntityManager em = JPAUtil.getEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        try {
            tx.begin();
            em.merge(user);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw e;
        } finally {
            em.close();
        }
    }
    
    public void delete(Long id) {
        EntityManager em = JPAUtil.getEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        try {
            tx.begin();
            User user = em.find(User.class, id);
            if (user != null) {
                em.remove(user);
            }
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw e;
        } finally {
            em.close();
        }
    }
}

Entity Mapping

Basic Entity

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "username", unique = true, nullable = false, length = 50)
    private String username;
    
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    
    @Column(name = "password", nullable = false)
    private String password;
    
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status")
    private UserStatus status;
    
    @Lob
    @Column(name = "profile_picture")
    private byte[] profilePicture;
    
    // Constructors
    public User() {}
    
    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.createdAt = LocalDateTime.now();
        this.status = UserStatus.ACTIVE;
    }
    
    // Lifecycle callbacks
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    // ... other getters and setters
}

enum UserStatus {
    ACTIVE, INACTIVE, SUSPENDED, DELETED
}

Advanced Mapping

@Entity
@Table(name = "products", 
       indexes = {
           @Index(name = "idx_product_name", columnList = "name"),
           @Index(name = "idx_product_category", columnList = "category_id")
       })
@NamedQueries({
    @NamedQuery(name = "Product.findByCategory", 
                query = "SELECT p FROM Product p WHERE p.category = :category"),
    @NamedQuery(name = "Product.findByPriceRange", 
                query = "SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice")
})
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name", nullable = false)
    private String name;
    
    @Column(name = "description", columnDefinition = "TEXT")
    private String description;
    
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    
    @Column(name = "stock_quantity")
    private Integer stockQuantity;
    
    @Column(name = "sku", unique = true)
    private String sku;
    
    @Embedded
    private ProductDimensions dimensions;
    
    @ElementCollection
    @CollectionTable(name = "product_tags", 
                    joinColumns = @JoinColumn(name = "product_id"))
    @Column(name = "tag")
    private Set<String> tags = new HashSet<>();
    
    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_images",
                    joinColumns = @JoinColumn(name = "product_id"))
    @MapKeyColumn(name = "image_type")
    @Column(name = "image_url")
    private Map<String, String> images = new HashMap<>();
    
    // Constructors, getters, setters...
}

@Embeddable
public class ProductDimensions {
    
    @Column(name = "length")
    private Double length;
    
    @Column(name = "width")
    private Double width;
    
    @Column(name = "height")
    private Double height;
    
    @Column(name = "weight")
    private Double weight;
    
    // Constructors, getters, setters...
}

Relationships

One-to-One

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "username")
    private String username;
    
    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private UserProfile profile;
    
    // Constructors, getters, setters...
}

@Entity
@Table(name = "user_profiles")
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "bio")
    private String bio;
    
    @Column(name = "phone_number")
    private String phoneNumber;
    
    @Column(name = "date_of_birth")
    private LocalDate dateOfBirth;
    
    @OneToOne
    @JoinColumn(name = "user_id", unique = true)
    private User user;
    
    // Constructors, getters, setters...
}

One-to-Many / Many-to-One

@Entity
@Table(name = "categories")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Product> products = new ArrayList<>();
    
    // Helper methods
    public void addProduct(Product product) {
        products.add(product);
        product.setCategory(this);
    }
    
    public void removeProduct(Product product) {
        products.remove(product);
        product.setCategory(null);
    }
    
    // Constructors, getters, setters...
}

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
    
    // Constructors, getters, setters...
}

Many-to-Many

@Entity
@Table(name = "students")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    @ManyToMany
    @JoinTable(
        name = "student_courses",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
    
    // Helper methods
    public void enrollInCourse(Course course) {
        courses.add(course);
        course.getStudents().add(this);
    }
    
    public void dropCourse(Course course) {
        courses.remove(course);
        course.getStudents().remove(this);
    }
    
    // Constructors, getters, setters...
}

@Entity
@Table(name = "courses")
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    @Column(name = "credits")
    private Integer credits;
    
    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
    
    // Constructors, getters, setters...
}

// Many-to-Many with additional attributes
@Entity
@Table(name = "enrollments")
public class Enrollment {
    @EmbeddedId
    private EnrollmentId id;
    
    @ManyToOne
    @MapsId("studentId")
    @JoinColumn(name = "student_id")
    private Student student;
    
    @ManyToOne
    @MapsId("courseId")
    @JoinColumn(name = "course_id")
    private Course course;
    
    @Column(name = "enrollment_date")
    private LocalDate enrollmentDate;
    
    @Column(name = "grade")
    private String grade;
    
    // Constructors, getters, setters...
}

@Embeddable
public class EnrollmentId implements Serializable {
    @Column(name = "student_id")
    private Long studentId;
    
    @Column(name = "course_id")
    private Long courseId;
    
    // Constructors, getters, setters, equals, hashCode...
}

JPQL and Criteria API

JPQL Queries

public class ProductRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // Basic JPQL queries
    public List<Product> findByName(String name) {
        return em.createQuery(
            "SELECT p FROM Product p WHERE p.name = :name", Product.class)
            .setParameter("name", name)
            .getResultList();
    }
    
    public List<Product> findByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        return em.createQuery(
            "SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice", 
            Product.class)
            .setParameter("minPrice", minPrice)
            .setParameter("maxPrice", maxPrice)
            .getResultList();
    }
    
    // Join queries
    public List<Product> findByCategoryName(String categoryName) {
        return em.createQuery(
            "SELECT p FROM Product p JOIN p.category c WHERE c.name = :categoryName", 
            Product.class)
            .setParameter("categoryName", categoryName)
            .getResultList();
    }
    
    // Aggregation queries
    public Long countProductsByCategory(String categoryName) {
        return em.createQuery(
            "SELECT COUNT(p) FROM Product p JOIN p.category c WHERE c.name = :categoryName", 
            Long.class)
            .setParameter("categoryName", categoryName)
            .getSingleResult();
    }
    
    public BigDecimal getAveragePrice() {
        return em.createQuery(
            "SELECT AVG(p.price) FROM Product p", BigDecimal.class)
            .getSingleResult();
    }
    
    // Complex queries with subqueries
    public List<Product> findExpensiveProducts() {
        return em.createQuery(
            "SELECT p FROM Product p WHERE p.price > " +
            "(SELECT AVG(p2.price) FROM Product p2)", 
            Product.class)
            .getResultList();
    }
    
    // Pagination
    public List<Product> findAllPaginated(int page, int pageSize) {
        return em.createQuery("SELECT p FROM Product p ORDER BY p.name", Product.class)
            .setFirstResult(page * pageSize)
            .setMaxResults(pageSize)
            .getResultList();
    }
    
    // Named queries
    public List<Product> findByCategory(Category category) {
        return em.createNamedQuery("Product.findByCategory", Product.class)
            .setParameter("category", category)
            .getResultList();
    }
    
    // Update and delete queries
    public int updatePrices(BigDecimal percentage) {
        return em.createQuery(
            "UPDATE Product p SET p.price = p.price * :percentage")
            .setParameter("percentage", percentage)
            .executeUpdate();
    }
    
    public int deleteOutOfStockProducts() {
        return em.createQuery(
            "DELETE FROM Product p WHERE p.stockQuantity = 0")
            .executeUpdate();
    }
}

Criteria API

import javax.persistence.criteria.*;

public class ProductCriteriaRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    public List<Product> findWithCriteria(String name, BigDecimal minPrice, 
                                         BigDecimal maxPrice, String categoryName) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Product> cq = cb.createQuery(Product.class);
        Root<Product> product = cq.from(Product.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        // Add name filter if provided
        if (name != null && !name.isEmpty()) {
            predicates.add(cb.like(cb.lower(product.get("name")), 
                                 "%" + name.toLowerCase() + "%"));
        }
        
        // Add price range filter
        if (minPrice != null) {
            predicates.add(cb.greaterThanOrEqualTo(product.get("price"), minPrice));
        }
        if (maxPrice != null) {
            predicates.add(cb.lessThanOrEqualTo(product.get("price"), maxPrice));
        }
        
        // Add category filter
        if (categoryName != null && !categoryName.isEmpty()) {
            Join<Product, Category> categoryJoin = product.join("category");
            predicates.add(cb.equal(categoryJoin.get("name"), categoryName));
        }
        
        // Combine all predicates with AND
        cq.where(cb.and(predicates.toArray(new Predicate[0])));
        
        // Add ordering
        cq.orderBy(cb.asc(product.get("name")));
        
        return em.createQuery(cq).getResultList();
    }
    
    // Count query with criteria
    public Long countWithCriteria(String name, BigDecimal minPrice, BigDecimal maxPrice) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Long> cq = cb.createQuery(Long.class);
        Root<Product> product = cq.from(Product.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        if (name != null && !name.isEmpty()) {
            predicates.add(cb.like(cb.lower(product.get("name")), 
                                 "%" + name.toLowerCase() + "%"));
        }
        
        if (minPrice != null) {
            predicates.add(cb.greaterThanOrEqualTo(product.get("price"), minPrice));
        }
        
        if (maxPrice != null) {
            predicates.add(cb.lessThanOrEqualTo(product.get("price"), maxPrice));
        }
        
        cq.select(cb.count(product));
        cq.where(cb.and(predicates.toArray(new Predicate[0])));
        
        return em.createQuery(cq).getSingleResult();
    }
    
    // Complex aggregation with criteria
    public List<Object[]> getCategorySummary() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
        Root<Product> product = cq.from(Product.class);
        Join<Product, Category> category = product.join("category");
        
        cq.multiselect(
            category.get("name"),
            cb.count(product),
            cb.avg(product.get("price")),
            cb.sum(product.get("stockQuantity"))
        );
        
        cq.groupBy(category.get("name"));
        cq.orderBy(cb.desc(cb.count(product)));
        
        return em.createQuery(cq).getResultList();
    }
}

Native Queries

public class ProductNativeRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // Simple native query
    @SuppressWarnings("unchecked")
    public List<Product> findByNameNative(String name) {
        return em.createNativeQuery(
            "SELECT * FROM products WHERE name = ?", Product.class)
            .setParameter(1, name)
            .getResultList();
    }
    
    // Named native query
    @SuppressWarnings("unchecked")
    public List<Object[]> getProductStatistics() {
        return em.createNativeQuery(
            "SELECT c.name as category_name, " +
            "       COUNT(p.id) as product_count, " +
            "       AVG(p.price) as avg_price, " +
            "       SUM(p.stock_quantity) as total_stock " +
            "FROM products p " +
            "JOIN categories c ON p.category_id = c.id " +
            "GROUP BY c.id, c.name " +
            "ORDER BY product_count DESC")
            .getResultList();
    }
    
    // Native query with result set mapping
    @SqlResultSetMapping(
        name = "ProductSummaryMapping",
        classes = @ConstructorResult(
            targetClass = ProductSummary.class,
            columns = {
                @ColumnResult(name = "id"),
                @ColumnResult(name = "name"),
                @ColumnResult(name = "price"),
                @ColumnResult(name = "category_name")
            }
        )
    )
    public List<ProductSummary> getProductSummaries() {
        return em.createNativeQuery(
            "SELECT p.id, p.name, p.price, c.name as category_name " +
            "FROM products p " +
            "JOIN categories c ON p.category_id = c.id",
            "ProductSummaryMapping")
            .getResultList();
    }
    
    // Stored procedure call
    public void updateProductPrices(BigDecimal percentage) {
        em.createNativeQuery("CALL update_product_prices(?)")
            .setParameter(1, percentage)
            .executeUpdate();
    }
}

// DTO for native query results
public class ProductSummary {
    private Long id;
    private String name;
    private BigDecimal price;
    private String categoryName;
    
    public ProductSummary(Long id, String name, BigDecimal price, String categoryName) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.categoryName = categoryName;
    }
    
    // Getters and setters...
}

Transaction Management

Programmatic Transactions

public class OrderService {
    
    @PersistenceContext
    private EntityManager em;
    
    public void createOrder(Order order) {
        EntityTransaction tx = em.getTransaction();
        
        try {
            tx.begin();
            
            // Validate order
            validateOrder(order);
            
            // Update product stock
            for (OrderItem item : order.getItems()) {
                Product product = em.find(Product.class, item.getProductId());
                if (product.getStockQuantity() < item.getQuantity()) {
                    throw new InsufficientStockException(
                        "Not enough stock for product: " + product.getName());
                }
                product.setStockQuantity(product.getStockQuantity() - item.getQuantity());
                em.merge(product);
            }
            
            // Save order
            em.persist(order);
            
            // Send notification
            sendOrderConfirmation(order);
            
            tx.commit();
            
        } catch (Exception e) {
            if (tx.isActive()) {
                tx.rollback();
            }
            throw e;
        }
    }
    
    private void validateOrder(Order order) {
        if (order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order must contain at least one item");
        }
        
        if (order.getCustomer() == null) {
            throw new IllegalArgumentException("Order must have a customer");
        }
    }
    
    private void sendOrderConfirmation(Order order) {
        // Implementation for sending confirmation
    }
}

Declarative Transactions (with Spring)

import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;

@Service
@Transactional
public class UserService {
    
    @PersistenceContext
    private EntityManager em;
    
    // Default transaction settings
    public User createUser(User user) {
        em.persist(user);
        return user;
    }
    
    // Read-only transaction
    @Transactional(readOnly = true)
    public User findById(Long id) {
        return em.find(User.class, id);
    }
    
    // Custom transaction settings
    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.READ_COMMITTED,
        timeout = 30,
        rollbackFor = Exception.class
    )
    public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {
        User fromUser = em.find(User.class, fromUserId);
        User toUser = em.find(User.class, toUserId);
        
        if (fromUser.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException("Insufficient balance");
        }
        
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        toUser.setBalance(toUser.getBalance().add(amount));
        
        em.merge(fromUser);
        em.merge(toUser);
        
        // Log transaction
        auditService.logTransaction(fromUserId, toUserId, amount);
    }
    
    // Transaction with manual rollback
    @Transactional
    public void processPayment(Payment payment) {
        try {
            // Process payment logic
            em.persist(payment);
            
            // Call external payment service
            PaymentResult result = paymentGateway.processPayment(payment);
            
            if (!result.isSuccessful()) {
                // Manual rollback
                throw new PaymentProcessingException("Payment failed: " + result.getErrorMessage());
            }
            
            payment.setStatus(PaymentStatus.COMPLETED);
            em.merge(payment);
            
        } catch (Exception e) {
            // Transaction will be rolled back automatically
            throw new PaymentProcessingException("Payment processing failed", e);
        }
    }
}

Performance Optimization

Lazy Loading and Fetch Strategies

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // Lazy loading (default for collections)
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
    
    // Eager loading for frequently accessed data
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // Lazy loading with join fetch in queries
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shipping_address_id")
    private Address shippingAddress;
}

// Repository with optimized queries
public class OrderRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // Join fetch to avoid N+1 problem
    public List<Order> findOrdersWithItems() {
        return em.createQuery(
            "SELECT DISTINCT o FROM Order o " +
            "LEFT JOIN FETCH o.items " +
            "LEFT JOIN FETCH o.customer", 
            Order.class)
            .getResultList();
    }
    
    // Multiple join fetches
    public Order findOrderWithAllData(Long orderId) {
        return em.createQuery(
            "SELECT o FROM Order o " +
            "LEFT JOIN FETCH o.items i " +
            "LEFT JOIN FETCH i.product " +
            "LEFT JOIN FETCH o.customer " +
            "LEFT JOIN FETCH o.shippingAddress " +
            "WHERE o.id = :orderId", 
            Order.class)
            .setParameter("orderId", orderId)
            .getSingleResult();
    }
    
    // Batch fetching
    public List<Order> findOrdersWithBatchedItems(List<Long> orderIds) {
        List<Order> orders = em.createQuery(
            "SELECT o FROM Order o WHERE o.id IN :orderIds", Order.class)
            .setParameter("orderIds", orderIds)
            .getResultList();
        
        // Batch load items
        em.createQuery(
            "SELECT i FROM OrderItem i " +
            "JOIN FETCH i.product " +
            "WHERE i.order.id IN :orderIds")
            .setParameter("orderIds", orderIds)
            .getResultList();
        
        return orders;
    }
}

Caching

// Entity-level caching
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {
    // Entity implementation
}

// Query result caching
public class ProductRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    public List<Product> findByCategory(String categoryName) {
        return em.createQuery(
            "SELECT p FROM Product p JOIN p.category c WHERE c.name = :categoryName", 
            Product.class)
            .setParameter("categoryName", categoryName)
            .setHint("org.hibernate.cacheable", true)
            .getResultList();
    }
}

// Second-level cache configuration
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setCacheNames(Arrays.asList("products", "categories", "users"));
        return cacheManager;
    }
}

Batch Operations

public class BatchOperationService {
    
    @PersistenceContext
    private EntityManager em;
    
    // Batch insert
    @Transactional
    public void batchInsertProducts(List<Product> products) {
        int batchSize = 20;
        
        for (int i = 0; i < products.size(); i++) {
            em.persist(products.get(i));
            
            if (i % batchSize == 0 && i > 0) {
                em.flush();
                em.clear();
            }
        }
    }
    
    // Batch update
    @Transactional
    public void batchUpdatePrices(Map<Long, BigDecimal> priceUpdates) {
        int batchSize = 20;
        int count = 0;
        
        for (Map.Entry<Long, BigDecimal> entry : priceUpdates.entrySet()) {
            Product product = em.find(Product.class, entry.getKey());
            product.setPrice(entry.getValue());
            
            if (++count % batchSize == 0) {
                em.flush();
                em.clear();
            }
        }
    }
    
    // Bulk operations
    @Transactional
    public int bulkUpdateProductPrices(String categoryName, BigDecimal multiplier) {
        return em.createQuery(
            "UPDATE Product p SET p.price = p.price * :multiplier " +
            "WHERE p.category.name = :categoryName")
            .setParameter("multiplier", multiplier)
            .setParameter("categoryName", categoryName)
            .executeUpdate();
    }
}

Hibernate-Specific Features

Custom Types

// Custom Hibernate type
public class JsonType implements UserType {
    
    @Override
    public int[] sqlTypes() {
        return new int[]{Types.JAVA_OBJECT};
    }
    
    @Override
    public Class returnedClass() {
        return Map.class;
    }
    
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, 
                             SharedSessionContractImplementor session, 
                             Object owner) throws SQLException {
        String jsonString = rs.getString(names[0]);
        if (jsonString == null) {
            return null;
        }
        
        try {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(jsonString, Map.class);
        } catch (Exception e) {
            throw new SQLException("Could not parse JSON", e);
        }
    }
    
    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index,
                           SharedSessionContractImplementor session) throws SQLException {
        if (value == null) {
            st.setNull(index, Types.JAVA_OBJECT);
        } else {
            try {
                ObjectMapper mapper = new ObjectMapper();
                st.setString(index, mapper.writeValueAsString(value));
            } catch (Exception e) {
                throw new SQLException("Could not serialize object to JSON", e);
            }
        }
    }
    
    // Other required methods...
}

// Using custom type
@Entity
public class Product {
    @Id
    private Long id;
    
    @Type(type = "com.example.JsonType")
    @Column(name = "metadata", columnDefinition = "TEXT")
    private Map<String, Object> metadata;
    
    // Other fields...
}

Interceptors and Events

// Hibernate interceptor
public class AuditInterceptor implements Interceptor {
    
    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, 
                         String[] propertyNames, Type[] types) {
        if (entity instanceof Auditable) {
            Auditable auditable = (Auditable) entity;
            auditable.setCreatedAt(LocalDateTime.now());
            auditable.setCreatedBy(getCurrentUser());
            
            // Update state array
            setValue(state, propertyNames, "createdAt", auditable.getCreatedAt());
            setValue(state, propertyNames, "createdBy", auditable.getCreatedBy());
        }
        return false;
    }
    
    @Override
    public boolean onFlushDirty(Object entity, Serializable id, 
                               Object[] currentState, Object[] previousState,
                               String[] propertyNames, Type[] types) {
        if (entity instanceof Auditable) {
            Auditable auditable = (Auditable) entity;
            auditable.setUpdatedAt(LocalDateTime.now());
            auditable.setUpdatedBy(getCurrentUser());
            
            setValue(currentState, propertyNames, "updatedAt", auditable.getUpdatedAt());
            setValue(currentState, propertyNames, "updatedBy", auditable.getUpdatedBy());
        }
        return false;
    }
    
    private void setValue(Object[] state, String[] propertyNames, 
                         String propertyName, Object value) {
        for (int i = 0; i < propertyNames.length; i++) {
            if (propertyName.equals(propertyNames[i])) {
                state[i] = value;
                break;
            }
        }
    }
    
    private String getCurrentUser() {
        // Get current user from security context
        return "current_user";
    }
}

// Event listeners
@Component
public class ProductEventListener {
    
    @EventListener
    public void handleProductCreated(ProductCreatedEvent event) {
        Product product = event.getProduct();
        // Handle product creation
        sendNotification("Product created: " + product.getName());
    }
    
    @EventListener
    public void handleProductUpdated(ProductUpdatedEvent event) {
        Product product = event.getProduct();
        // Handle product update
        updateSearchIndex(product);
    }
    
    private void sendNotification(String message) {
        // Send notification implementation
    }
    
    private void updateSearchIndex(Product product) {
        // Update search index implementation
    }
}

Best Practices

1. Entity Design

// Good entity design
@Entity
@Table(name = "products")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // Always override equals and hashCode for entities
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Product)) return false;
        Product product = (Product) o;
        return Objects.equals(id, product.id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
    
    // Use proper field validation
    @Column(name = "name", nullable = false, length = 255)
    @NotBlank(message = "Product name is required")
    @Size(max = 255, message = "Product name must not exceed 255 characters")
    private String name;
    
    // Use appropriate precision for monetary values
    @Column(name = "price", precision = 19, scale = 2)
    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.0", inclusive = false, message = "Price must be positive")
    private BigDecimal price;
    
    // Initialize collections
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, 
               orphanRemoval = true, fetch = FetchType.LAZY)
    private List<ProductImage> images = new ArrayList<>();
    
    // Helper methods for bidirectional relationships
    public void addImage(ProductImage image) {
        images.add(image);
        image.setProduct(this);
    }
    
    public void removeImage(ProductImage image) {
        images.remove(image);
        image.setProduct(null);
    }
}

2. Query Optimization

public class OptimizedProductRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // Use projections for read-only data
    public List<ProductSummaryDTO> findProductSummaries() {
        return em.createQuery(
            "SELECT new com.example.dto.ProductSummaryDTO(" +
            "p.id, p.name, p.price, c.name) " +
            "FROM Product p JOIN p.category c", 
            ProductSummaryDTO.class)
            .getResultList();
    }
    
    // Use pagination for large result sets
    public Page<Product> findProducts(Pageable pageable) {
        String countQuery = "SELECT COUNT(p) FROM Product p";
        String dataQuery = "SELECT p FROM Product p ORDER BY p.name";
        
        Long total = em.createQuery(countQuery, Long.class).getSingleResult();
        
        List<Product> content = em.createQuery(dataQuery, Product.class)
            .setFirstResult((int) pageable.getOffset())
            .setMaxResults(pageable.getPageSize())
            .getResultList();
        
        return new PageImpl<>(content, pageable, total);
    }
    
    // Use specific queries instead of loading entire entities
    public void updateProductPrice(Long productId, BigDecimal newPrice) {
        em.createQuery(
            "UPDATE Product p SET p.price = :price WHERE p.id = :id")
            .setParameter("price", newPrice)
            .setParameter("id", productId)
            .executeUpdate();
    }
    
    // Avoid N+1 queries with join fetch
    public List<Product> findProductsWithCategories() {
        return em.createQuery(
            "SELECT DISTINCT p FROM Product p " +
            "LEFT JOIN FETCH p.category " +
            "ORDER BY p.name", 
            Product.class)
            .getResultList();
    }
}

3. Transaction Management

@Service
@Transactional
public class OrderService {
    
    // Keep transactions short and focused
    @Transactional(readOnly = true)
    public Order findOrderById(Long id) {
        return orderRepository.findById(id);
    }
    
    // Use appropriate transaction boundaries
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // Validate input
        validateOrderRequest(request);
        
        // Create order
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setOrderDate(LocalDateTime.now());
        order.setStatus(OrderStatus.PENDING);
        
        // Add items
        for (OrderItemRequest itemRequest : request.getItems()) {
            OrderItem item = createOrderItem(itemRequest);
            order.addItem(item);
        }
        
        // Save order
        return orderRepository.save(order);
    }
    
    // Handle exceptions properly
    @Transactional(rollbackFor = Exception.class)
    public void processPayment(Long orderId, PaymentRequest paymentRequest) {
        try {
            Order order = orderRepository.findById(orderId);
            
            // Process payment
            PaymentResult result = paymentService.processPayment(paymentRequest);
            
            if (result.isSuccessful()) {
                order.setStatus(OrderStatus.PAID);
                order.setPaymentId(result.getPaymentId());
            } else {
                throw new PaymentException("Payment failed: " + result.getErrorMessage());
            }
            
        } catch (Exception e) {
            // Log error
            logger.error("Payment processing failed for order: " + orderId, e);
            throw e; // Transaction will be rolled back
        }
    }
}

Summary

JPA and Hibernate provide powerful ORM capabilities for Java applications:

Key Benefits:

  • Object-relational mapping eliminates boilerplate SQL code
  • Database independence through abstraction layer
  • Built-in caching and performance optimization
  • Rich query capabilities with JPQL and Criteria API

Best Practices:

  • Design entities with proper equals/hashCode implementations
  • Use appropriate fetch strategies and avoid N+1 queries
  • Implement efficient pagination for large datasets
  • Keep transactions focused and handle exceptions properly
  • Use projections and bulk operations for performance

Performance Considerations:

  • Configure second-level cache appropriately
  • Use batch operations for bulk data modifications
  • Optimize queries with join fetches
  • Monitor and analyze SQL generation

JPA/Hibernate remains essential for enterprise Java development, providing a robust foundation for data access while abstracting database-specific details and enabling developers to work with familiar object-oriented concepts.