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_prod
…(truncated)