middle
Как организовать работу с данными в Spring Boot?
Spring Data JPA с Hibernate 6 остаётся основным способом работы с реляционными данными. PostgreSQL занимает позицию дефолтной базы данных для Java-проектов.
JPA-сущность
Пример JPA-сущности с optimistic locking
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
private UUID customerId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItemJpaEntity> items = new ArrayList<>();
@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal totalAmount;
@CreationTimestamp
@Column(updatable = false)
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@Version
private Long version; // Optimistic locking
public void addItem(OrderItemJpaEntity item) {
items.add(item);
item.setOrder(this);
recalculateTotal();
}
}
Репозиторий с различными типами запросов
Пример репозитория
public interface OrderJpaRepository extends JpaRepository<OrderJpaEntity, UUID> {
// Derived query
List<OrderJpaEntity> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
// JPQL с проекцией
@Query("""
SELECT new com.example.order.adapter.out.persistence.OrderSummary(
o.id, o.status, o.totalAmount, o.createdAt
)
FROM OrderJpaEntity o
WHERE o.customerId = :customerId
ORDER BY o.createdAt DESC
""")
Page<OrderSummary> findOrderSummaries(
@Param("customerId") UUID customerId, Pageable pageable);
// Native query
@Query(value = """
SELECT o.* FROM orders o
WHERE o.status = 'CREATED'
AND o.created_at < NOW() - INTERVAL '30 minutes'
FOR UPDATE SKIP LOCKED
LIMIT :limit
""", nativeQuery = true)
List<OrderJpaEntity> findStaleOrders(@Param("limit") int limit);
// Batch update
@Modifying
@Query("UPDATE OrderJpaEntity o SET o.status = :status WHERE o.id IN :ids")
int updateStatusBatch(@Param("ids") List<UUID> ids, @Param("status") OrderStatus status);
}
Настройка Hibernate для production
Пример
spring:
jpa:
open-in-view: false # Отключить ОБЯЗАТЕЛЬНО
hibernate:
ddl-auto: validate # Только валидация, миграции через Flyway
properties:
hibernate:
default_batch_fetch_size: 20
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
Redis для кэширования
Пример
@Service
public class ProductService {
@Cacheable(value = "products", key = "#productId")
public Product getProduct(UUID productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
}
@CacheEvict(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
}
Частые ошибки
- N+1 проблема: загрузка связанных сущностей в цикле. Решение: @EntityGraph, JOIN FETCH, default_batch_fetch_size
- Использование ddl-auto=update в production — потенциальная потеря данных
- Кэширование без стратегии инвалидации
- Игнорирование connection pool настроек — дефолтный HikariCP pool size (10) может быть недостаточен
- open-in-view=true (значение по умолчанию) — держит транзакцию открытой на весь HTTP-запрос
На собеседовании: три обязательных пункта: 1) open-in-view=false, 2) ddl-auto=validate в production, 3) знание N+1 проблемы и способов решения. Без этих знаний разговор о JPA на middle-уровне не пройдёт. Бонус: упомянуть, что Virtual Threads снизили необходимость в R2DBC.