带有 ON 子句或替代方法的休眠 LEFT JOIN FETCH

Posted

技术标签:

【中文标题】带有 ON 子句或替代方法的休眠 LEFT JOIN FETCH【英文标题】:Hibernate LEFT JOIN FETCH with ON clause or alternative 【发布时间】:2021-04-15 08:47:20 【问题描述】:

我有亲子关系。

父类具有以下字段(id, name),子类具有以下列(id, name, date, parent_id)。我想要返回的最终结果 JSON 如下。即使它没有孩子,我也总是想返回父母,这就是为什么我在孩子身上LEFT OUTER JOIN 并带有ON 子句

[


    "name": "parnet1",
    "child": 
      "2021-01-01": 
        "name": "child1"
      ,
      "2021-01-02": 
        "name": "child2"
      
    
  ,
  
    "name": "parnet2",
    "child": 
    
  
]

我的数据库在此示例中的外观示例

parents

id    |     name
1     |     parent 1
2     |     parent 2

child

id    |     name    |    date    |    parent_id
1     |     child1  |  2021-01-01|    1
2     |     child2  |  2021-01-02|    1
3     |     child3  |  2020-12-31|    2

对于以下示例,我将传递 2021-01-01 的日期

所以月份是一月 (1),年份是 2021

在父类中,我有一个映射来引用子类

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JsonManagedReference
@MapKey(name = "date")
private Map<LocalDate, Child> children;

这是我的查询

@Query("select p from Parent p left join p.child c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth

这个问题是休眠后自动运行的第二个查询如下:

select * from Child where c.parent_id = ?

这最终会得到所有连接条件不成立的孩子。

然后我尝试了这个

 @Query("select new Parent(p.id, p.name, c.id, c.date, c.name) from Parent p left join p.child c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth

并制作了这个构造函数

public Parent(int id, String name, int childId, LocalDate date, String childName) 

    this.id = id;
    this.name = name;
    this.children = new HashMap<LocalDate, Child>();
    if (childId != null) 
        Child child = new Child();
        child.setId(id);
        child.setName(name);
        this.children.put(date, child);
    

但问题是,当我希望数组的顶层长度为 2 时,我得到一个长度为 4 的 JSON 数组,因为目前数据库中只有 2 个父级。

如何修改它以获得我想要的 JSON 有效负载,它发布在问题的顶部。

非常感谢

编辑

如果我要传入 date = '2021-01-01',则不会返回 child2,因此使用以下内容不起作用

select distinct p from Parent p 
left join fetch p.children c 
where (YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMont) 
or c.parent is null

EDIT 2(关于自动生成的查询)

我使用 Spring Boot 将其作为 API 运行,但没有额外处理,这是我的 API 和当前查询。

@GetMapping(path = "/parents/date", produces =  "application/json" )
@Operation(summary = "Get all parents for date")
public @ResponseBody List<Parent> getParentsByDate(@PathVariable("date") String dateString) 

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    LocalDate dateOccurred = LocalDate.parse(dateString, formatter);

    return parentRepository.getParentsForDate(dateOccurred);



然后在我的存储库中,我只有我的 @Query,这是我目前在 ParentRepository 类中拥有的内容

public interface ParentRepository extends CrudRepository<Parent, Integer> 

    @Query("select p from Parent p left join fetch p.children c where (YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth) or c.parent is null")
    public List<Parent> getParentsForDate(@Param("dateOccurred") LocalDate dateOccurred

但正如所说的那样,问题是空检查确实可以正常工作,如果父母在另一个月份有任何孩子,但不是当前的孩子,父母不会返回

【问题讨论】:

你在哪个领域加入了他们? YEAR(c.date) = :dateYear 是什么意思?不应该是@Query("select p from Parent p left join p.child c on c.parent_id = p.id WHERE YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth?跨度> @MrFisherman 我也总是想返回父级,即使它没有子级,这就是为什么我在子级上使用 ON 子句 LEFT OUTER JOIN,而不是在 WHERE YEAR(c.日期) = :dateYear 和 MONTH(c.date) = :dateMonth。我已经更新了这个问题 @iqueqiorio 请指出第二个查询select * from Child where c.parent_id = ? 的生成时间?我的意思是,您是否以任何方式处理实体?例如,您会将它们转换为 JSON 吗? @jccampanero 生成第二个查询并在我的查询注释查询之后运行,但我在代码中没有做任何事情来调用它。我正在使用 spring boot 并相信这是生成它我已经发布了第二次编辑 非常感谢。我将尝试提供有关该信息的答案。只有一件事:您是否以任何方式使用@Transactional?如果是这样,您能指出在哪里吗? 【参考方案1】:

解决方案


我为join 操作找到了获取和多条件的解决方案: 我们可以申请EntityGraph 功能:

@NamedEntityGraph(name = "graph.Parent.children", attributeNodes = @NamedAttributeNode("children"))
public class Parent 
 ...

在存储库中,您需要将这个EntityGraphQuery 链接起来,如下所示:

...
@Query("select distinct p from Parent p left join p.children c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMont")
@EntityGraph(value = "graph.Parent.children")
List<Parent> findAllParents(Integer dateYear, Integer dateMont);
...

以前的解决方案


如您所知,Hibernate 不允许在 on 中为 fetch join 使用多个条件,结果:with-clause not allowed on fetched associations; use filters 异常,因此我建议使用以下 HQL 查询:

select distinct p from Parent p 
left join fetch p.children c 
where (YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMont) 
or c.parent is null

注意:distinct是避免父实体重复所必需的,关于有文章:https://thorben-janssen.com/hibernate-tips-apply-distinct-to-jpql-but-not-sql-query/

因此,Hibernate 将生成一个支持没有孩子的父母的查询,如果应该按照发布的 json 示例为孩子订购,您需要为 children 字段添加 @SortNatural

@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JsonManagedReference
@MapKey(name = "date")
@SortNatural
private Map<LocalDate, Child> children;

结果,json 将与发布的相同。

建议:不要在运行时使用计算值,因为这些操作索引无法应用,在您的情况下,我建议使用虚拟列(如果是 mysql)或仅在准备好的情况下使用单独的索引列搜索值,并尝试按索引连接表,这些建议将加快您的查询执行速度。

【讨论】:

感谢您的回复,问题是,如果父级有任何子级,则查询的 null 部分不起作用并且不会返回父级 @iqueqiorio 抱歉,我错过了过滤案例,我更新了答案,请参阅解决问题的新方法。 *** 上有无数线程可以处理这个问题(如症状 with-clause not allowed on fetched associations; use filters 所示),而您使用 EntityGraph 的解决方案是我能找到的唯一可行的解​​决方案。干得好!【参考方案2】:

您的原始查询:

@Query("select p from Parent p left join p.child c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth")

看起来不错,它会根据您的需要独立返回所有Parent 实体,无论它们是否有children

问题是之后,出于任何原因,可能当您将信息序列化为 JSON 格式时,对于原始查询返回的每个 parentOneToManychildren 的关系正在解决。结果,您提到的第二个查询被执行,您获得的结果children 不符合过滤条件。

虽然我不清楚你的事务分界是在哪里建立的,但这可能会发生,因为当实体被序列化时,它们仍然与持久性上下文相关联 - 相反,它会导致延迟集合初始化出现问题。

您可以尝试的一个选项是在序列化这些实体之前将它们从持久性上下文中分离出来。

要实现该目标,首先,在您的查询中包含 fetch 术语:

@Query("select p from Parent p left join fetch p.children c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth"

不幸的是,Spring Data 没有为分离实体提供开箱即用的解决方案,但您可以轻松实现必要的代码。

首先,定义一个基础仓库接口:

import java.io.Serializable;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

@NoRepositoryBean
public interface CustomRepository<T, ID extends Serializable> extends JpaRepository<T, ID> 
  void detach(T entity);

及其实现:


import java.io.Serializable;

import javax.persistence.EntityManager;

import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

public class CustomRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CustomRepository<T, ID> 

  private final EntityManager entityManager;

  public CustomRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) 
    super(entityInformation, entityManager);
    this.entityManager = entityManager;
  

  public CustomRepositoryImpl(Class<T> domainClass, EntityManager entityManager) 
    super(domainClass, entityManager);
    this.entityManager = entityManager;
  

  public void detach(T entity) 
    this.entityManager.detach(entity);
  


现在,为新存储库创建一个工厂 bean:

import java.io.Serializable;

import javax.persistence.EntityManager;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;

public class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> 

  public CustomRepositoryFactoryBean(Class<? extends R> repositoryInterface) 
    super(repositoryInterface);
  

  @Override
  protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) 
    return new CustomRepositoryFactory(entityManager);
  

  private static class CustomRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory 

    private final EntityManager em;

    public CustomRepositoryFactory(EntityManager em) 

      super(em);
      this.em = em;
    

    protected <T, ID extends Serializable> SimpleJpaRepository<?, ?> getTargetRepository(RepositoryMetadata metadata, EntityManager entityManager) 
      SimpleJpaRepository<?, ?> repo = new CustomRepositoryImpl(metadata.getDomainType(), entityManager);
      return repo;
    

    protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) 
      return CustomRepositoryImpl.class;
    
  


这个工厂必须在你的@EnableJpaRepositories注解中注册:

@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositoryFactoryBean.class /* your other configuration */)

您的存储库需要实现新定义的:

public interface ParentRepository extends CustomRepository<Parent, Integer> 

    @Query("select p from Parent p left join fetch p.children c where (YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth) or c.parent is null")
    @Transactional(readOnly=true)
    public List<Parent> getParentsForDate(@Param("dateOccurred") LocalDate dateOccurred)

创建一个新服务:

public interface ParentService 
  List<Parent> getParentsForDate(LocalDate dateOccurred);

以及对应的实现:

@Service
public class ParentServiceImpl implements ParentService 
  private final ParentRepository parentRepository;

  public ParentServiceImpl(ParentRepository parentRepository) 
    this.parentRepository = parentRepository;
  

  @Override
  @Transactional
  public List<Parent> getParentsForDate(LocalDate dateOccurred) 
    Lis<Parent> parents = parentRepository.getParentsForDate(dateOccurred);

    // Now, the tricky part... I must recognize that I never tried
    // something like this, but I think it should work
    parents.forEach(parent -> parentRepository.detach(parent));
    return parents;
  

并使用控制器中的服务:

@GetMapping(path = "/parents/date", produces =  "application/json" )
@Operation(summary = "Get all parents for date")
public @ResponseBody List<Parent> getParentsByDate(@PathVariable("date") String dateString) 

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    LocalDate dateOccurred = LocalDate.parse(dateString, formatter);

    return parentService.getParentsForDate(dateOccurred);


我没有测试过代码,我不知道这种分离实体的方式是否可行,但我认为它可能会。

如果这种方法不起作用,我的建议是您尝试不同的方法。

不用查询父级,直接过滤子级,获取符合过滤条件的子级。

@Query("select c from Child c on YEAR(c.date) = :dateYear and MONTH(c.date) = :dateMonth)

另一方面,列出所有Parents(例如使用findAll),并创建某种中间POJO,一些DTO,以混合获得的几个结果的信息,返回给客户的信息:

public class ParentDTO 
  private long id;
  private String name;
  private Map<LocalDate, ChildDTO> children = Collections.emptyMap();

  // Setters and getters

public class ChildDTO 
  private long id;
  private String name;
  private LocalDate date;

  // Setters and getters

  List<Child> children = /* the aforementioned query conforming to filter criteria */
  List<Parent> parents = parentRepository.findAll();

  List<ParentDTO> parentDTOs = new ArrayList(parents.size());
  for (Parent parent:parents) 
    ParentDTO parentDTO = new ParentDTO();
    parentDTOs.add(parentDTO);

    parentDTO.setId(parent.getId());
    parentDTO.setName(parent.getName());

    // Contains parent?
    if (children == null || children.isEmpty()) 
      continue;
    

    Map<LocalDate, ChildDTO> childrenForThisParent = children.stream()
      .filter(child -> child.parent.equals(parent))
      .map(child -> 
        ChildDTO childDTO = new ChildDTO();
        childDTO.setId(child.getId());
        childDTO.setName(child.getName());
        childDTO.setDate(child.getDate());
        return childDTO;
      )
      .collect(Collectors.toMap(c -> c.getDate(), c));

    parentTO.setChildren(childrenForThisParent);
  

代码可以通过多种方式进行优化,但我希望您能理解。

【讨论】:

以上是关于带有 ON 子句或替代方法的休眠 LEFT JOIN FETCH的主要内容,如果未能解决你的问题,请参考以下文章

mysql left join的深入探讨

“java.sql.SQLSyntaxErrorException:'on 子句'中的未知列'*'”在休眠条件下加入表时

使用带有休眠 SQL 查询的多列的参数化 IN 子句

MySQL LEFT JOIN 你可能需要了解的三点

关于 MySQL LEFT JOIN 你可能需要了解的三点

为啥以及何时在 WHERE 子句中带有条件的 LEFT JOIN 不等于在 ON 中的相同 LEFT JOIN? [复制]