将 Spring 数据 JPA EntityGraph 与 NamedAttributeNode 字段的延迟加载模式一起使用
Using Spring data JPA EntityGraph with LAZY load mode for NamedAttributeNode field
我面临 2 个问题:N + 1 查询和内存不足 (OOM)。
我通过分页和懒加载解决了OOM:
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Set<Employee> employees;
但是当我使用延迟加载时,发生了N + 1次查询。所以我尝试将 EntityGraph
用作 https://www.baeldung.com/spring-data-jpa-named-entity-graphs。但是作为我的研究和本地测试,EntityGraph
总是对 NamedAttributeNode
字段 - 关联字段进行预加载,我想延迟加载 - 首先不要加载所有数据:
@Entity
@Table(name = "department")
@NamedEntityGraph(name = "Department",
attributeNodes = {
@NamedAttributeNode("employees")
}
)
public class Department implements Serializable {
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Set<Employee> employees;
}
那么有什么方法可以同时获得它们吗?使用 EntityGraph
来避免 N + 1 和延迟加载来避免 OOM?
更新:
EntityGraph
可以有效地与 Pageable 配合使用吗?我的意思是不要在 JOIN 查询中加载所有数据。
使用 EntityGraph
,您所有的 NamedAttributeNode
关联都将加载到带有 Join
子句的 1 个查询中。启用sql日志查看hibernate在不同场景下加载实体的查询次数
logging.level.org.hibernate.SQL=DEBUG
您会看到使用 @OneToMany(fetch = FetchType.EAGER)
而不使用 EntityGraph
它会在单独的 select
查询 (N + 1) 中加载员工,但使用 EntityGraph
它只执行 1 select ... join
也不要忘记在存储库中指定实体图名称,例如:
@EntityGraph(value = "Department")
List<Department> findAll();
更新: Spring 数据分页在数据库端不起作用。它将获取所有数据,然后在内存中进行过滤。这就是它的工作原理。有一些解决方法,请查看此链接:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
Avoiding "HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!" using Spring Data
VladMihalcea Blog The best way to fix the Hibernate HHH000104
对我来说,解决方案可能是创建自定义存储库并使用 EntityManager
手动构建查询。
一个简单的解决方案是:
- 首先获取有分页但没有
@EntityGraph
的记录。
- 使用
@EntityGraph
从之前返回的 ID 中获取您的记录。
=> 没有 N+1 复杂性,只有 2 SQL 个请求,没有本地查询等
示例:
在您的主存储库中,添加 @EntityGraph
带有您要获取的关联的注释:
public interface CarRepository extends JpaRepository<Car, String>, JpaSpecificationExecutor<Car>, CarCustomRepository {
@Override
@EntityGraph(attributePaths = { "foo", "bar", "bar.baz" })
List<Car> findAllById(Iterable<String> ids);
}
使用 findAllWithEntityGraph
方法创建自定义存储库:
public interface CarCustomRepository {
Page<Car> findAllWithEntityGraph(Specification<Car> specification, Pageable pageable);
}
自定义存储库的实现。它首先获取没有实体图的实体,然后 re-fetch 它们带有实体图以加载关联。不要忘记 re-sort 个实体以保持顺序:
public class CarCustomRepositoryImpl implements CarCustomRepository {
@Autowired
@Lazy
private CarRepository carRepository;
@Override
public Page<Car> findAllWithEntityGraph(Specification<Car> specification, Pageable pageable) {
Page<Car> page = carRepository.findAll(specification, pageable);
List<String> ids = page.getContent().stream().map(Car::getId).collect(Collectors.toList());
List<Car> cars = carRepository.findAllById(ids).stream()
.sorted(Comparator.comparing(car -> ids.indexOf(car.getId())))
.collect(Collectors.toList());
return new PageImpl<>(cars, pageable, page.getTotalElements());
}
}
然后,您只需调用 CarRepository#findAllWithEntityGraph
方法来获取 with 分页和 no N+1 的记录复杂度。
问题是:为什么hibernate默认没有这种行为?
我面临 2 个问题:N + 1 查询和内存不足 (OOM)。
我通过分页和懒加载解决了OOM:
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Set<Employee> employees;
但是当我使用延迟加载时,发生了N + 1次查询。所以我尝试将 EntityGraph
用作 https://www.baeldung.com/spring-data-jpa-named-entity-graphs。但是作为我的研究和本地测试,EntityGraph
总是对 NamedAttributeNode
字段 - 关联字段进行预加载,我想延迟加载 - 首先不要加载所有数据:
@Entity
@Table(name = "department")
@NamedEntityGraph(name = "Department",
attributeNodes = {
@NamedAttributeNode("employees")
}
)
public class Department implements Serializable {
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Set<Employee> employees;
}
那么有什么方法可以同时获得它们吗?使用 EntityGraph
来避免 N + 1 和延迟加载来避免 OOM?
更新:
EntityGraph
可以有效地与 Pageable 配合使用吗?我的意思是不要在 JOIN 查询中加载所有数据。
使用 EntityGraph
,您所有的 NamedAttributeNode
关联都将加载到带有 Join
子句的 1 个查询中。启用sql日志查看hibernate在不同场景下加载实体的查询次数
logging.level.org.hibernate.SQL=DEBUG
您会看到使用 @OneToMany(fetch = FetchType.EAGER)
而不使用 EntityGraph
它会在单独的 select
查询 (N + 1) 中加载员工,但使用 EntityGraph
它只执行 1 select ... join
也不要忘记在存储库中指定实体图名称,例如:
@EntityGraph(value = "Department")
List<Department> findAll();
更新: Spring 数据分页在数据库端不起作用。它将获取所有数据,然后在内存中进行过滤。这就是它的工作原理。有一些解决方法,请查看此链接:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
Avoiding "HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!" using Spring Data
VladMihalcea Blog The best way to fix the Hibernate HHH000104
对我来说,解决方案可能是创建自定义存储库并使用 EntityManager
手动构建查询。
一个简单的解决方案是:
- 首先获取有分页但没有
@EntityGraph
的记录。 - 使用
@EntityGraph
从之前返回的 ID 中获取您的记录。
=> 没有 N+1 复杂性,只有 2 SQL 个请求,没有本地查询等
示例:
在您的主存储库中,添加 @EntityGraph
带有您要获取的关联的注释:
public interface CarRepository extends JpaRepository<Car, String>, JpaSpecificationExecutor<Car>, CarCustomRepository {
@Override
@EntityGraph(attributePaths = { "foo", "bar", "bar.baz" })
List<Car> findAllById(Iterable<String> ids);
}
使用 findAllWithEntityGraph
方法创建自定义存储库:
public interface CarCustomRepository {
Page<Car> findAllWithEntityGraph(Specification<Car> specification, Pageable pageable);
}
自定义存储库的实现。它首先获取没有实体图的实体,然后 re-fetch 它们带有实体图以加载关联。不要忘记 re-sort 个实体以保持顺序:
public class CarCustomRepositoryImpl implements CarCustomRepository {
@Autowired
@Lazy
private CarRepository carRepository;
@Override
public Page<Car> findAllWithEntityGraph(Specification<Car> specification, Pageable pageable) {
Page<Car> page = carRepository.findAll(specification, pageable);
List<String> ids = page.getContent().stream().map(Car::getId).collect(Collectors.toList());
List<Car> cars = carRepository.findAllById(ids).stream()
.sorted(Comparator.comparing(car -> ids.indexOf(car.getId())))
.collect(Collectors.toList());
return new PageImpl<>(cars, pageable, page.getTotalElements());
}
}
然后,您只需调用 CarRepository#findAllWithEntityGraph
方法来获取 with 分页和 no N+1 的记录复杂度。
问题是:为什么hibernate默认没有这种行为?