90 KiB
90 KiB
Spring Data JPA - Reference Documentation
Repository Interfaces
Repository Hierarchy
Spring Data JPA provides a clear inheritance hierarchy for repositories:
Repository (marker interface)
├── CrudRepository (basic CRUD operations)
│ ├── PagingAndSortingRepository (add pagination/sorting)
│ │ └── JpaRepository (JPA-specific operations)
│ └── ListCrudRepository (Spring Data 3+)
└── Reactive variants (ReactiveCrudRepository, etc.)
CrudRepository
Basic CRUD operations for all entities.
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
// CREATE/UPDATE
<S extends T> S save(S entity); // Save or update entity
<S extends T> Iterable<S> saveAll(Iterable<S> entities); // Save multiple entities
// READ
Optional<T> findById(ID id); // Find by primary key
T getById(ID id); // Returns proxy, never null
T getReferenceById(ID id); // Like getById
boolean existsById(ID id); // Check existence
Iterable<T> findAll(); // Find all entities
Iterable<T> findAllById(Iterable<ID> ids); // Find by multiple IDs
// AGGREGATE
long count(); // Count entities
// DELETE
void deleteById(ID id); // Delete by ID
void delete(T entity); // Delete entity
void deleteAllById(Iterable<? extends ID> ids); // Delete by multiple IDs
void deleteAll(Iterable<? extends T> entities); // Delete multiple entities
void deleteAll(); // Delete all entities
}
PagingAndSortingRepository
Extends CrudRepository with pagination and sorting capabilities.
public interface PagingAndSortingRepository<T, ID extends Serializable>
extends CrudRepository<T, ID> {
// Sorting
Iterable<T> findAll(Sort sort); // Find all with sorting
// Pagination
Page<T> findAll(Pageable pageable); // Find all with pagination
}
JpaRepository
The most comprehensive interface extending PagingAndSortingRepository with JPA-specific operations.
public interface JpaRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID> {
// Enhanced read operations
List<T> findAll(); // Returns List instead of Iterable
List<T> findAllById(Iterable<ID> ids); // Returns List instead of Iterable
List<T> findAll(Sort sort); // Returns List instead of Iterable
Page<T> findAll(Pageable pageable); // Returns Page
// Batch operations
<S extends T> List<S> saveAll(Iterable<S> entities); // Save multiple with return
// Batch delete operations
void deleteInBatch(Iterable<T> entities); // Delete without flushing
void deleteAllInBatch(Iterable<ID> ids); // Delete by IDs without flushing
void deleteAllInBatch(); // Delete all without flushing
// Flush operations
void flush(); // Flush to database
<S extends T> S saveAndFlush(S entity); // Save and immediately flush
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities); // Save all and flush
}
Query Methods
Derived Query Methods
Spring Data automatically generates queries from method names following naming conventions.
Simple Lookups
Optional<User> findByEmail(String email); // Single result
List<User> findByUsername(String username); // Multiple results
User findFirstByEmail(String email); // First result
User findTopByOrderByAgeDesc(); // Top by age descending
Conditional Operators
// Equality
List<User> findByStatus(String status);
List<User> findByStatusNot(String status);
// Comparison
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeLessThanEqual(Integer age);
List<User> findByAgeBetween(Integer min, Integer max);
List<User> findByAgeGreaterThanEqual(25); // Static comparison
// Null/Empty checks
List<User> findByEmailIsNull();
List<User> findByEmailIsNotNull();
List<User> findByEmailNotEmpty();
// Boolean properties
List<User> findByActiveTrue();
List<User>ByEmailActiveFalse();
String Operations
// Pattern matching
List<User> findByEmailContaining(String pattern); // LIKE '%pattern%'
List<User> findByEmailStartsWith(String prefix); // LIKE 'prefix%'
List<User> findByEmailEndsWith(String suffix); // LIKE '%suffix'
List<User> findByEmailLike(String pattern); // Exact LIKE pattern
// Case sensitivity
List<User> findByEmailIgnoreCase(String email);
Date and Time Operations
// After/Before
List<Order> findByOrderDateAfter(LocalDate date);
List<Order> findByCreatedDateBefore(LocalDateTime dateTime);
// Between
List<Order> findByOrderDateBetween(LocalDate start, LocalDate end);
// Range queries
List<Order> findByTotalPriceGreaterThan(BigDecimal min);
List<Order> findByTotalPriceBetween(BigDecimal min, BigDecimal max);
// Current date comparisons
List<Order> findByCreatedDateBefore(LocalDateTime.now().minusDays(7));
Ordering
// Simple ordering
List<User> findByStatusOrderByCreatedDateDesc(String status);
List<User> findAllByOrderByLastNameAsc();
// Multiple sort criteria
List<Product> findByCategoryOrderByPriceAscNameDesc(String category);
// Dynamic sorting
List<User> findAll(Sort.by("lastName").ascending());
List<User> findByActiveTrue(Sort.by("createdDate").descending());
Pagination Integration
Page<User> findByStatus(String status, Pageable pageable);
Slice<User> findByActiveTrue(Pageable pageable); // No total count
List<User> findTop10ByOrderByCreatedDateDesc(); // Fixed limit
Delete Operations
// Delete with return count
long deleteByEmail(String email);
long deleteByStatusAndAge(String status, Integer age);
// Delete entities
void deleteByStatus(String status); // Bulk delete
Custom Queries with @Query
JPQL Queries
Use Java Persistence Query Language for complex queries.
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Basic query with named parameters
@Query("SELECT o FROM Order o WHERE o.status = :status AND o.totalPrice > :minPrice")
List<Order> findActiveOrdersAbovePrice(
@Param("status") String status,
@Param("minPrice") BigDecimal minPrice
);
// Query with JOIN FETCH to avoid N+1 problem
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.customerId = :customerId")
List<Order> findOrdersWithItems(@Param("customerId") Long customerId);
// Aggregate function
@Query("SELECT COUNT(o) FROM Order o WHERE o.status = 'COMPLETED'")
long countCompletedOrders();
// IN clause
@Query("SELECT o FROM Order o WHERE o.status IN :statuses")
List<Order> findByStatuses(@Param("statuses") List<String> statuses);
// EXISTS clause
@Query("SELECT o FROM Order o WHERE EXISTS (SELECT 1 FROM o.items i WHERE i.quantity = 0)")
List<Order> findOrdersWithZeroQuantityItems();
}
Native SQL Queries
Use native SQL for database-specific queries.
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Simple native query
@Query(value = "SELECT * FROM products WHERE category = :category AND price < :maxPrice",
nativeQuery = true)
List<Product> findProductsByCategory(
@Param("category") String category,
@Param("maxPrice") BigDecimal maxPrice
);
// Native query with mapping
@Query(value = """
SELECT p.id, p.name, p.price, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.price > :minPrice
ORDER BY p.price DESC
""", nativeQuery = true)
@QueryResults projection = ProductSummary.class; // Custom projection
List<ProductSummary> findExpensiveProductSummaries(@Param("minPrice") BigDecimal minPrice);
}
Modifying Queries
Use @Modifying for INSERT, UPDATE, DELETE operations.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Transactional
@Query("UPDATE User u SET u.lastLoginDate = :now WHERE u.id = :userId")
void updateLastLoginDate(
@Param("userId") Long userId,
@Param("now") LocalDateTime now
);
@Modifying
@Transactional
@Query("DELETE FROM User u WHERE u.createdDate < :cutoffDate")
int deleteInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
@Modifying
@Transactional
@Query(value = "UPDATE users SET status = 'INACTIVE' WHERE last_login < :cutoff",
nativeQuery = true)
int deactivateInactiveUsersNative(@Param("cutoff") LocalDateTime cutoff);
}
Entity Relationships
One-to-One Relationship
Foreign Key Approach
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String email;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
}
@Entity
@Table(name = "addresses")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String street;
private String city;
private String postalCode;
@OneToOne(mappedBy = "address")
private User user;
}
Shared Primary Key Approach
@Entity
@Table(name = "employees")
public class Employee {
@Id
private Long id; // Shared with profile
@Column(nullable = false)
private String firstName;
@OneToOne(mappedBy = "employee", fetch = FetchType.LAZY)
private EmployeeProfile profile;
}
@Entity
@Table(name = "employee_profiles")
public class EmployeeProfile {
@Id
private Long id; // Same as employee ID
@Column(length = 500)
private String bio;
@OneToOne(fetch = FetchType.LAZY)
@MapsId // Maps to employee.id
@JoinColumn(name = "id")
private Employee employee;
}
One-to-Many Relationship
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@OneToMany(mappedBy = "category",
cascade = CascadeType.ALL,
orphanRemoval = true,
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);
}
}
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
}
Many-to-Many Relationship
Simple Join Table
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public void addCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
}
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}
Join Table with Additional Attributes
@Entity
@Table(name = "enrollments")
public class Enrollment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "student_id", nullable = false)
private Student student;
@ManyToOne
@JoinColumn(name = "course_id", nullable = false)
private Course course;
@Column(nullable = false)
private LocalDateTime enrolledAt;
@Column(precision = 3, scale = 2)
private Double grade; // Can be null
@Column(length = 20)
private String status; // ACTIVE, WITHDRAWN, COMPLETED
// Composite primary key (alternative approach)
@EmbeddedId
private EnrollmentId enrollmentId;
@ManyToOne
@JoinColumn(name = "student_id", insertable = false, updatable = false)
private Student student;
@ManyToOne
@JoinColumn(name = "course_id", insertable = false, updatable = false)
private Course course;
}
@Embeddable
public class EnrollmentId implements Serializable {
private Long studentId;
private Long courseId;
// equals(), hashCode() implementation
}
Bidirectional Relationships
Best Practices
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// Helper method for consistency
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "department_id", nullable = false)
private Department department;
}
Pagination and Sorting
Pagination Basics
@Service
public class ProductService {
private final ProductRepository repository;
public Page<Product> getProductsPage(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return repository.findAll(pageable);
}
public Page<Product> getProductsWithSorting(int page, int size, String sortField) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortField));
return repository.findAll(pageable);
}
public Page<Product> getProductsWithMultiSort(int page, int size) {
Sort sort = Sort.by("price").ascending()
.and(Sort.by("name").ascending());
Pageable pageable = PageRequest.of(page, size, sort);
return repository.findAll(pageable);
}
}
Advanced Pagination
@Service
public class OrderService {
private final OrderRepository repository;
public Page<Order> getOrdersByStatus(String status, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdDate").descending());
return repository.findByStatus(status, pageable);
}
public Slice<Order> getRecentOrders(int page, int size) {
// Slice doesn't count total elements (more efficient for large datasets)
Pageable pageable = PageRequest.of(page, size);
return repository.findByStatus("NEW", pageable);
}
public Stream<Order> streamAllOrders() {
// Stream for large datasets
return repository.streamAllBy();
}
}
Custom Pagination Queries
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.active = true")
Page<User> findActiveUsers(Pageable pageable);
@Query(value = "SELECT * FROM users WHERE created_at > :date",
nativeQuery = true)
Page<User> findUsersCreatedAfter(@Param("date") LocalDateTime date, Pageable pageable);
}
Database Auditing
Spring Data JPA Auditing
Configuration
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class)
public class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName)
.or(() -> Optional.of("system"));
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}
Auditing Entities
@Entity
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(nullable = false, updatable = false, length = 50)
private String createdBy;
@LastModifiedBy
@Column(nullable = false, length = 50)
private String lastModifiedBy;
@Version
private Long version; // Optimistic locking
}
@Entity
public class Product extends BaseEntity {
@Column(nullable = false, length = 255)
private String name;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
private ProductStatus status;
}
public enum ProductStatus {
ACTIVE, INACTIVE, DISCONTINUED
}
Custom Auditor Provider
@Component
public class CustomAuditorProvider implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Try to get from security context first
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return Optional.of(authentication.getName());
}
// Fallback to system user or throw exception
return Optional.of("system");
}
}
JPA Lifecycle Callbacks
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 20)
private String status;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (status == null) {
status = "PENDING";
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
@PreRemove
protected void onRemove() {
// Cleanup logic before deletion
}
@PostLoad
protected void onLoad() {
// Post-load processing
}
}
Hibernate Envers for Auditing
Configuration
@Configuration
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRepositoryFactoryBean.class)
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class EnversConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
Audited Entities
@Entity
@Audited
@RevisionEntity(EntityRevisionListener.class)
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Enumerated(EnumType.STRING)
private DocumentStatus status;
@ManyToOne
@JoinColumn(name = "author_id", nullable = false)
private User author;
}
@RevisionEntity
public class EntityRevisionListener implements RevisionListener {
@Override
public void newRevision(Object revisionEntity) {
EntityRevision revision = (EntityRevision) revisionEntity;
revision.setRevisionDate(LocalDateTime.now());
revision.setUsername(SecurityContextHolder.getContext()
.getAuthentication().getName());
}
}
@Entity
public class EntityRevision {
@Id
@GeneratedValue
private Integer id;
private LocalDateTime revisionDate;
@Column(length = 50)
private String username;
}
Repository Usage
public interface DocumentRepository extends JpaRepository<Document, Long>,
JpaEntityRepository<Document, Long, Integer> {
@Query("SELECT d FROM Document d WHERE d.id = :id AND d.revisionNumber <= :revision")
Document findHistoricalVersion(@Param("id") Long id, @Param("revision") Integer revision);
List<Number> findRevisions(Long documentId);
<T> T findRevision(Class<T> entityClass, Number revision);
}
@Service
public class DocumentAuditService {
private final DocumentRepository documentRepository;
private final AuditReader auditReader;
public List<DocumentRevision> getDocumentHistory(Long documentId) {
List<Number> revisions = auditReader.getRevisions(Document.class, documentId);
return revisions.stream()
.map(revision -> new DocumentRevision(
revision,
auditReader.find(Document.class, documentId, revision),
auditReader.getRevisionDateForRevision(revision)
))
.collect(Collectors.toList());
}
}
Transactions and Deletion
Transaction Management
Transaction Configuration
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
private final InventoryService inventoryService;
@Transactional
public Order createOrder(Order order, List<Payment> payments) {
// Start transaction
Order savedOrder = orderRepository.save(order);
// Process payments
payments.forEach(payment -> {
payment.setOrderId(savedOrder.getId());
paymentRepository.save(payment);
});
// Update inventory
inventoryService.reserveItems(savedOrder.getItems());
return savedOrder;
}
@Transactional(readOnly = true)
public Order getOrderWithDetails(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("Order not found: " + orderId));
}
@Transactional(rollbackFor = {PaymentException.class, InventoryException.class})
public void processPayment(Long orderId) throws PaymentException {
Order order = getOrderWithDetails(orderId);
// Process payment logic
paymentService.process(order.getPayments());
// Update order status
order.setStatus("PROCESSING");
orderRepository.save(order);
// This will trigger rollback if thrown
if (paymentFailed) {
throw new PaymentException("Payment processing failed");
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderCreation(Order order) {
// Always creates new transaction
auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
}
}
Propagation Types
@Service
public class TransactionalService {
@Transactional
public void methodA() {
methodB(); // Runs in same transaction
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// Runs in new transaction
}
@Transactional(propagation = Propagation.NESTED)
public void methodC() {
// Runs in nested transaction (savepoint)
}
@Transactional(propagation = Propagation.SUPPORTS)
public void methodD() {
// Runs in existing transaction or none
}
}
Isolation Levels
@Service
public class OrderService {
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrderWithConsistentData(Long orderId) {
// Read committed isolation - prevents dirty reads
return orderRepository.findById(orderId).orElseThrow();
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processInventoryUpdate(List<InventoryItem> items) {
// Serializable isolation - prevents all concurrency issues
items.forEach(inventoryService::updateStock);
}
@Transactional(timeout = 30) // 30 seconds timeout
public void processLongRunningTask() {
// Long-running operation with timeout
externalApiService.callExternalSystem();
}
}
Delete Operations
Repository Delete Methods
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Basic delete operations
void deleteById(Long id);
void delete(Order entity);
void deleteAllById(Iterable<Long> ids);
void deleteAll(Iterable<Order> entities);
void deleteAll();
// Derived delete queries
long deleteByStatus(String status);
long deleteByCreatedDateBefore(LocalDateTime date);
long deleteByTotalPriceLessThan(BigDecimal threshold);
// Custom delete query
@Modifying
@Transactional
@Query("DELETE FROM Order o WHERE o.status = :status AND o.totalPrice < :minPrice")
int deleteOldPendingOrders(
@Param("status") String status,
@Param("minPrice") BigDecimal minPrice
);
}
Service Layer Delete Operations
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OrderItemRepository orderItemRepository;
@Transactional
public void deleteOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// Delete related items first (or use cascade)
orderItemRepository.deleteByOrderId(orderId);
// Delete order
orderRepository.delete(order);
}
@Transactional
public long cleanupExpiredOrders(LocalDateTime cutoffDate) {
return orderRepository.deleteByCreatedDateBefore(cutoffDate);
}
@Transactional
public int cleanupOldPendingOrders(BigDecimal minPrice) {
return orderRepository.deleteOldPendingOrders("PENDING", minPrice);
}
@Transactional
public void batchDeleteOrders(List<Long> orderIds) {
// Batch delete for better performance
orderRepository.deleteAllById(orderIds);
}
}
Cascade and Orphan Removal
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@OneToMany(mappedBy = "category",
cascade = CascadeType.ALL, // Cascade save/update/delete
orphanRemoval = true) // Remove children when removed from collection
private List<Product> products = new ArrayList<>();
@OneToMany(mappedBy = "category",
cascade = CascadeType.PERSIST, // Only cascade save operations
orphanRemoval = false)
private List<Product> inactiveProducts = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
product.setCategory(this);
}
public void removeProduct(Product product) {
products.remove(product);
product.setCategory(null); // Triggers orphan removal
}
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
// Bidirectional relationship management
public void setCategory(Category category) {
if (this.category != null) {
this.category.removeProduct(this);
}
this.category = category;
if (category != null) {
category.addProduct(this);
}
}
}
Batch Operations
@Service
public class BatchOrderService {
private final OrderRepository orderRepository;
@Transactional
public void batchUpdateOrders(List<OrderUpdate> updates) {
// Use batch processing for large updates
int batchSize = 50;
for (int i = 0; i < updates.size(); i++) {
OrderUpdate update = updates.get(i);
Order order = orderRepository.findById(update.orderId())
.orElseThrow();
order.setStatus(update.status());
order.setNotes(update.notes());
if (i % batchSize == 0) {
orderRepository.flush(); // Flush periodically
orderRepository.clear(); // Clear persistence context
}
}
}
@Transactional
public void batchDeleteOrdersByStatus(List<String> statuses) {
// Batch delete by status
statuses.forEach(status ->
orderRepository.deleteByStatus(status));
}
}
UUID as Primary Key
Modern Approach (Hibernate 6.2+ with JPA 3.1)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(length = 50)
private String username;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreatedDate
private LocalDateTime createdDate;
}
@Entity
@Table(name = "sessions")
public class UserSession {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "user_id", nullable = false)
private UUID userId;
@Column(nullable = false, unique = true)
private String token;
@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt;
}
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
List<User> findByStatus(UserStatus status);
}
@Service
public class UserService {
private final UserRepository repository;
public User createUser(CreateUserRequest request) {
if (repository.findByEmail(request.email()).isPresent()) {
throw new EmailAlreadyExistsException(request.email());
}
User user = new User();
user.setEmail(request.email());
user.setUsername(request.username());
user.setStatus(UserStatus.ACTIVE);
return repository.save(user);
}
public User getUser(UUID id) {
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
Hibernate-Specific UUID Generation
@Entity
@Table(name = "orders")
public class Order {
@Id
@UuidGenerator(style = UuidGenerator.Style.TIME) // Version 1: Time-based
private UUID id;
@Column(nullable = false, length = 50, unique = true)
private String orderNumber;
@Column(precision = 12, scale = 2)
private BigDecimal totalAmount;
@CreatedDate
private LocalDateTime createdAt;
}
@Entity
@Table(name = "products")
public class Product {
@Id
@UuidGenerator(style = UuidGenerator.Style.RANDOM) // Version 4: Random
private UUID id;
@Column(nullable = false, length = 255)
private String name;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "sku", length = 50, unique = true)
private String sku;
}
@Entity
@Table(name = "events")
public class SystemEvent {
@Id
@UuidGenerator // Default: RANDOM (Version 4)
private UUID id;
@Column(nullable = false, length = 50)
private String eventType;
@Column(columnDefinition = "TEXT")
private String eventData;
@CreatedDate
private LocalDateTime timestamp;
}
UUID Storage Options
@Entity
@Table(name = "transactions")
public class Transaction {
@Id
@UuidGenerator
@Column(columnDefinition = "VARCHAR(36)") // Store as string for some databases
private String id;
@Column(precision = 19, scale = 4) // High precision for financial data
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private TransactionStatus status;
@Column(name = "reference_id", length = 36)
private String referenceId; // Secondary UUID field
}
@Entity
@Table(name = "audit_logs")
public class AuditLog {
@Id
@UuidGenerator
private UUID id; // Native UUID type (PostgreSQL, etc.)
@Column(name = "entity_id", length = 36) // May need VARCHAR for MySQL
private String entityId; // Foreign key as string for compatibility
@Column(nullable = false, length = 100)
private String action;
@Column(columnDefinition = "TEXT")
private String changes;
@CreatedDate
private LocalDateTime timestamp;
}
UUID vs Sequential ID Comparison
// UUID Entity: Better for distributed systems
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id; // Good for: microservices, distributed DBs, offline-first
@Column(nullable = false, length = 200)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne
private User author;
}
// Sequential Entity: Better for single database
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Good for: single database, better index performance
@Column(nullable = false, columnDefinition = "TEXT")
private String text;
@ManyToOne
private Article article;
}
// Hybrid approach: Best of both worlds
@Entity
@Table(name = "blogs")
public class Blog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id; // Unique identifier
@Column(unique = true, name = "slug")
private String slug; // URL-friendly, sequential-like identifier
@Column(nullable = false, length = 200)
private String title;
@ManyToOne
private User owner;
}
Performance Considerations
@Service
public class UserService {
private final UserRepository repository;
// Batch operations with UUIDs
@Transactional
public List<User> batchCreateUsers(List<CreateUserRequest> requests) {
List<User> users = requests.stream()
.map(request -> {
User user = new User();
user.setEmail(request.email());
user.setUsername(request.username());
return user;
})
.collect(Collectors.toList());
return repository.saveAll(users);
}
// Index optimization for UUID queries
@Transactional(readOnly = true)
public Page<User> findUsersByEmailPattern(String pattern, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return repository.findByEmailContaining(pattern, pageable);
}
// Cache UUID lookups
@Cacheable(value = "users", key = "#id")
public User getUser(UUID id) {
return repository.findById(id).orElseThrow();
}
}
Database Indexing
Basic Index Definitions
@Entity
@Table(
name = "products",
indexes = {
@Index(name = "idx_product_name", columnList = "name"),
@Index(name = "idx_product_category", columnList = "category_id")
}
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "created_date")
private LocalDateTime createdDate;
}
@Entity
@Table(
name = "users",
indexes = {
// Unique index for email
@Index(name = "idx_user_email_unique", columnList = "email", unique = true),
// Index for username (unique)
@Index(name = "idx_user_username", columnList = "username", unique = true),
// Index for status filtering
@Index(name = "idx_user_status", columnList = "status")
}
)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(length = 50, unique = true)
private String username;
@Enumerated(EnumType.STRING)
private UserStatus status;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
Composite Indexes
@Entity
@Table(
name = "orders",
indexes = {
// Single column indexes
@Index(name = "idx_order_status", columnList = "status"),
@Index(name = "idx_order_customer", columnList = "customer_id"),
// Composite index for common query pattern
@Index(name = "idx_status_customer_created",
columnList = "status, customer_id, created_at DESC"),
// Another composite index
@Index(name = "idx_order_created_status",
columnList = "created_at DESC, status"),
// Index for reporting queries
@Index(name = "idx_order_report",
columnList = "status, total_amount, created_at")
}
)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20)
private String status; // PENDING, PROCESSING, COMPLETED, CANCELLED
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(precision = 12, scale = 2)
private BigDecimal totalAmount;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Enumerated(EnumType.STRING)
private OrderType type; // RETAIL, WHOLESALE, B2B
}
@Entity
@Table(
name = "order_items",
indexes = {
// Index for finding items by order
@Index(name = "idx_order_item_order", columnList = "order_id"),
// Composite index for order and product queries
@Index(name = "idx_order_item_order_product",
columnList = "order_id, product_id"),
// Index for price-based queries
@Index(name = "idx_order_item_price",
columnList = "unit_price DESC, quantity"),
// Index for inventory sync
@Index(name = "idx_order_item_product_created",
columnList = "product_id, created_at DESC")
}
)
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private Integer quantity;
@Column(precision = 10, scale = 2, name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
Index Sorting and Uniqueness
@Entity
@Table(
name = "products",
indexes = {
// Ascending indexes (default)
@Index(name = "idx_product_price_asc", columnList = "price ASC"),
@Index(name = "idx_product_name_asc", columnList = "name ASC"),
// Descending indexes for "newest first" queries
@Index(name = "idx_product_created_desc",
columnList = "created_at DESC"),
// Unique indexes
@Index(name = "idx_product_sku_unique",
columnList = "sku", unique = true),
@Index(name = "idx_product_code_unique",
columnList = "product_code", unique = true),
// Multi-column unique index
@Index(
name = "idx_category_code_unique",
columnList = "category_id, product_code",
unique = true
),
// Mixed sort order
@Index(
name = "idx_category_price",
columnList = "category_id ASC, price DESC"
),
// Index for search
@Index(
name = "idx_product_search",
columnList = "name, description"
)
}
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, length = 50)
private String sku;
@Column(nullable = false, length = 100)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "created_at")
private LocalDateTime createdAt;
@ManyToOne
private Category category;
@Column(name = "product_code", length = 20)
private String productCode;
}
Indexes for Search and Filtering
@Entity
@Table(
name = "articles",
indexes = {
// Full-text search indexes
@Index(name = "idx_article_title", columnList = "title"),
@Index(name = "idx_article_content", columnList = "content"),
@Index(name = "idx_article_author", columnList = "author_id"),
// Indexes for filtering and sorting
@Index(name = "idx_article_status_date",
columnList = "status, published_at DESC"),
@Index(name = "idx_author_date",
columnList = "author_id, published_at DESC"),
@Index(name = "idx_category_date",
columnList = "category_id, published_at DESC"),
// Range query indexes
@Index(name = "idx_article_published_range",
columnList = "published_at DESC"),
@Index(name = "idx_article_created_range",
columnList = "created_at DESC"),
// Pagination optimization
@Index(name = "idx_article_id_date",
columnList = "id, published_at DESC"),
// Status-specific indexes
@Index(name = "idx_article_published",
columnList = "status = 'PUBLISHED'"),
@Index(name = "idx_article_draft",
columnList = "status = 'DRAFT'")
}
)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Enumerated(EnumType.STRING)
private ArticleStatus status; // DRAFT, PUBLISHED, ARCHIVED
@Column(name = "published_at")
private LocalDateTime publishedDate;
@Column(name = "created_at")
private LocalDateTime createdDate;
@ManyToOne
private User author;
@ManyToOne
private Category category;
@Column(length = 100)
private String slug; // URL-friendly identifier
@Column(length = 50)
private String excerpt;
}
@Repository
public interface ArticleRepository extends JpaRepository<Article, Long> {
// Uses idx_article_status_date
Page<Article> findByStatusOrderByPublishedDateDesc(
String status,
Pageable pageable
);
// Uses idx_author_date
Page<Article> findByAuthorOrderByPublishedDateDesc(
User author,
Pageable pageable
);
// Uses idx_article_published_range
List<Article> findByPublishedDateAfterOrderByPublishedDateDesc(
LocalDateTime date
);
// Custom query that benefits from multiple indexes
@Query("""
SELECT a FROM Article a
WHERE a.status = :status
AND a.publishedDate BETWEEN :start AND :end
AND a.category = :category
ORDER BY a.publishedDate DESC
""")
Page<Article> findPublishedInCategoryAndDateRange(
@Param("status") String status,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("category") Category category,
Pageable pageable
);
}
@Service
@Transactional(readOnly = true)
public class ArticleSearchService {
private final ArticleRepository repository;
public Page<Article> getPublishedArticles(int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("publishedDate").descending());
return repository.findByStatusOrderByPublishedDateDesc("PUBLISHED", pageable);
}
public Page<Article> getArticlesByAuthor(User author, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("publishedDate").descending());
return repository.findByAuthorOrderByPublishedDateDesc(author, pageable);
}
public List<Article> getRecentArticles(int limit) {
return repository.findByPublishedDateAfterOrderByPublishedDateDesc(
LocalDateTime.now().minusMonths(1)
).stream().limit(limit).collect(Collectors.toList());
}
}
Indexes on Relationships
@Entity
@Table(
name = "comments",
indexes = {
// Index for finding comments by article (most common query)
@Index(name = "idx_comment_article", columnList = "article_id"),
// Composite index for recent comments by article
@Index(name = "idx_comment_article_created",
columnList = "article_id, created_at DESC"),
// Index for approved comments filtering
@Index(name = "idx_comment_article_approved",
columnList = "article_id, approved"),
// Index for finding user comments
@Index(name = "idx_comment_user", columnList = "user_id"),
// Index for moderation
@Index(name = "idx_comment_status_created",
columnList = "status, created_at DESC"),
// Index for spam detection
@Index(name = "idx_comment_user_article",
columnList = "user_id, article_id"),
// Index for sentiment analysis
@Index(name = "idx_comment_sentiment",
columnList = "sentiment_score")
}
)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id", nullable = false)
private Article article;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(columnDefinition = "TEXT")
private String text;
@Column(name = "created_at")
private LocalDateTime createdDate;
private boolean approved;
@Column(name = "approved_at")
private LocalDateTime approvedDate;
@Enumerated(EnumType.STRING)
private CommentStatus status; // PENDING, APPROVED, REJECTED, SPAM
private Integer sentimentScore; // For sentiment analysis
}
@Entity
@Table(
name = "categories",
indexes = {
// Basic index
@Index(name = "idx_category_name", columnList = "name"),
// Composite index for product queries
@Index(name = "idx_category_products_count",
columnList = "parent_id, product_count DESC"),
// Index for hierarchical navigation
@Index(name = "idx_category_hierarchy",
columnList = "parent_id, sort_order"),
// Index for search
@Index(name = "idx_category_search",
columnList = "name, description, slug"),
// Index for statistics
@Index(name = "idx_category_stats",
columnList = "is_active, product_count, created_at")
}
)
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(length = 200)
private String description;
@Column(name = "slug", length = 100, unique = true)
private String slug;
@ManyToOne
@JoinColumn(name = "parent_id")
private Category parent;
private Integer sortOrder;
private boolean isActive;
@Column(name = "product_count")
private Long productCount;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
// Uses idx_comment_article
List<Comment> findByArticle(Article article);
// Uses idx_comment_article_created
List<Comment> findByArticleOrderByCreatedDateDesc(Article article);
// Uses idx_comment_article_approved
long countByArticleAndApproved(Article article, boolean approved);
// Uses idx_comment_user
List<Comment> findByUser(User user);
// Uses idx_comment_status_created
List<Comment> findByStatusOrderByCreatedDateDesc(CommentStatus status);
// Multiple indexes considered by query optimizer
@Query("""
SELECT c FROM Comment c
WHERE c.article = :article
AND c.approved = true
ORDER BY c.createdDate DESC
""")
List<Comment> findApprovedCommentsByArticle(
@Param("article") Article article,
Pageable pageable
);
}
Index Best Practices
@Entity
@Table(
name = "products",
indexes = {
// Rule 1: Index on columns used in WHERE clauses
@Index(name = "idx_product_status", columnList = "status"),
// Rule 2: Index columns used in JOIN conditions
@Index(name = "idx_product_category", columnList = "category_id"),
// Rule 3: Create composite indexes for common multi-column queries
@Index(name = "idx_category_status",
columnList = "category_id, status"),
// Rule 4: Include sorting columns in composite indexes
@Index(name = "idx_status_created",
columnList = "status, created_at DESC"),
// Rule 5: Avoid over-indexing - only index what's frequently queried
@Index(name = "idx_product_popularity",
columnList = "view_count, sales_count DESC"),
// Rule 6: Use partial indexes for filtered queries
@Index(name = "idx_active_products",
columnList = "status = 'ACTIVE'"),
// Rule 7: Consider covering indexes for common query patterns
@Index(name = "idx_product_covering",
columnList = "category_id, name, price")
}
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 255)
private String name;
@Enumerated(EnumType.STRING)
private ProductStatus status;
@ManyToOne
private Category category;
private Double viewCount;
private Double salesCount;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
// Example queries that benefit from proper indexing
@Service
public class ProductService {
private final ProductRepository repository;
// Benefits from idx_category_status
public List<Product> getActiveProductsByCategory(Category category) {
return repository.findByCategoryAndStatus(category, "ACTIVE");
}
// Benefits from idx_status_created
public Page<Product> getRecentActiveProducts(int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdAt").descending());
return repository.findByStatus("ACTIVE", pageable);
}
// Benefits from idx_product_popularity
public List<Product> getPopularProducts(int limit) {
return repository.findTop10ByOrderBySalesCountDesc()
.stream()
.limit(limit)
.collect(Collectors.toList());
}
}
Multiple Database Configuration
Basic Configuration for Multiple Databases
Primary Database Configuration
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.users.repository",
entityManagerFactoryRef = "usersEntityManager",
transactionManagerRef = "usersTransactionManager"
)
@PropertySource("classpath:application-users.properties")
public class UsersDbConfig {
@Bean
@ConfigurationProperties(prefix = "users.datasource")
public DataSource usersDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean usersEntityManager(
DataSource usersDataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(usersDataSource);
em.setPackagesToScan("com.example.users.model");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(hibernateProperties());
return em;
}
@Bean
public PlatformTransactionManager usersTransactionManager(
@Qualifier("usersEntityManager")
LocalContainerEntityManagerFactoryBean usersEntityManager) {
return new JpaTransactionManager(usersEntityManager.getObject());
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect",
"org.hibernate.dialect.MySQL8Dialect");
properties.setProperty("hibernate.hbm2ddl.auto", "validate");
properties.setProperty("hibernate.show_sql", "false");
properties.setProperty("hibernate.format_sql", "true");
return properties;
}
}
Secondary Database Configuration
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.products.repository",
entityManagerFactoryRef = "productsEntityManager",
transactionManagerRef = "productsTransactionManager"
)
@PropertySource("classpath:application-products.properties")
public class ProductsDbConfig {
@Bean
@ConfigurationProperties(prefix = "products.datasource")
public DataSource productsDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean productsEntityManager(
DataSource productsDataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(productsDataSource);
em.setPackagesToScan("com.example.products.model");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(hibernateProperties());
return em;
}
@Bean
public PlatformTransactionManager productsTransactionManager(
@Qualifier("productsEntityManager")
LocalContainerEntityManagerFactoryBean productsEntityManager) {
return new JpaTransactionManager(productsEntityManager.getObject());
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.dialect",
"org.hibernate.dialect.PostgreSQLDialect");
properties.setProperty("hibernate.hbm2ddl.auto", "update");
properties.setProperty("hibernate.show_sql", "false");
return properties;
}
}
Properties Configuration
# users.properties
users.datasource.url=jdbc:mysql://localhost:3306/users_db
users.datasource.username=root
users.datasource.password=password
users.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# products.properties
products.datasource.url=jdbc:postgresql://localhost:5432/products_db
products.datasource.username=postgres
products.datasource.password=postgres
products.datasource.driver-class-name=org.postgresql.Driver
Entity Configuration for Multiple Databases
// Users database entities
@Entity
@Table(name = "users", schema = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(length = 50, unique = true)
private String username;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@Entity
@Table(name = "user_profiles", schema = "users")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@Column(length = 100)
private String firstName;
@Column(length = 100)
private String lastName;
@Column(length = 20)
private String phoneNumber;
@Enumerated(EnumType.STRING)
private LanguagePreference language;
}
// Products database entities
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@ManyToOne
private Category category;
private Boolean isActive;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@ManyToOne
private Category parent;
private Integer sortOrder;
private Boolean isActive;
}
Repository Configuration
// Users repositories
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
List<User> findByStatus(UserStatus status);
}
@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
Optional<UserProfile> findByUserId(Long userId);
}
// Products repositories
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(Category category);
List<Product> findByIsActiveTrue();
Page<Product> findByIsActiveTrue(Pageable pageable);
}
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByParentIsNull();
List<Category> findByParent(Category parent);
List<Category> findByIsActiveTrueOrderBySortOrderAsc();
}
Service Layer Configuration
@Service
@Transactional("usersTransactionManager")
public class UserService {
private final UserRepository userRepository;
private final UserProfileRepository userProfileRepository;
public UserService(UserRepository userRepository,
UserProfileRepository userProfileRepository) {
this.userRepository = userRepository;
this.userProfileRepository = userProfileRepository;
}
@Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
public User createUser(UserCreateRequest request) {
User user = new User();
user.setEmail(request.email());
user.setUsername(request.username());
user.setStatus(UserStatus.ACTIVE);
User savedUser = userRepository.save(user);
UserProfile profile = new UserProfile();
profile.setUser(savedUser);
profile.setFirstName(request.firstName());
profile.setLastName(request.lastName());
profile.setLanguage(request.language());
userProfileRepository.save(profile);
return savedUser;
}
@Transactional(value = "usersTransactionManager", readOnly = true)
public Optional<User> getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
@Transactional(value = "usersTransactionManager")
public void updateUserProfile(Long userId, ProfileUpdateRequest request) {
UserProfile profile = userProfileRepository.findByUserId(userId)
.orElseThrow(() -> new UserProfileNotFoundException(userId));
profile.setFirstName(request.firstName());
profile.setLastName(request.lastName());
profile.setPhoneNumber(request.phoneNumber());
userProfileRepository.save(profile);
}
}
@Service
@Transactional("productsTransactionManager")
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
public ProductService(ProductRepository productRepository,
CategoryRepository categoryRepository) {
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
}
@Transactional(value = "productsTransactionManager", readOnly = true)
public Page<Product> getActiveProducts(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return productRepository.findByIsActiveTrue(pageable);
}
@Transactional(value = "productsTransactionManager")
public Product createProduct(ProductCreateRequest request) {
Category category = categoryRepository.findById(request.categoryId())
.orElseThrow(() -> new CategoryNotFoundException(request.categoryId()));
Product product = new Product();
product.setName(request.name());
product.setPrice(request.price());
product.setCategory(category);
product.setIsActive(true);
return productRepository.save(product);
}
@Transactional(value = "productsTransactionManager")
public void updateProduct(Long productId, ProductUpdateRequest request) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
product.setName(request.name());
product.setPrice(request.price());
product.setUpdatedAt(LocalDateTime.now());
productRepository.save(product);
}
}
Cross-Database Transactions
@Service
public class OrderService {
private final UserService userService;
private final ProductService productService;
private final OrderRepository orderRepository;
@Transactional
public Order createOrder(OrderRequest request) {
// Start transaction (will handle cross-database operations)
Order order = new Order();
order.setOrderNumber(generateOrderNumber());
order.setTotalAmount(request.totalAmount());
order.setStatus(OrderStatus.PENDING);
// Get user from users database
User user = userService.getUserByEmail(request.userEmail())
.orElseThrow(() -> new UserNotFoundException(request.userEmail()));
// Get products from products database
List<Product> products = request.productIds().stream()
.map(productId -> productService.getProductById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId)))
.collect(Collectors.toList());
// Calculate total amount and validate
BigDecimal calculatedTotal = products.stream()
.map(Product::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (!calculatedTotal.equals(request.totalAmount())) {
throw new OrderAmountMismatchException();
}
// Save order in products database
Order savedOrder = orderRepository.save(order);
// Create order items (cross-database operation)
products.forEach(product -> {
OrderItem item = new OrderItem();
item.setOrder(savedOrder);
item.setProduct(product);
item.setQuantity(1); // Simplified for example
item.setUnitPrice(product.getPrice());
orderRepository.saveOrderItem(item); // Custom method
});
return savedOrder;
}
@Transactional(value = "usersTransactionManager")
@Transactional(value = "productsTransactionManager", propagation = Propagation.REQUIRES_NEW)
public void processPayment(Order order, Payment payment) {
// Payment processing logic
// Updates both user and product databases
}
}
Database-Specific Configuration
// Custom Hibernate configuration for each database
@Configuration
public class HibernateConfig {
@Bean
@Primary
public HibernateJpaVendorAdapter primaryJpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabasePlatform("org.hibernate.dialect.MySQL8Dialect");
return adapter;
}
@Bean
public HibernateJpaVendorAdapter secondaryJpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setDatabasePlatform("org.hibernate.dialect.PostgreSQLDialect");
return adapter;
}
@Bean
public JpaProperties jpaProperties() {
return new JpaProperties();
}
}
// DataSource routing for dynamic database selection
@Component
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getCurrentDatabase();
}
}
@Component
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseType> CONTEXT = new ThreadLocal<>();
public static void setCurrentDatabase(DatabaseType databaseType) {
CONTEXT.set(databaseType);
}
public static DatabaseType getCurrentDatabase() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
public enum DatabaseType {
USERS, PRODUCTS
}
// Dynamic database selection aspect
@Aspect
@Component
public class DatabaseAspect {
@Around("@annotation(selectDatabase)")
public Object selectDatabase(ProceedingJoinPoint joinPoint,
SelectDatabase selectDatabase) throws Throwable {
DatabaseType databaseType = selectDatabase.value();
DatabaseType previousType = DatabaseContextHolder.getCurrentDatabase();
try {
DatabaseContextHolder.setCurrentDatabase(databaseType);
return joinPoint.proceed();
} finally {
DatabaseContextHolder.setCurrentDatabase(previousType);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SelectDatabase {
DatabaseType value();
}
Advanced Configuration with Connection Pooling
@Configuration
@EnableConfigurationProperties(DbProperties.class)
public class MultiDatabaseConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "primary.datasource.hikari")
public HikariDataSource primaryDataSource(DbProperties dbProperties) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbProperties.getPrimary().getUrl());
dataSource.setUsername(dbProperties.getPrimary().getUsername());
dataSource.setPassword(dbProperties.getPrimary().getPassword());
dataSource.setDriverClassName(dbProperties.getPrimary().getDriverClassName());
// Connection pool settings
dataSource.setMaximumPoolSize(20);
dataSource.setMinimumIdle(5);
dataSource.setConnectionTimeout(30000);
dataSource.setIdleTimeout(600000);
dataSource.setMaxLifetime(1800000);
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "secondary.datasource.hikari")
public HikariDataSource secondaryDataSource(DbProperties dbProperties) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(dbProperties.getSecondary().getUrl());
dataSource.setUsername(dbProperties.getSecondary().getUsername());
dataSource.setPassword(dbProperties.getSecondary().getPassword());
dataSource.setDriverClassName(dbProperties.getSecondary().getDriverClassName());
// Different connection pool settings for secondary DB
dataSource.setMaximumPoolSize(10);
dataSource.setMinimumIdle(2);
dataSource.setConnectionTimeout(20000);
return dataSource;
}
}
@ConfigurationProperties
@Data
public class DbProperties {
private DataSourceConfig primary;
private DataSourceConfig secondary;
@Data
public static class DataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}
}
Examples
Complete CRUD Service
@Service
@Transactional("usersTransactionManager")
public class UserService {
private final UserRepository repository;
private final UserProfileRepository profileRepository;
private final RoleRepository roleRepository;
public UserService(UserRepository repository,
UserProfileRepository profileRepository,
RoleRepository roleRepository) {
this.repository = repository;
this.profileRepository = profileRepository;
this.roleRepository = roleRepository;
}
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return repository.findAll();
}
@Transactional(readOnly = true)
public Page<User> getUsersPage(int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdDate").descending());
return repository.findAll(pageable);
}
@Transactional(readOnly = true)
public User getUser(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));
}
@Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
public User createUser(CreateUserRequest request) {
if (repository.findByEmail(request.email()).isPresent()) {
throw new EmailAlreadyExistsException(request.email());
}
User user = new User();
user.setEmail(request.email());
user.setUsername(request.username());
user.setStatus(UserStatus.ACTIVE);
// Set default roles
List<Role> defaultRoles = roleRepository.findByNameIn(Arrays.asList("USER"));
user.setRoles(new HashSet<>(defaultRoles));
User savedUser = repository.save(user);
// Create profile
UserProfile profile = new UserProfile();
profile.setUser(savedUser);
profile.setFirstName(request.firstName());
profile.setLastName(request.lastName());
profile.setLanguage(request.language());
profileRepository.save(profile);
return savedUser;
}
@Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
public User updateUser(Long id, UpdateUserRequest request) {
User user = getUser(id);
if (!request.email().equals(user.getEmail()) &&
repository.findByEmail(request.email()).isPresent()) {
throw new EmailAlreadyExistsException(request.email());
}
user.setEmail(request.email());
user.setUsername(request.username());
user.setStatus(request.status());
User updatedUser = repository.save(user);
// Update profile
UserProfile profile = profileRepository.findByUserId(id)
.orElseThrow(() -> new UserProfileNotFoundException(id));
profile.setFirstName(request.firstName());
profile.setLastName(request.lastName());
profile.setLanguage(request.language());
profileRepository.save(profile);
return updatedUser;
}
@Transactional(value = "usersTransactionManager", rollbackFor = Exception.class)
public void deleteUser(Long id) {
User user = getUser(id);
// Soft delete by setting status
user.setStatus(UserStatus.DELETED);
repository.save(user);
// Delete profile
profileRepository.deleteByUserId(id);
}
@Transactional(value = "usersTransactionManager", readOnly = true)
public long countByStatus(UserStatus status) {
return repository.countByStatus(status);
}
@Transactional(value = "usersTransactionManager", readOnly = true)
public List<User> searchUsers(UserSearchCriteria criteria) {
if (criteria.isEmpty()) {
return repository.findAll();
}
return repository.findByEmailContainingOrUsernameContainingOrFirstNameContainingOrLastNameContaining(
criteria.searchTerm(),
criteria.searchTerm(),
criteria.searchTerm(),
criteria.searchTerm()
);
}
}
Search with Multiple Criteria
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived query methods
List<Product> findByCategory(Category category);
List<Product> findByPriceGreaterThan(BigDecimal price);
List<Product> findByStatusAndPriceBetween(ProductStatus status,
BigDecimal minPrice, BigDecimal maxPrice);
// Custom search query
@Query("""
SELECT p FROM Product p
WHERE (:name IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :name, '%')))
AND (:category IS NULL OR p.category.id = :category)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
AND (:status IS NULL OR p.status = :status)
AND p.isActive = true
ORDER BY p.createdAt DESC
""")
Page<Product> searchProducts(
@Param("name") String name,
@Param("category") Long category,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice,
@Param("status") ProductStatus status,
Pageable pageable
);
// Native query for complex filtering
@Query(value = """
SELECT p.* FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE (:name IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :name, '%')))
AND (:category IS NULL OR c.id = :category)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
AND (:status IS NULL OR p.status = :status)
AND p.is_active = true
ORDER BY p.created_at DESC
""", nativeQuery = true)
Page<Product> searchProductsNative(
@Param("name") String name,
@Param("category") Long category,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice,
@Param("status") String status,
Pageable pageable
);
}
@Service
public class ProductSearchService {
private final ProductRepository repository;
public Page<Product> search(ProductSearchCriteria criteria, int page, int size) {
Pageable pageable = PageRequest.of(page, size,
Sort.by("createdAt").descending());
return repository.searchProducts(
criteria.name(),
criteria.categoryId(),
criteria.minPrice(),
criteria.maxPrice(),
criteria.status(),
pageable
);
}
public List<Product> findPopularProducts(int limit) {
return repository.findTop10ByOrderBySalesCountDesc()
.stream()
.limit(limit)
.collect(Collectors.toList());
}
public List<Product> findRelatedProducts(Product product, int limit) {
return repository.findByCategoryAndIdNot(product.getCategory(), product.getId())
.stream()
.limit(limit)
.collect(Collectors.toList());
}
}
// Search criteria DTO
public record ProductSearchCriteria(
@NotBlank String name,
Long categoryId,
BigDecimal minPrice,
BigDecimal maxPrice,
String status
) {}
// Search criteria builder
public class ProductSearchCriteriaBuilder {
private String name;
private Long categoryId;
private BigDecimal minPrice;
private BigDecimal maxPrice;
private String status;
public ProductSearchCriteriaBuilder name(String name) {
this.name = name;
return this;
}
public ProductSearchCriteriaBuilder categoryId(Long categoryId) {
this.categoryId = categoryId;
return this;
}
public ProductSearchCriteriaBuilder priceRange(BigDecimal min, BigDecimal max) {
this.minPrice = min;
this.maxPrice = max;
return this;
}
public ProductSearchCriteriaBuilder status(String status) {
this.status = status;
return this;
}
public ProductSearchCriteria build() {
return new ProductSearchCriteria(name, categoryId, minPrice, maxPrice, status);
}
}
Best Practices
Entity Design
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, length = 255)
private String name;
@Column(name = "price", precision = 10, scale = 2, nullable = false)
private BigDecimal price;
@Column(name = "created_date", nullable = false, updatable = false)
private LocalDateTime createdDate;
@Column(name = "updated_date")
private LocalDateTime updatedDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private Category category;
@Enumerated(EnumType.STRING)
private ProductStatus status;
// Constructor injection pattern
public Product(String name, BigDecimal price, Category category) {
this.name = name;
this.price = price;
this.category = category;
this.status = ProductStatus.ACTIVE;
this.createdDate = LocalDateTime.now();
this.updatedDate = LocalDateTime.now();
}
// Business logic methods
public void updatePrice(BigDecimal newPrice) {
if (newPrice.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Price must be positive");
}
this.price = newPrice;
this.updatedDate = LocalDateTime.now();
}
public void activate() {
this.status = ProductStatus.ACTIVE;
this.updatedDate = LocalDateTime.now();
}
public void deactivate() {
this.status = ProductStatus.INACTIVE;
this.updatedDate = LocalDateTime.now();
}
// equals() and hashCode() based on id
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return Objects.equals(id, product.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Repository Queries
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Good: Simple derived query
Optional<User> findByEmail(String email);
// Good: Complex query with @Query
@Query("""
SELECT u FROM User u
WHERE u.status = :status
AND u.createdDate >= :startDate
ORDER BY u.createdDate DESC
""")
List<User> findActiveUsersSince(
@Param("status") UserStatus status,
@Param("startDate") LocalDateTime startDate
);
// Good: Using projection for better performance
@Query("""
SELECT new com.example.dto.UserSummary(u.id, u.email, u.status, u.createdDate)
FROM User u
WHERE u.status = :status
""")
List<UserSummary> findUserSummariesByStatus(@Param("status") UserStatus status);
// Avoid: Long method name
// List<User> findByStatusAndCreatedDateGreaterThanAndLastLoginDateLessThan(...)
// Avoid: Not using Optional for single results
// User findByEmail(String email); // Should return Optional<User>
}
Pagination Best Practices
@Service
public class ProductService {
private final ProductRepository repository;
public Page<Product> getProducts(int page, int size) {
// Validate pagination parameters
if (page < 0) {
throw new IllegalArgumentException("Page number must be non-negative");
}
if (size <= 0 || size > 100) {
throw new IllegalArgumentException("Page size must be between 1 and 100");
}
Pageable pageable = PageRequest.of(page, size,
Sort.by("id").ascending());
return repository.findAll(pageable);
}
public List<Product> getRecentProducts(int limit) {
// Use list for smaller datasets where you need exact count
if (limit <= 0 || limit > 100) {
throw new IllegalArgumentException("Limit must be between 1 and 100");
}
return repository.findTop10ByOrderByCreatedAtDesc()
.stream()
.limit(limit)
.collect(Collectors.toList());
}
public Slice<Product> getRecentProductsWithCursor(Long lastId, int size) {
// Use cursor-based pagination for better performance
Sort sort = Sort.by("id").ascending();
Pageable pageable = PageRequest.of(0, size, sort);
if (lastId != null) {
return repository.findByIdGreaterThan(lastId, pageable);
} else {
return repository.findAll(pageable);
}
}
}
Transaction Management
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
@Transactional
public Order createOrder(Order order, Payment payment) {
// Validate business rules
if (order.getItems().isEmpty()) {
throw new EmptyOrderException("Order must contain at least one item");
}
// Calculate total amount
BigDecimal total = order.getItems().stream()
.map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (total.compareTo(BigDecimal.ZERO) <= 0) {
throw new InvalidOrderAmountException("Order total must be positive");
}
// Set order details
order.setTotalAmount(total);
order.setStatus(OrderStatus.PENDING);
order.setOrderNumber(generateOrderNumber());
// Save order
Order savedOrder = orderRepository.save(order);
// Process payment
payment.setOrderId(savedOrder.getId());
payment.setAmount(total);
paymentRepository.save(payment);
return savedOrder;
}
@Transactional(readOnly = true)
public Order getOrderWithDetails(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
@Transactional(rollbackFor = {PaymentException.class, InventoryException.class})
public void processPayment(Long orderId) throws PaymentException {
Order order = getOrderWithDetails(orderId);
// Process payment logic
paymentService.process(order.getPayment());
// Update order status
order.setStatus(OrderStatus.PROCESSING);
order.setUpdatedAt(LocalDateTime.now());
orderRepository.save(order);
// This will trigger rollback if thrown
if (paymentFailed) {
throw new PaymentException("Payment processing failed");
}
// Update inventory after successful payment
inventoryService.reserveItems(order.getItems());
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderCreation(Order order) {
// Always creates new transaction
auditLogRepository.save(new AuditLog("ORDER_CREATED", order.getId()));
}
}
N+1 Query Problem Solutions
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Solution 1: Use JOIN FETCH
@Query("""
SELECT DISTINCT o FROM Order o
JOIN FETCH o.items
JOIN FETCH o.customer
WHERE o.id = :orderId
""")
Optional<Order> findOrderWithItemsAndCustomer(@Param("orderId") Long orderId);
// Solution 2: Use EntityGraph
@EntityGraph(attributePaths = {"items", "customer", "items.product"})
Optional<Order> findWithDetailsById(Long orderId);
// Solution 3: Use batch loading
@Query("SELECT o FROM Order o WHERE o.customer.id = :customerId")
List<Order> findByCustomerId(@Param("customerId") Long customerId);
// Solution 4: Use DTO projection
@Query("""
SELECT new com.example.dto.OrderSummary(
o.id, o.orderNumber, o.totalAmount, o.status,
c.firstName, c.lastName
) FROM Order o
JOIN o.customer c
WHERE o.customer.id = :customerId
""")
List<OrderSummary> findOrderSummariesByCustomerId(@Param("customerId") Long customerId);
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
// Solution using JOIN FETCH
@Transactional(readOnly = true)
public Order getOrderWithDetails(Long orderId) {
return orderRepository.findOrderWithItemsAndCustomer(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Solution using batch loading
@Transactional(readOnly = true)
public List<Order> getCustomerOrdersWithDetails(Long customerId) {
List<Order> orders = orderRepository.findByCustomerId(customerId);
// Batch load items for all orders
Map<Long, List<OrderItem>> orderItems = orderRepository.findOrderItemsByOrderIds(
orders.stream().map(Order::getId).collect(Collectors.toList())
);
// Batch load products for all items
Map<Long, Product> products = orderRepository.findProductsByItemIds(
orderItems.values().stream()
.flatMap(List::stream)
.map(OrderItem::getId)
.collect(Collectors.toList())
);
// Associate items with their orders
orders.forEach(order -> {
List<OrderItem> items = orderItems.getOrDefault(order.getId(), Collections.emptyList());
items.forEach(item -> {
item.setProduct(products.get(item.getProductId()));
order.addItem(item);
});
});
return orders;
}
// Solution using DTO projection (most efficient)
@Transactional(readOnly = true)
public List<OrderSummary> getCustomerOrderSummaries(Long customerId) {
return orderRepository.findOrderSummariesByCustomerId(customerId);
}
}
Exception Handling
@Service
public class ProductService {
private final ProductRepository repository;
public Product getProduct(Long id) {
return repository.findById(id)
.orElseThrow(() ->
new ResourceNotFoundException("Product not found: " + id)
);
}
public Product createProduct(ProductCreateRequest request) {
// Validate business rules
if (request.name() == null || request.name().trim().isEmpty()) {
throw new InvalidProductException("Product name cannot be empty");
}
if (request.price() == null || request.price().compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidProductException("Product price must be non-negative");
}
if (repository.findByNameIgnoreCase(request.name()).isPresent()) {
throw new DuplicateProductException("Product with name '" + request.name() + "' already exists");
}
Product product = new Product();
product.setName(request.name());
product.setPrice(request.price());
product.setDescription(request.description());
product.setStatus(ProductStatus.ACTIVE);
return repository.save(product);
}
public Product updateProduct(Long id, ProductUpdateRequest request) {
Product product = getProduct(id);
if (request.name() != null) {
if (!request.name().equals(product.getName()) &&
repository.findByNameIgnoreCase(request.name()).isPresent()) {
throw new DuplicateProductException("Product with name '" + request.name() + "' already exists");
}
product.setName(request.name());
}
if (request.price() != null) {
if (request.price().compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidProductException("Product price must be non-negative");
}
product.setPrice(request.price());
}
if (request.description() != null) {
product.setDescription(request.description());
}
return repository.save(product);
}
@Transactional(rollbackFor = Exception.class)
public void deleteProduct(Long id) {
Product product = getProduct(id);
// Check if product can be deleted (e.g., has orders)
if (orderService.hasOrdersForProduct(id)) {
throw new ProductDeletionException("Cannot delete product with existing orders");
}
repository.delete(product);
}
}
// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
public class DuplicateProductException extends RuntimeException {
public DuplicateProductException(String message) {
super(message);
}
}
public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}
public class ProductDeletionException extends RuntimeException {
public ProductDeletionException(String message) {
super(message);
}
}