• 解决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));
            }
        }

      此时可以看到控制台里面只发了一条联表查询就得到了关联对象。

  • 相关阅读:
    sqlserver 2000备份文件还原到sqlserver 2005(2008)
    .dll文件有什么用?
    汇编片段
    以POST方式请求数据的Ajax实现方式
    有两个数据据服务器上有两个一样结构的数据库,现想将一服务器上的一数据库里的一个表的一部份记录插入到另一服务器上的一数据库的一表中.
    揭开ASP.NET中Cookie编程的奥秘(2)
    商城网店初步完成了,很多不足
    ajax上传(xmlhttp上传文件突破大小限制)
    查询优化
    金山词霸”屏幕取词技术揭密(讨论稿)
  • 原文地址:https://www.cnblogs.com/ealenxie/p/9800818.html
Copyright © 2020-2023  润新知