Оптимизированный выбор агрегированных значений с фильтрацией с использованием спецификаций JPA - PullRequest
0 голосов
/ 06 февраля 2020

У меня следующая проблема - мне нужно выбрать агрегированные (сводные) данные из БД. Будет много фильтров, которые будут динамически изменяться, и эта функциональность фильтрации уже поддерживается и реализуется с использованием спецификаций Spring Data JPA, которые я хотел бы использовать.

Я хотел бы оптимизировать реальное решение, которое работает, но потребляет много памяти. Мне нужно сложить все зарплаты пользователя, количество детей и получить их средний возраст в соответствии со спецификациями. У меня есть метод, подобный этому

public List<User> findAll(Specification<User> userSpecification)

Я могу получить список всех пользователей, соответствующих критериям фильтра, итерировать их и собрать необходимые значения в Java, и это прекрасно работает. Но если количество извлекаемых пользователей слишком велико, проблема заключается в большом количестве потребляемой памяти.

По моему мнению, было бы намного лучше объединить эти значения в БД и вернуть только 3 числа из БД в Java -> сумма зарплат пользователей, количество всех их детей и средний возраст выбранных пользователей.

Может ли кто-нибудь помочь мне, что было бы оптимальным решением? Или есть способ, как объединить спецификации JPA с возвратом только агрегированных данных из БД в Java ??

Большое спасибо.

Ответы [ 2 ]

0 голосов
/ 08 февраля 2020

Как вы уже знаете, вы не можете комбинировать пользовательские запросы со спецификациями. Можно повторно использовать ваши спецификации с CriteriaBuilder API и использовать его для запроса вашей базы данных. Предполагая следующий прогноз:

public class AggregatedUserDetails {
  private long salarySum;
  private double ageAverage;
  private long childrenSum;

  public AggregatedUserDetails(long salarySum, double ageAverage, long childrenSum) {
    this.salarySum = salarySum;
    this.ageAverage = ageAverage;
    this.childrenSum = childrenSum;
  }
  // Getters & setters...
}

Вы можете сделать следующее (я сделал некоторые предположения относительно вашей сущности пользователя ...):

Обновление: обратите внимание, что «Фасад» часть имени класса UserDetailsFacade относится к фасаду приложения из книги «Шаблоны анализа» (Fowler, 1997)

@Component
public class UserDetailsFacade {

  private final EntityManager entityManager;

  public UserDetailsFacade(EntityManager entityManager) {
    this.entityManager = entityManager;
  }

  public AggregatedUserDetails aggregatedUserDetails() {
    return aggregatedUserDetails(null);
  }

  @Transactional(readOnly = true)
  public AggregatedUserDetails aggregatedUserDetails(Specification<User> userSpecification) {
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<AggregatedUserDetails> query = criteriaBuilder.createQuery(AggregatedUserDetails.class);
    Root<User> from = query.from(User.class);

    Predicate predicate = null;

    if ( userSpecification != null )
      predicate = userSpecification.toPredicate(from, query, criteriaBuilder);

    CompoundSelection<AggregatedUserDetails> construct = criteriaBuilder.construct(
        AggregatedUserDetails.class,
        criteriaBuilder.sum(from.get("salary")),
        criteriaBuilder.avg(from.get("age")),
        criteriaBuilder.sum(from.get("children"))
    );

    CriteriaQuery<AggregatedUserDetails> select = query.select(construct);

    if ( predicate != null )
      select.where(predicate);

    return entityManager.createQuery(select).getSingleResult();
  }
}

, который позволит вам использовать ваши спецификации Spring, избегая при этом лишних затрат на поиск всех соответствующих сущности и вычисление значения в приложении:

@DataJpaTest
@ExtendWith(SpringExtension.class)
public class UserDetailsFacadeTest {
  @TestConfiguration
  public static class UserDaoTestConfiguration {
    @Bean
    public UserDetailsFacade getUserDao(EntityManager entityManager) {
      return new UserDetailsFacade(entityManager);
    }
  }

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private UserDetailsFacade userDetailsFacade;

  @BeforeEach
  public void setUp() {
    User tom = new User();
    User dick = new User();
    User sally = new User();

    tom.setAge(17);
    dick.setAge(40);
    sally.setAge(35);

    tom.setChildren(0);
    dick.setChildren(1);
    sally.setChildren(0);

    tom.setSalary(240);
    dick.setSalary(40000);
    sally.setSalary(40000);

    userRepository.save(tom);
    userRepository.save(dick);
    userRepository.save(sally);
  }

  @Test
  public void testGetExpectedSummedSallary() {
    AggregatedUserDetails aggregatedUserDetails = userDetailsFacade.aggregatedUserDetails();
    assertThat(aggregatedUserDetails.getSalarySum(), is(80240L));
  }

  @Test
  public void testGetExpectedAverageAge() {
    AggregatedUserDetails aggregatedUserDetails = userDetailsFacade.aggregatedUserDetails();
    assertThat(Math.round(aggregatedUserDetails.getAgeAverage()), is(31L));
  }

  @Test
  public void testGetExpectedSummedChildren() {
    AggregatedUserDetails aggregatedUserDetails = userDetailsFacade.aggregatedUserDetails();
    assertThat(Math.round(aggregatedUserDetails.getChildrenSum()), is(1));
  }

  @Test
  public void testGetExpectedSummedSallaryOver1k() {
    AggregatedUserDetails aggregatedUserDetails = userDetailsFacade.aggregatedUserDetails(
        (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.ge(root.get("salary"), 1000));
    assertThat(aggregatedUserDetails.getSalarySum(), is(80000L));
  }
}

Вывод:

// com.example.demo.dao.UserDetailsFacadeTest#testGetExpectedAverageAge
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)

// com.example.demo.dao.UserDetailsFacadeTest#testGetExpectedSummedChildren
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: select sum(user0_.salary) as col_0_0_, avg(cast(user0_.age as double)) as col_1_0_, sum(user0_.children) as col_2_0_ from user user0_

// com.example.demo.dao.UserDetailsFacadeTest#testGetExpectedSummedSallaryOver1k
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: select sum(user0_.salary) as col_0_0_, avg(cast(user0_.age as double)) as col_1_0_, sum(user0_.children) as col_2_0_ from user user0_ where user0_.salary>=1000

// com.example.demo.dao.UserDetailsFacadeTest#testGetExpectedSummedSallary
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: insert into user (id, age, children, salary) values (null, ?, ?, ?)
Hibernate: select sum(user0_.salary) as col_0_0_, avg(cast(user0_.age as double)) as col_1_0_, sum(user0_.children) as col_2_0_ from user user0_
0 голосов
/ 06 февраля 2020

Я бы сделал это, выполнив собственный запрос SQL, который агрегирует:

Query q = em.createNativeQuery("SELECT a,b,c from ...");
List<Object[]> result = q.getResultList();

Список результатов будет содержать один объект [] на строку. Я предполагаю, что ваше утверждение вернет только одну строку, верно? Вы можете привести массив объектов для доступа к содержимому:

String     a= (String)     result.get(0)[0];
Long       b= (Long)       result.get(0)[1];
BigDecimal c= (BigDecimal) result.get(0)[2];

Если вы не уверены, какие типы Java возвращены, установите точку останова и проверьте список результатов с помощью отладчика. Конечно, это где-то задокументировано и также зависит от конфигурации.

Вместо использования Object вы можете указать JPA сопоставить результат с объектом POJO:

Query q = em.createNativeQuery("SELECT a,b,c from ...", MyResult.class);
List<MyResult> result = q.getResultList();

В этом случае myResult должен содержать атрибуты с соответствующими именами и типами, например:

class MyResult {
    private String a;
    private Long b;
    private BigDecimal c;

    // add setters ang getters here
}
...