• 利用@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进一步处理吧。
  • 相关阅读:
    函数练习之计算机
    函数练习小程序
    Java—Day5课堂练习
    mysql-用户权限管理
    liunx-tail 实时显示文件内容
    Linux-diff --比较两个文件并输出不同之处
    linux-查找某目录下包含关键字内容的文件
    mysql-允许远程连接
    mysql-基本操作
    liunx-指令
  • 原文地址:https://www.cnblogs.com/echo1937/p/13093418.html
Copyright © 2020-2023  润新知