# 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. ```java public interface CrudRepository extends Repository { // CREATE/UPDATE S save(S entity); // Save or update entity Iterable saveAll(Iterable entities); // Save multiple entities // READ Optional 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 findAll(); // Find all entities Iterable findAllById(Iterable 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 ids); // Delete by multiple IDs void deleteAll(Iterable entities); // Delete multiple entities void deleteAll(); // Delete all entities } ``` ### PagingAndSortingRepository Extends CrudRepository with pagination and sorting capabilities. ```java public interface PagingAndSortingRepository extends CrudRepository { // Sorting Iterable findAll(Sort sort); // Find all with sorting // Pagination Page findAll(Pageable pageable); // Find all with pagination } ``` ### JpaRepository The most comprehensive interface extending PagingAndSortingRepository with JPA-specific operations. ```java public interface JpaRepository extends PagingAndSortingRepository { // Enhanced read operations List findAll(); // Returns List instead of Iterable List findAllById(Iterable ids); // Returns List instead of Iterable List findAll(Sort sort); // Returns List instead of Iterable Page findAll(Pageable pageable); // Returns Page // Batch operations List saveAll(Iterable entities); // Save multiple with return // Batch delete operations void deleteInBatch(Iterable entities); // Delete without flushing void deleteAllInBatch(Iterable ids); // Delete by IDs without flushing void deleteAllInBatch(); // Delete all without flushing // Flush operations void flush(); // Flush to database S saveAndFlush(S entity); // Save and immediately flush List saveAllAndFlush(Iterable entities); // Save all and flush } ``` ## Query Methods ### Derived Query Methods Spring Data automatically generates queries from method names following naming conventions. #### Simple Lookups ```java Optional findByEmail(String email); // Single result List findByUsername(String username); // Multiple results User findFirstByEmail(String email); // First result User findTopByOrderByAgeDesc(); // Top by age descending ``` #### Conditional Operators ```java // Equality List findByStatus(String status); List findByStatusNot(String status); // Comparison List findByAgeGreaterThan(Integer age); List findByAgeLessThanEqual(Integer age); List findByAgeBetween(Integer min, Integer max); List findByAgeGreaterThanEqual(25); // Static comparison // Null/Empty checks List findByEmailIsNull(); List findByEmailIsNotNull(); List findByEmailNotEmpty(); // Boolean properties List findByActiveTrue(); ListByEmailActiveFalse(); ``` #### String Operations ```java // Pattern matching List findByEmailContaining(String pattern); // LIKE '%pattern%' List findByEmailStartsWith(String prefix); // LIKE 'prefix%' List findByEmailEndsWith(String suffix); // LIKE '%suffix' List findByEmailLike(String pattern); // Exact LIKE pattern // Case sensitivity List findByEmailIgnoreCase(String email); ``` #### Date and Time Operations ```java // After/Before List findByOrderDateAfter(LocalDate date); List findByCreatedDateBefore(LocalDateTime dateTime); // Between List findByOrderDateBetween(LocalDate start, LocalDate end); // Range queries List findByTotalPriceGreaterThan(BigDecimal min); List findByTotalPriceBetween(BigDecimal min, BigDecimal max); // Current date comparisons List findByCreatedDateBefore(LocalDateTime.now().minusDays(7)); ``` #### Ordering ```java // Simple ordering List findByStatusOrderByCreatedDateDesc(String status); List findAllByOrderByLastNameAsc(); // Multiple sort criteria List findByCategoryOrderByPriceAscNameDesc(String category); // Dynamic sorting List findAll(Sort.by("lastName").ascending()); List findByActiveTrue(Sort.by("createdDate").descending()); ``` #### Pagination Integration ```java Page findByStatus(String status, Pageable pageable); Slice findByActiveTrue(Pageable pageable); // No total count List findTop10ByOrderByCreatedDateDesc(); // Fixed limit ``` #### Delete Operations ```java // 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. ```java @Repository public interface OrderRepository extends JpaRepository { // Basic query with named parameters @Query("SELECT o FROM Order o WHERE o.status = :status AND o.totalPrice > :minPrice") List 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 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 findByStatuses(@Param("statuses") List statuses); // EXISTS clause @Query("SELECT o FROM Order o WHERE EXISTS (SELECT 1 FROM o.items i WHERE i.quantity = 0)") List findOrdersWithZeroQuantityItems(); } ``` #### Native SQL Queries Use native SQL for database-specific queries. ```java @Repository public interface ProductRepository extends JpaRepository { // Simple native query @Query(value = "SELECT * FROM products WHERE category = :category AND price < :maxPrice", nativeQuery = true) List 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 findExpensiveProductSummaries(@Param("minPrice") BigDecimal minPrice); } ``` #### Modifying Queries Use `@Modifying` for INSERT, UPDATE, DELETE operations. ```java @Repository public interface UserRepository extends JpaRepository { @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 ```java @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 ```java @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 ```java @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 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 ```java @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 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 students = new HashSet<>(); } ``` #### Join Table with Additional Attributes ```java @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 ```java @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 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 ```java @Service public class ProductService { private final ProductRepository repository; public Page getProductsPage(int page, int size) { Pageable pageable = PageRequest.of(page, size); return repository.findAll(pageable); } public Page getProductsWithSorting(int page, int size, String sortField) { Pageable pageable = PageRequest.of(page, size, Sort.by(sortField)); return repository.findAll(pageable); } public Page 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 ```java @Service public class OrderService { private final OrderRepository repository; public Page getOrdersByStatus(String status, int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("createdDate").descending()); return repository.findByStatus(status, pageable); } public Slice 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 streamAllOrders() { // Stream for large datasets return repository.streamAllBy(); } } ``` ### Custom Pagination Queries ```java @Repository public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u WHERE u.active = true") Page findActiveUsers(Pageable pageable); @Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true) Page findUsersCreatedAfter(@Param("date") LocalDateTime date, Pageable pageable); } ``` ## Database Auditing ### Spring Data JPA Auditing #### Configuration ```java @Configuration @EnableJpaAuditing(auditorAwareRef = "auditorProvider") @EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class) public class AuditingConfig { @Bean public AuditorAware 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 ```java @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 ```java @Component public class CustomAuditorProvider implements AuditorAware { @Override public Optional 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 ```java @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 ```java @Configuration @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRepositoryFactoryBean.class) @EnableJpaAuditing(auditorAwareRef = "auditorProvider") public class EnversConfig { @Bean public AuditorAware auditorProvider() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .map(Authentication::getName); } } ``` #### Audited Entities ```java @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 ```java public interface DocumentRepository extends JpaRepository, JpaEntityRepository { @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 findRevisions(Long documentId); T findRevision(Class entityClass, Number revision); } @Service public class DocumentAuditService { private final DocumentRepository documentRepository; private final AuditReader auditReader; public List getDocumentHistory(Long documentId) { List 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 ```java @Service @Transactional public class OrderService { private final OrderRepository orderRepository; private final PaymentRepository paymentRepository; private final InventoryService inventoryService; @Transactional public Order createOrder(Order order, List 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 ```java @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 ```java @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 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 ```java @Repository public interface OrderRepository extends JpaRepository { // Basic delete operations void deleteById(Long id); void delete(Order entity); void deleteAllById(Iterable ids); void deleteAll(Iterable 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 ```java @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 orderIds) { // Batch delete for better performance orderRepository.deleteAllById(orderIds); } } ``` #### Cascade and Orphan Removal ```java @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 products = new ArrayList<>(); @OneToMany(mappedBy = "category", cascade = CascadeType.PERSIST, // Only cascade save operations orphanRemoval = false) private List 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 ```java @Service public class BatchOrderService { private final OrderRepository orderRepository; @Transactional public void batchUpdateOrders(List 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 statuses) { // Batch delete by status statuses.forEach(status -> orderRepository.deleteByStatus(status)); } } ``` ## UUID as Primary Key ### Modern Approach (Hibernate 6.2+ with JPA 3.1) ```java @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 { Optional findByEmail(String email); Optional findByUsername(String username); List 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 ```java @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 ```java @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 ```java // 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 ```java @Service public class UserService { private final UserRepository repository; // Batch operations with UUIDs @Transactional public List batchCreateUsers(List requests) { List 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 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 ```java @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 ```java @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 ```java @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 ```java @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 { // Uses idx_article_status_date Page
findByStatusOrderByPublishedDateDesc( String status, Pageable pageable ); // Uses idx_author_date Page
findByAuthorOrderByPublishedDateDesc( User author, Pageable pageable ); // Uses idx_article_published_range List
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
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
getPublishedArticles(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("publishedDate").descending()); return repository.findByStatusOrderByPublishedDateDesc("PUBLISHED", pageable); } public Page
getArticlesByAuthor(User author, int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("publishedDate").descending()); return repository.findByAuthorOrderByPublishedDateDesc(author, pageable); } public List
getRecentArticles(int limit) { return repository.findByPublishedDateAfterOrderByPublishedDateDesc( LocalDateTime.now().minusMonths(1) ).stream().limit(limit).collect(Collectors.toList()); } } ``` ### Indexes on Relationships ```java @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 { // Uses idx_comment_article List findByArticle(Article article); // Uses idx_comment_article_created List findByArticleOrderByCreatedDateDesc(Article article); // Uses idx_comment_article_approved long countByArticleAndApproved(Article article, boolean approved); // Uses idx_comment_user List findByUser(User user); // Uses idx_comment_status_created List 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 findApprovedCommentsByArticle( @Param("article") Article article, Pageable pageable ); } ``` ### Index Best Practices ```java @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 getActiveProductsByCategory(Category category) { return repository.findByCategoryAndStatus(category, "ACTIVE"); } // Benefits from idx_status_created public Page 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 getPopularProducts(int limit) { return repository.findTop10ByOrderBySalesCountDesc() .stream() .limit(limit) .collect(Collectors.toList()); } } ``` ## Multiple Database Configuration ### Basic Configuration for Multiple Databases #### Primary Database Configuration ```java @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 ```java @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 ```properties # 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 ```java // 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 ```java // Users repositories @Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByUsername(String username); List findByStatus(UserStatus status); } @Repository public interface UserProfileRepository extends JpaRepository { Optional findByUserId(Long userId); } // Products repositories @Repository public interface ProductRepository extends JpaRepository { List findByCategory(Category category); List findByIsActiveTrue(); Page findByIsActiveTrue(Pageable pageable); } @Repository public interface CategoryRepository extends JpaRepository { List findByParentIsNull(); List findByParent(Category parent); List findByIsActiveTrueOrderBySortOrderAsc(); } ``` ### Service Layer Configuration ```java @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 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 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 ```java @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 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 ```java // 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 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 ```java @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 ```java @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 getAllUsers() { return repository.findAll(); } @Transactional(readOnly = true) public Page 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 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 searchUsers(UserSearchCriteria criteria) { if (criteria.isEmpty()) { return repository.findAll(); } return repository.findByEmailContainingOrUsernameContainingOrFirstNameContainingOrLastNameContaining( criteria.searchTerm(), criteria.searchTerm(), criteria.searchTerm(), criteria.searchTerm() ); } } ``` ### Search with Multiple Criteria ```java @Repository public interface ProductRepository extends JpaRepository { // Derived query methods List findByCategory(Category category); List findByPriceGreaterThan(BigDecimal price); List 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 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 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 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 findPopularProducts(int limit) { return repository.findTop10ByOrderBySalesCountDesc() .stream() .limit(limit) .collect(Collectors.toList()); } public List 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 ```java @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 ```java @Repository public interface UserRepository extends JpaRepository { // Good: Simple derived query Optional 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 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 findUserSummariesByStatus(@Param("status") UserStatus status); // Avoid: Long method name // List findByStatusAndCreatedDateGreaterThanAndLastLoginDateLessThan(...) // Avoid: Not using Optional for single results // User findByEmail(String email); // Should return Optional } ``` ### Pagination Best Practices ```java @Service public class ProductService { private final ProductRepository repository; public Page 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 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 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 ```java @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 ```java @Repository public interface OrderRepository extends JpaRepository { // 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 findOrderWithItemsAndCustomer(@Param("orderId") Long orderId); // Solution 2: Use EntityGraph @EntityGraph(attributePaths = {"items", "customer", "items.product"}) Optional findWithDetailsById(Long orderId); // Solution 3: Use batch loading @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId") List 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 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 getCustomerOrdersWithDetails(Long customerId) { List orders = orderRepository.findByCustomerId(customerId); // Batch load items for all orders Map> orderItems = orderRepository.findOrderItemsByOrderIds( orders.stream().map(Order::getId).collect(Collectors.toList()) ); // Batch load products for all items Map products = orderRepository.findProductsByItemIds( orderItems.values().stream() .flatMap(List::stream) .map(OrderItem::getId) .collect(Collectors.toList()) ); // Associate items with their orders orders.forEach(order -> { List 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 getCustomerOrderSummaries(Long customerId) { return orderRepository.findOrderSummariesByCustomerId(customerId); } } ``` ### Exception Handling ```java @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); } } ``` ## References - [Spring Data JPA Official Documentation](https://spring.io/projects/spring-data-jpa) - [Hibernate Documentation](https://hibernate.org/) - [JPA Specification](https://jakarta.ee/specifications/persistence/) - [Spring Data Commons Reference](https://docs.spring.io/spring-data/commons/docs/current/reference/html/) - [Database Indexing Guide](https://use-the-index-luke.com/)