繁体   English   中英

Hibernate LEFT JOIN FETCH with ON 子句或替代

[英]Hibernate LEFT JOIN FETCH with ON clause or alternative

我有亲子关系。

父 class 具有以下字段(id, name) ,子 class 具有以下列(id, name, date, parent_id) 我想返回的最终结果 JSON 如下。 即使没有孩子,我也总是想返回父母,这就是为什么我在孩子LEFT OUTER JOINON子句的原因

[

{
    "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

在父 class 我有一个 map 这个来引用孩子

@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

问题是 hibernate 自动运行第二个查询,如下所示:

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

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

我将其作为 API 与 spring 启动运行,但没有额外的处理,这是我的 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 class 中拥有的

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
}

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

解决方案


我找到了用于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 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)或单独的索引列,仅使用准备好的值进行搜索,并尝试通过连接表索引,这些建议将加快您的查询执行速度。

您的原始查询:

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

看起来不错,它会独立返回所有Parent实体,如果它们有或没有你需要的children

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

并使用 controller 中的服务:

@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)

另一方面,列出所有Parent (例如通过使用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);
  }

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM