簡體   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