# 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 |