Gymterview
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.