1. 程式人生 > >解決JPA懶載入典型的N+1問題-註解@NamedEntityGraph

解決JPA懶載入典型的N+1問題-註解@NamedEntityGraph

因為在設計一個樹形結構的實體中用到了多對一,一對多的對映關係,在載入其關聯物件的時候,為了效能考慮,很自然的想到了懶載入。

也由此遇到了N+1的典型問題 : 通常1的這方,通過1條SQL查詢得到1個物件,而JPA基於Hibernate,fetch策略預設為select(並非聯表查詢),由於關聯的存在 ,又需要將這個物件關聯的集合取出,集合數量是N,則要發出N條SQL,於是本來的1條聯表查詢SQL可解決的問題變成了N+1條SQL

我採取的解決方法是 : 不修改懶載入策略,JPA也不寫native SQL,通過聯表查詢進行解決。

如果對該例子比較感興趣或者覺得言語表達比較囉嗦,可檢視完整的demo地址 : https://github.com/EalenXie/springboot-jpa-N-plus-One

場景如下 : 

  我設計了一個典型的二叉樹結構實體叫做Area,代表的含義是區域 (省、市、區)。省是樹的一級根節點,市是省的子節點,區是市的子節點。如 : 廣東省,廣州市,天河區

  1 . Area實體設計採用自關聯,關聯的子集fetch策略為懶載入。

package name.ealen.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.List; /** * Created by EalenXie on 2018/10/16 16:49. * 典型的 多層級 區域關係 */ @Entity @Table(name = "jpa_area") public class Area { /** * Id 使用UUID生成策略 */ @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private String id; /** * 區域名 */ private String name; /** * 一個區域資訊下面很多子區域(多級) 比如 : 廣東省 (子)區域 : 廣州市 (孫)子區域 : 天河區 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") @JsonIgnore private Area parent; @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) private List<Area> children; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Area getParent() { return parent; } public void setParent(Area parent) { this.parent = parent; } public List<Area> getChildren() { return children; } public void setChildren(List<Area> children) { this.children = children; } }

  

  2 . 為Area寫一個簡單的dao進行資料庫訪問:AreaRepository

 

package name.ealen.dao;

import name.ealen.entity.Area;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by EalenXie on 2018/10/16 16:56.
 */
public interface AreaRepository extends JpaRepository<Area, String> {
}

  

  3.  現在來進行一波關鍵性的測試 : 首先我們插入資料測試 : 

 

    @Autowired
    private AreaRepository areaRepository;

    /**
     * 新增區域測試
     */
    @Test
    public void addArea() {

        // 廣東省 (頂級區域)
        Area guangdong = new Area();
        guangdong.setName("廣東省");
        areaRepository.save(guangdong);

        //廣東省 下面的 廣州市(二級區域)
        Area guangzhou = new Area();
        guangzhou.setName("廣州市");
        guangzhou.setParent(guangdong);
        areaRepository.save(guangzhou);

        //廣州市 下面的 天河區(三級區域)
        Area tianhe = new Area();
        tianhe.setName("天河區");
        tianhe.setParent(guangzhou);
        areaRepository.save(tianhe);

        //廣東省 下面的 湛江市(二級區域)
        Area zhanjiang = new Area();
        zhanjiang.setName("湛江市");
        zhanjiang.setParent(guangdong);
        areaRepository.save(zhanjiang);

        //湛江市 下面的 霞山區(三級區域)
        Area xiashan = new Area();
        xiashan.setName("霞山區");
        xiashan.setParent(zhanjiang);
        areaRepository.save(xiashan);

    }

   

  4 . 進行查詢,並觸發懶載入 : 

 

    /**
     * 觸發懶載入查詢 典型的 N+1 現象
     */
    @Test
    @Transactional
    public void findAllArea() {
        List<Area> areas = areaRepository.findAll();
        System.out.println(JSONArray.toJSONString(areas.get(0)));
    }

  此時,我們可以在控制檯中看到,觸發了懶載入,導致了N+1的問題。

  上面我們首先發出 1 條SQL查出了所有的Area物件,然後為了取第一個中的關聯物件發了5條SQL。

 

解決的方法如下 : 

  1 . 首先在實體上面註解@NamedEntityGraph,指明name供查詢方法使用,attributeNodes 指明被標註為懶載入的屬性節點

 

  如下 : Category實體

 

package name.ealen.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.Set;

/**
 * Created by EalenXie on 2018/10/16 16:13.
 * 典型的 多層級 分類
 * <p>
 * :@NamedEntityGraph :註解在實體上 , 解決典型的N+1問題
 * name表示實體圖名, 與 repository中的註解 @EntityGraph的value屬性相對應,
 * attributeNodes 表示被標註要懶載入的屬性節點 比如此例中 : 要懶載入的子分類集合children
 */

@Entity
@Table(name = "jpa_category")
@NamedEntityGraph(name = "Category.Graph", attributeNodes = {@NamedAttributeNode("children")})
public class Category {

    /**
     * Id 使用UUID生成策略
     */
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    private String id;

    /**
     * 分類名
     */
    private String name;

    /**
     * 一個商品分類下面可能有多個商品子分類(多級) 比如 分類 : 家用電器  (子)分類 : 電腦  (孫)子分類 : 膝上型電腦
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    @JsonIgnore
    private Category parent;                //父分類

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private Set<Category> children;       //子分類集合

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Category getParent() {
        return parent;
    }

    public void setParent(Category parent) {
        this.parent = parent;
    }

    public Set<Category> getChildren() {
        return children;
    }

    public void setChildren(Set<Category> children) {
        this.children = children;
    }
}

     2 . 在訪問的dao的查詢方法上面註解@EntityGraph,value屬性值為@NamedEntityGraph的name屬性值,如 CategoryRepository : 

package name.ealen.dao;

import name.ealen.entity.Category;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * Created by EalenXie on 2018/10/16 16:19.
 */
public interface CategoryRepository extends JpaRepository<Category, String> {
    /**
     * 解決 懶載入 JPA 典型的 N + 1 問題
     */
    @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)
    List<Category> findAll();
}

  

  3 . 進行測試 : 新增一些分類 

 

  @Autowired
    private CategoryRepository categoryRepository;

    /**
     * 新增分類測試
     */
    @Test
    public void addCategory() {

        //一個 家用電器分類(頂級分類)
        Category appliance = new Category();
        appliance.setName("家用電器");
        categoryRepository.save(appliance);

        //家用電器 下面的 電腦分類(二級分類)
        Category computer = new Category();
        computer.setName("電腦");
        computer.setParent(appliance);
        categoryRepository.save(computer);

        //電腦 下面的 膝上型電腦分類(三級分類)
        Category notebook = new Category();
        notebook.setName("膝上型電腦");
        notebook.setParent(computer);
        categoryRepository.save(notebook);

        //家用電器 下面的 手機分類(二級分類)
        Category mobile = new Category();
        mobile.setName("手機");
        mobile.setParent(appliance);
        categoryRepository.save(mobile);

        //手機 下面的 智慧機 / 老人機(三級分類)
        Category smartPhone = new Category();
        smartPhone.setName("智慧機");
        smartPhone.setParent(mobile);
        categoryRepository.save(smartPhone);

        Category oldPhone = new Category();
        oldPhone.setName("老人機");
        oldPhone.setParent(mobile);
        categoryRepository.save(oldPhone);
    }

   

  進行查詢 ,並觸發懶載入 : 

 

    /**
     * 查詢分類測試  已經解決了經典的 N+1 問題
     */
    @Test
    @Transactional
    public void findCategory() {
        List<Category> categories = categoryRepository.findAll();
        for (Category category : categories) {
            System.out.println(JSONArray.toJSONString(category));
        }
    }

  此時可以看到控制檯裡面只發了一條聯表查詢就得到了關聯物件。