# Data Access - Spring Data JPA ## JPA Entity Pattern ```java @Entity @Table(name = "users", indexes = { @Index(name = "idx_email", columnList = "email", unique = true), @Index(name = "idx_username", columnList = "username") }) @EntityListeners(AuditingEntityListener.class) @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 100) private String email; @Column(nullable = false, length = 100) private String password; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) @Builder.Default private Boolean active = true; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List
addresses = new ArrayList<>(); @ManyToMany @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) @Builder.Default private Set roles = new HashSet<>(); @CreatedDate @Column(nullable = false, updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(nullable = false) private LocalDateTime updatedAt; @Version private Long version; // Helper methods for bidirectional relationships public void addAddress(Address address) { addresses.add(address); address.setUser(this); } public void removeAddress(Address address) { addresses.remove(address); address.setUser(null); } } ``` ## Spring Data JPA Repository ```java @Repository public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByEmail(String email); Optional findByUsername(String username); boolean existsByEmail(String email); boolean existsByUsername(String username); @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email = :email") Optional findByEmailWithRoles(@Param("email") String email); @Query("SELECT u FROM User u WHERE u.active = true AND u.createdAt >= :since") List findActiveUsersSince(@Param("since") LocalDateTime since); @Modifying @Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :threshold") int deactivateInactiveUsers(@Param("threshold") LocalDateTime threshold); // Projection for read-only DTOs @Query("SELECT new com.example.dto.UserSummary(u.id, u.username, u.email) " + "FROM User u WHERE u.active = true") List findAllActiveSummaries(); } ``` ## Repository with Specifications ```java public class UserSpecifications { public static Specification hasEmail(String email) { return (root, query, cb) -> email == null ? null : cb.equal(root.get("email"), email); } public static Specification isActive() { return (root, query, cb) -> cb.isTrue(root.get("active")); } public static Specification createdAfter(LocalDateTime date) { return (root, query, cb) -> date == null ? null : cb.greaterThanOrEqualTo(root.get("createdAt"), date); } public static Specification hasRole(String roleName) { return (root, query, cb) -> { Join roles = root.join("roles", JoinType.INNER); return cb.equal(roles.get("name"), roleName); }; } } // Usage in service @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; public Page searchUsers(UserSearchCriteria criteria, Pageable pageable) { Specification spec = Specification .where(UserSpecifications.hasEmail(criteria.email())) .and(UserSpecifications.isActive()) .and(UserSpecifications.createdAfter(criteria.createdAfter())); return userRepository.findAll(spec, pageable); } } ``` ## Transaction Management ```java @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class OrderService { private final OrderRepository orderRepository; private final PaymentService paymentService; private final InventoryService inventoryService; private final NotificationService notificationService; @Transactional public Order createOrder(OrderCreateRequest request) { // All operations in single transaction Order order = Order.builder() .customerId(request.customerId()) .status(OrderStatus.PENDING) .build(); request.items().forEach(item -> { inventoryService.reserveStock(item.productId(), item.quantity()); order.addItem(item); }); order = orderRepository.save(order); try { paymentService.processPayment(order); order.setStatus(OrderStatus.PAID); } catch (PaymentException e) { order.setStatus(OrderStatus.PAYMENT_FAILED); throw e; // Transaction will rollback } return orderRepository.save(order); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void logOrderEvent(Long orderId, String event) { // Separate transaction - will commit even if parent rolls back OrderEvent orderEvent = new OrderEvent(orderId, event); orderEventRepository.save(orderEvent); } @Transactional(noRollbackFor = NotificationException.class) public void completeOrder(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new ResourceNotFoundException("Order not found")); order.setStatus(OrderStatus.COMPLETED); orderRepository.save(order); // Won't rollback transaction if notification fails try { notificationService.sendCompletionEmail(order); } catch (NotificationException e) { log.error("Failed to send notification for order {}", orderId, e); } } } ``` ## Auditing Configuration ```java @Configuration @EnableJpaAuditing public class JpaAuditingConfig { @Bean public AuditorAware auditorProvider() { return () -> { Authentication authentication = SecurityContextHolder .getContext() .getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { return Optional.of("system"); } return Optional.of(authentication.getName()); }; } } @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @Setter public abstract class AuditableEntity { @CreatedDate @Column(nullable = false, updatable = false) private LocalDateTime createdAt; @CreatedBy @Column(nullable = false, updatable = false, length = 100) private String createdBy; @LastModifiedDate @Column(nullable = false) private LocalDateTime updatedAt; @LastModifiedBy @Column(nullable = false, length = 100) private String updatedBy; } ``` ## Projections ```java // Interface-based projection public interface UserSummary { Long getId(); String getUsername(); String getEmail(); @Value("#{target.firstName + ' ' + target.lastName}") String getFullName(); } // Class-based projection (DTO) public record UserSummaryDto( Long id, String username, String email ) {} // Usage public interface UserRepository extends JpaRepository { List findAllBy(); List findAllBy(Class type); } // Service usage List summaries = userRepository.findAllBy(); List dtos = userRepository.findAllBy(UserSummaryDto.class); ``` ## Query Optimization ```java @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserQueryService { private final UserRepository userRepository; private final EntityManager entityManager; // N+1 problem solved with JOIN FETCH @Query("SELECT DISTINCT u FROM User u " + "LEFT JOIN FETCH u.addresses " + "LEFT JOIN FETCH u.roles " + "WHERE u.active = true") List findAllActiveWithAssociations(); // Batch fetching @BatchSize(size = 25) @OneToMany(mappedBy = "user") private List orders; // EntityGraph for dynamic fetching @EntityGraph(attributePaths = {"addresses", "roles"}) List findAllByActiveTrue(); // Pagination to avoid loading all data public Page findAllUsers(Pageable pageable) { return userRepository.findAll(pageable); } // Native query for complex queries @Query(value = """ SELECT u.* FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE o.created_at >= :since GROUP BY u.id HAVING COUNT(o.id) >= :minOrders """, nativeQuery = true) List findFrequentBuyers(@Param("since") LocalDateTime since, @Param("minOrders") int minOrders); } ``` ## Database Migrations (Flyway) ```sql -- V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(100) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, username VARCHAR(50) NOT NULL UNIQUE, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, version BIGINT NOT NULL DEFAULT 0 ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_active ON users(active); -- V2__create_addresses_table.sql CREATE TABLE addresses ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, street VARCHAR(200) NOT NULL, city VARCHAR(100) NOT NULL, country VARCHAR(2) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_addresses_user_id ON addresses(user_id); ``` ## Quick Reference | Annotation | Purpose | |------------|---------| | `@Entity` | Marks class as JPA entity | | `@Table` | Specifies table details and indexes | | `@Id` | Marks primary key field | | `@GeneratedValue` | Auto-generated primary key strategy | | `@Column` | Column constraints and mapping | | `@OneToMany/@ManyToOne` | One-to-many/many-to-one relationships | | `@ManyToMany` | Many-to-many relationships | | `@JoinColumn/@JoinTable` | Join column/table configuration | | `@Transactional` | Declares transaction boundaries | | `@Query` | Custom JPQL/native queries | | `@Modifying` | Marks query as UPDATE/DELETE | | `@EntityGraph` | Defines fetch graph for associations | | `@Version` | Optimistic locking version field |