利用@NamedEntityGraph解決N+1查詢問題


上一文中我們使用@ManyToOne、@OneToMany進行自關聯查詢,遇到的“N+1”問題需要通過@NamedEntityGraph來解決。

Entity:

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

@Entity
@Table
@Data
@NamedEntityGraph(name = "Category.Graph", attributeNodes = {@NamedAttributeNode("children")})
public class Category {
    @Id
    @GeneratedValue
    private Long id;

    // 分類名
    private String name;

    // 一個商品分類下面可能有多個商品子分類(多級) 比如 分類 : 家用電器  (子)分類 : 電腦  (孫)子分類 : 筆記本電腦
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnore
    private Category parent;                //父分類

//    @JsonInclude(JsonInclude.Include.NON_EMPTY)   // 不要使用值為null或內容為空的屬性。
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private List<Category> children;       //子分類集合,用Set代替List時,請在類上添加注解@EqualsAndHashCode(exclude = "children")
}

Repository:

public interface CategoryRepository extends JpaRepository<Category, Long> {
    /**
     * 解決 懶加載 JPA 典型的 N + 1 問題
     */
    @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)
    List<Category> findAll();

    @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)  // 無效, 始終存在N+1問題
    Category findByName(String name);

    @EntityGraph(value = "Category.Graph", type = EntityGraph.EntityGraphType.FETCH)  // 無效, 始終存在N+1問題
    Optional<Category> findById(Long id);
}

Controller:

@RestController
public class Output {

    @Autowired
    private CategoryRepository categoryRepository;

    @GetMapping("category")
    public Category getCategory() {
        List<Category> categories = categoryRepository.findAll();
        // return categories.get(0); // 一條SQL
        // return categories.stream().filter(category -> category.getName().equals("家用電器")).findFirst().orElse(null); // 一條SQL
        return categoryRepository.findByName("家用電器"); // 無論findByName(String name)方法加不加@EntityGraph,都不會觸發N+1查詢,對比下文更神奇
    }

    @GetMapping("category/name/{name}")
    public Category getCategoryByName(@PathVariable String name) {
        // 雖然findByName(String name)添加了@EntityGraph,但是沒有起作用, 依然存在N+1問題
        return categoryRepository.findByName(name);
    }

    @GetMapping("category/id/{id}")
    public Category getCategoryById(@PathVariable Long id) {
        // 雖然findById(Long id)添加了@EntityGraph,但是沒有起作用, 依然存在N+1問題
        return categoryRepository.findById(id).orElse(null);
    }
    
    @GetMapping("categories")
    public List<Category> getCategories() {
        return categoryRepository.findAll();
    }
}

插入數據:

    @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);
    }
View Code

數據庫條目:

返回結果:

// 20200611150503
// http://localhost:8080/category

{
  "id": 6,
  "name": "家用電器",
  "children": [
    {
      "id": 7,
      "name": "電腦",
      "children": [
        {
          "id": 8,
          "name": "筆記本電腦",
          "children": [
            
          ]
        }
      ]
    },
    {
      "id": 9,
      "name": "手機",
      "children": [
        {
          "id": 10,
          "name": "智能機",
          "children": [
            
          ]
        },
        {
          "id": 11,
          "name": "老人機",
          "children": [
            
          ]
        }
      ]
    }
  ]
}
View Code

神奇的現象出現了

  • List<Category> findAll()方法,無論對List進行索引取值序列化,還是直接JSON序列化List,都不會觸發“N+1”問題;
  • Category findByName(String name)方法,通過"category/id/{id}"訪問,總是有N+1問題,通過"category"訪問則沒有"N+1”問題(兩條SQL);
  • Optional<Category> findById(Long id)方法,總是有N+1問題;

原因判斷:

  • 不熟悉@EntityGraph或者是@OneToMany的細節表現;
  • 由於Jackson序列化導致的問題;
  • 其他問題

解決辦法:

  • 總是使用List<Category> findAll()方法,然后通過stream進行結果流處理

參考鏈接:

代碼樣例:

6月11日更新:

  • 原因大致找到了,只要先運行帶有@EntityGraph的List<Category> findAll()方法,那么后續的Option<Category> findById(Long id)等方法無論帶不帶@EntityGraph,都不會觸發N+1查詢,看來是一級緩存的功勞;
  • 為什么@EntityGraph只對findAll起效的原因還在找,暫時就先用這個方法把結果都查詢出來,然后利用Stream API進一步處理吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM