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
- Entity Mapping
- Relationships
- JPQL and Criteria API
- Native Queries
- Transaction Management
- Performance Optimization
- Hibernate-Specific Features
- Best Practices
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.