java - 使用 JPA 规范过滤优化聚合值选择

标签 java spring-boot spring-data-jpa spring-data

我有以下问题 - 我需要从数据库中选择聚合(摘要)数据。将会有很多过滤器会动态更改,并且这种过滤功能已经使用我想使用的 Spring Data JPA 规范支持和实现。

我想优化实际的解决方案,该解决方案有效但消耗大量内存。我需要总结所有用户的工资、 child 的数量,并根据规范得到他们的平均年龄。我有这样的方法

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

我可以检索匹配过滤条件的所有用户的列表,并对它们进行迭代,并在 Java 中聚合所需的值,这是完美的。但如果获取的用户数量太大,则会消耗大量内存,这就是问题。

在我看来,最好将这些值聚合到数据库中,并仅从数据库返回 3 个数字到 Java -> 求和用户的工资、所有 child 的数量以及所选用户的平均年龄。

有人可以帮我什么是最佳解决方案吗?或者有没有办法将 JPA 规范与仅将聚合数据从 DB 返回到 Java 相结合?

非常感谢。

最佳答案

正如您所知,您不能将自定义查询与规范结合起来。可以通过 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...
}

您可以执行以下操作(我对您的 User 实体做了一些假设...):

更新:请注意,UserDetailsFacade 类名的“Facade”部分指的是《分析模式》(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_

关于java - 使用 JPA 规范过滤优化聚合值选择,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60102136/

相关文章:

java - 将 OC4J 服务器适配器与 Eclipse Kepler 结合使用

java - 如何通过按钮更改后对齐 imageView

java - 带有可选参数的 Spring @RequestMapping

java - 如何在本地使用 spring-cloud-starter-aws 运行应用程序?

java - 使用EntityManager的Spring数据: NullPointerException

java - Spring REST URI 存在检查

java - 在继续之前等待 AsyncCallback

java - Spring boot Reactive 应用程序启动 tomcat 代替 Netty

mysql - 当尝试使用现有字段插入新实体时,Spring 抛出 PersistentObjectException

spring-boot - javassist不能与Spring Boot Jar一起使用