解決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)); } }
此時可以看到控制檯裡面只發了一條聯表查詢就得到了關聯物件。