我的项目中有三个模型对象之间的关系(帖子末尾的模型和存储库片段。
当我拨打PlaceRepository.findById
时,它会触发三个选择查询:
( “SQL”)
SELECT * FROM place p where id = arg
SELECT * FROM user u where u.id = place.user.id
SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id
这是一种相当不寻常的行为(对我而言)。在阅读Hibernate文档后我可以告诉它应该总是使用JOIN查询。 FetchType.LAZY
类中FetchType.EAGER
更改为Place
时的查询(使用其他SELECT查询)时查询没有区别,City
类FetchType.LAZY
时相同}更改为FetchType.EAGER
(使用JOIN查询)。
当我使用CityRepository.findById
抑制火力时,有两个选择:
SELECT * FROM city c where id = arg
SELECT * FROM state s where id = city.state.id
我的目标是在所有情况下都有一个sam行为(总是JOIN或SELECT,但首选JOIN)。
模型定义:
地点:
@Entity
@Table(name = "place")
public class Place extends Identified {
@Fetch(FetchMode.JOIN)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_user_author")
private User author;
@Fetch(FetchMode.JOIN)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "area_city_id")
private City city;
//getters and setters
}
城市:
@Entity
@Table(name = "area_city")
public class City extends Identified {
@Fetch(FetchMode.JOIN)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "area_woj_id")
private State state;
//getters and setters
}
存储库:
PlaceRepository
public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
Place findById(int id);
}
UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAll();
User findById(int id);
}
CityRepository:
public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {
City findById(int id);
}
答案 0 :(得分:84)
我认为Spring Data忽略了FetchMode。在使用Spring Data
时,我总是使用@NamedEntityGraph
和@EntityGraph
注释
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {
// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();
…
}
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);
}
查看文档 here
答案 1 :(得分:41)
首先,@Fetch(FetchMode.JOIN)
和@ManyToOne(fetch = FetchType.LAZY)
是对立的,一个指示EAGER取物,而另一个指示LAZY取指。
Eager fetching是rarely a good choice,对于可预测的行为,最好使用查询时JOIN FETCH
指令:
public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
@Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id")
Place findById(@Param("id") int id);
}
public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {
@Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id")
City findById(@Param("id") int id);
}
答案 2 :(得分:15)
Spring-jpa使用实体管理器创建查询,如果查询是由实体管理器构建的,Hibernate将忽略获取模式。
以下是我使用过的工作:
Implement a custom repository witch inherits from SimpleJpaRepository
覆盖方法 getQuery(Specification<T> spec, Sort sort)
:
@Override
protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> query = builder.createQuery(getDomainClass());
Root<T> root = applySpecificationToCriteria(spec, query);
query.select(root);
applyFetchMode(root);
if (sort != null) {
query.orderBy(toOrders(sort, root, builder));
}
return applyRepositoryMethodMetadata(entityManager.createQuery(query));
}
在方法的中间,添加 applyFetchMode(root);
以应用获取模式,以使Hibernate使用正确的连接创建查询。
(不幸的是,我们需要从基类复制整个方法和相关的私有方法,因为没有其他扩展点。)
实施 applyFetchMode
:
private void applyFetchMode(Root<T> root) {
for (Field field : getDomainClass().getDeclaredFields()) {
Fetch fetch = field.getAnnotation(Fetch.class);
if (fetch != null && fetch.value() == FetchMode.JOIN) {
root.fetch(field.getName(), JoinType.LEFT);
}
}
}
答案 3 :(得分:2)
&#34; FetchType.LAZY
&#34;只会触发主表。如果在您的代码中调用任何其他具有父表依赖关系的方法,那么它将触发查询以获取该表信息。 (FIRES MULTIPLE SELECT)
&#34; FetchType.EAGER
&#34;将直接创建包括相关父表的所有表的连接。 (使用JOIN
)
何时使用:
假设您强制需要使用依赖父表信息,然后选择FetchType.EAGER
。
如果您只需要某些记录的信息,请使用FetchType.LAZY
。
请记住,如果您选择检索父表信息,FetchType.LAZY
需要在代码中的某个位置有一个活动的数据库会话工厂。
E.g。 LAZY
:
.. Place fetched from db from your dao loayer
.. only place table information retrieved
.. some code
.. getCity() method called... Here db request will be fired to get city table info
答案 4 :(得分:2)
我详细阐述了dream83619的答案,使其处理嵌套的Hibernate @Fetch
注释。我使用递归方法在嵌套的关联类中查找注释。
So you have to implement custom repository并覆盖getQuery(spec, domainClass, sort)
方法。
不幸的是,您还必须复制所有引用的私有方法:(。
这是代码,省略了复制的私有方法。
编辑:添加了剩余的私有方法。
@NoRepositoryBean
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {
private final EntityManager em;
protected JpaEntityInformation<T, ?> entityInformation;
public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.em = entityManager;
this.entityInformation = entityInformation;
}
@Override
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
applyFetchMode(root);
if (sort != null) {
query.orderBy(toOrders(sort, root, builder));
}
return applyRepositoryMethodMetadata(em.createQuery(query));
}
private Map<String, Join<?, ?>> joinCache;
private void applyFetchMode(Root<? extends T> root) {
joinCache = new HashMap<>();
applyFetchMode(root, getDomainClass(), "");
}
private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) {
for (Field field : clazz.getDeclaredFields()) {
Fetch fetch = field.getAnnotation(Fetch.class);
if (fetch != null && fetch.value() == FetchMode.JOIN) {
FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT);
String fieldPath = path + "." + field.getName();
joinCache.put(path, (Join) descent);
applyFetchMode(descent, field.getType(), fieldPath);
}
}
}
/**
* Applies the given {@link Specification} to the given {@link CriteriaQuery}.
*
* @param spec can be {@literal null}.
* @param domainClass must not be {@literal null}.
* @param query must not be {@literal null}.
* @return
*/
private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
CriteriaQuery<S> query) {
Assert.notNull(query);
Assert.notNull(domainClass);
Root<U> root = query.from(domainClass);
if (spec == null) {
return root;
}
CriteriaBuilder builder = em.getCriteriaBuilder();
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
return root;
}
private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
if (getRepositoryMethodMetadata() == null) {
return query;
}
LockModeType type = getRepositoryMethodMetadata().getLockModeType();
TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);
applyQueryHints(toReturn);
return toReturn;
}
private void applyQueryHints(Query query) {
for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) {
query.setHint(hint.getKey(), hint.getValue());
}
}
public Class<T> getEntityType() {
return entityInformation.getJavaType();
}
public EntityManager getEm() {
return em;
}
}
答案 5 :(得分:2)
仅当通过id 选择对象(即使用entityManager.find()
时,提取模式才能工作。由于Spring Data将始终创建查询,因此获取模式配置将对您毫无用处。您可以对提取联接使用专用查询,也可以使用实体图。
要获得最佳性能时,应仅选择真正需要的数据子集。为此,通常建议使用DTO方法以避免获取不必要的数据,但这通常会导致很多容易出错的样板代码,因为您需要定义一个专用查询,该查询通过JPQL构造DTO模型构造函数表达式。
Spring Data投影可以在这里提供帮助,但有时需要使用Blaze-Persistence Entity Views之类的解决方案,该解决方案非常简单,并且袖子中有很多功能可以派上用场!您只需为每个实体创建一个DTO接口,其中的吸气剂表示您需要的数据子集。解决您的问题的方法可能是这样
@EntityView(Identified.class)
public interface IdentifiedView {
@IdMapping
Integer getId();
}
@EntityView(Identified.class)
public interface UserView extends IdentifiedView {
String getName();
}
@EntityView(Identified.class)
public interface StateView extends IdentifiedView {
String getName();
}
@EntityView(Place.class)
public interface PlaceView extends IdentifiedView {
UserView getAuthor();
CityView getCity();
}
@EntityView(City.class)
public interface CityView extends IdentifiedView {
StateView getState();
}
public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
PlaceView findById(int id);
}
public interface UserRepository extends JpaRepository<User, Long> {
List<UserView> findAllByOrderByIdAsc();
UserView findById(int id);
}
public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {
CityView findById(int id);
}
免责声明,我是Blaze-Persistence的作者,所以我可能会有偏见。
答案 6 :(得分:1)
根据弗拉德·米哈尔西娅(见https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/):
JPQL查询可能会覆盖默认的获取策略。如果我们不这样做 使用内部或左连接明确声明我们要获取的内容 提取指令,将应用默认的选择提取策略。
似乎JPQL查询可能会覆盖您声明的获取策略,因此您必须使用join fetch
来热切加载某些引用的实体,或者只是通过EntityManager的id加载(这将遵循您的获取策略,但可能并不是您的用例的解决方案。
答案 7 :(得分:1)
spring data jpa 使用的实体管理器忽略了 fetch 模式。
在 Repository 方法上使用 @EntityGraph 注解,
@EntityGraph(attributePaths = { "user", "hashtags"})
Page<LIPost> findByVoteTypeIn(Set<VoteType> listOfVotetype, Pageable paging);
这里的 user 和 hashtags 是 LIPost 实体中的属性。
Spring Data JPA 构建的查询使用左外连接来获取相关实体(用户和主题标签)数据。
在这种情况下,不需要在实体类上使用注解@NamedEntityGraph。
答案 8 :(得分:0)
如果你在Hibernate之上使用JPA,就没有办法将Hibernate使用的FetchMode设置为JOINHownate,如果你在Hibernate之上使用JPA,就没有办法将Hibernate使用的FetchMode设置为JOIN
Spring Data JPA库提供了域驱动设计规范API,允许您控制生成的查询的行为。
final long userId = 1;
final Specification<User> spec = new Specification<User>() {
@Override
public Predicate toPredicate(final Root<User> root, final
CriteriaQuery<?> query, final CriteriaBuilder cb) {
query.distinct(true);
root.fetch("permissions", JoinType.LEFT);
return cb.equal(root.get("id"), userId);
}
};
List<User> users = userRepository.findAll(spec);