Код-ревью Middle Spring Data JPA обновлено

Код-ревью: отчёт по клиентам

Вопрос

Найдите проблемы в работе с репозиториями и EntityManager.

public interface CustomerRepository extends JpaRepository<Customer, UUID> {
    @Modifying
    @Query("update Customer c set c.active = false where c.lastLogin < :date")
    int deactivateOld(@Param("date") Instant date);
}

@Service
public class CustomerReport {
    private final CustomerRepository repo;
    private final EntityManager em;

    public CustomerReport(CustomerRepository repo, EntityManager em) {
        this.repo = repo;
        this.em = em;
    }

    public List<Customer> byCity(String city) {
        return em.createNativeQuery(
            "select * from customers where city = '" + city + "'", Customer.class).getResultList();
    }

    public long countVip() {
        return repo.findAll().stream().filter(Customer::isVip).count();
    }

    @Transactional
    public void cleanup(Customer current) {
        repo.deactivateOld(Instant.now().minus(365, ChronoUnit.DAYS));
        current.setNote("cleanup done");
        repo.save(current);
    }

    public void importCustomer(CustomerDto dto) {
        Customer c = new Customer(UUID.randomUUID(), dto.name());
        repo.save(c);
        c.setCity(dto.city());
    }
}

Коротко

Общий принцип: считать и фильтровать в базе, параметры передавать параметрами, помнить, что bulk-запросы обходят контекст персистентности, а save с заданным id превращается в merge. Изменения сущности должны происходить внутри транзакции до коммита.

Что должен найти ревьюер

  1. Безопасностьстрока 19
    Город склеивается в текст native-запроса: SQL-инъекция. Нужен параметр (where city = :city и setParameter) или метод репозитория.
  2. Производительностьстрока 23
    findAll() грузит всех клиентов в память ради подсчёта: нужен countByVipTrue() в базе.
  3. Ошибкастрока 30
    Bulk-update идёт мимо контекста персистентности: если current среди старых клиентов, в объекте остаётся active = true, и merge в save(current) вернёт его обратно. Нужен clearAutomatically и аккуратный порядок.
  4. Производительностьстрока 34
    UUID задан вручную, сущность считается не новой: save делает merge с лишним SELECT перед INSERT. Нужен Persistable или @Version.
  5. Ошибкастрока 36
    Изменение после save вне транзакции не сохранится: метод без @Transactional, объект после save отсоединён.

Разбор

Общий принцип: считать и фильтровать в базе, параметры передавать параметрами, помнить, что bulk-запросы обходят контекст персистентности, а save с заданным id превращается в merge. Изменения сущности должны происходить внутри транзакции до коммита.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.