保姆级一条龙服务——自关联构造父子级关系(@JsonBackReference和@JsonManagedReference解决循环依赖)

废话篇

我这个不知名的菜鸟竟然都会被人催更(已经是五月份的事情了,一直没有兑现承诺🥴但是我还记得,因为受宠若惊🤣)。所以接下来我决定爆肝七天七夜出他个几篇精品(画个大饼先😑)

终于不是菜鸟学系列文章了,但我还是从前那个菜鸟,没有一丝丝改变,唯一变的是逐渐减少的发量。

前几天写代码被打击到了,我用两天写出来的菜单分类功能被组里一个同事用两个小时重新实现了一遍🙄,看看别人的代码真的美,再看看的我,💩💩💩一坨屎💩💩💩。

半年不见,废话略多。接下来就把这段美丽的代码附上一些讲解分享给大家(我的屎代码就不在这里现眼了)

正文

业务中难免会遇到一些分级展示商品或者菜单的需求,这些菜单管理就涉及到了分级分类。下面我先按照流程带大家实现一遍(因为我也是菜鸟,所以我知道大家可能对代码中涉及到的一些注解不是很了解,所以在后文我会一一解释,看懂代码后就可以放心大胆地使用CtrlCV技能了!)

构造分类实体

构造完实体以后也就实现了分类分级的父子结构关系,后续可以可以根据业务需求再拓展新实现。

@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
@Getter
@Table(name = "tb_category")
@GenericGenerator(name = "category-uuid", strategy = "uuid")
public class Category extends BaseEntity {

    @Id
    @GeneratedValue(generator = "category-uuid")
    private String categoryId;

    private String categoryName;

    // 进行自关联,实现父子关系
    
    @JsonBackReference
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "parent_id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
    @Getter
    @Setter
    @NotFound(action= NotFoundAction.IGNORE)
    private Category parent;

    @JsonManagedReference
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Category> childNodes;

    @JsonProperty("parentId")
    public String getParentId() {
        if (this.parent == null) return "";
        return this.parent.getCategoryId();
    }

    @JsonProperty("creationDate")
    public Long getCreationTime() {
        if(this.getCreationDate()==null) return 0L;
        return this.getCreationDate().getTime();
    }
    @JsonProperty("lastUpdatedDate")
    public Long getLastUpdatedTime() {
        if(this.getLastUpdatedDate()==null) return 0L;
        return this.getLastUpdatedDate().getTime();
    }

    @Setter
    private Integer categoryLevel = 1;

    private String categoryDescription;

    private String categoryIconName;

    private Integer status;

    public static final String CATEGORY_VALID = "valid";
    public static final String CATEGORY_ID = "categoryId";

    // 添加父节点
    public void addParentCategory(Category parentCategory) {
        setParent(parentCategory);
        setCategoryLevel(parentCategory.getCategoryLevel() + 1);
        parentCategory.addChildren(this);
    }

    // 添加子节点
    public void addChildren(Category child) {
        if (this.childNodes == null) {
            this.childNodes = Lists.newArrayList();
        }
        this.childNodes.add(child);
    }

    public boolean hasChildren() {
        return getChildNodes().size() > 0;
    }

    // 向下获得孩子节点(不包含自己)
    public List<Category> getChildNodes() {
        if (this.childNodes == null) return Lists.newArrayList();
        return Collections.unmodifiableList(this.childNodes.stream().filter(x -> x.getValid() != 0).collect(Collectors.toList()));
    }

    // 向下获得孩子节点id(不包含自己)
    @JsonIgnore
    public List<String> getChildIds() {
        if (this.childNodes == null) return Lists.newArrayList();
        return Collections.unmodifiableList(this.childNodes.stream().filter(x -> x.getValid() != 0).map(Category::getCategoryId).collect(Collectors.toList()));
    }

}

下面是一些常见需求的实现方式:

// 获取当前节点和所有的父节点
public List<Category> findCategoryAndAllParents(List<Category> categories, Category category) {
    // 先将当前节点加入到集合中
    categories.add(category);
    if (StringUtils.isNotEmpty(category.getParentId())) {
        Category parent = getCategoryById(category.getParentId());
        // 递归向上查找父节点
        findCategoryAndAllParents(categories, parent);
    }
    categories.sort(Comparator.comparing(Category::getCategoryLevel));
    return categories;
}

// 获取当前节点id及其所有子集节点id
public List<String> getChildrensAndSelfIds(String categoryId) {
    Category category = getCategoryById(categoryId);
    List<String> ids = new ArrayList<>();
    // 先将自己加入到集合中
    ids.add(category.getCategoryId());
    return getSelfIdAndAllChildIds(ids, category);
}
private List<String> getSelfIdAndAllChildIds(List<String> ids,Category category){
    // 将当前节点的所有子节点id加入集合中
    ids.addAll(category.getChildIds());
    // 遍历找到当前节点的子节点,递归将他们的子节点的id加入集合中
    category.getChildNodes().forEach(x->{
        getSelfIdAndAllChildIds(ids,x);
    });
    return ids;
}

注解解释

  • @JsonBackReference

    • Annotation used to indicate that associated property is part of two-way linkage between fields; and that its role is “child” (or “back”) link. Value type of the property must be a bean: it can not be a Collection, Map, Array or enumeration.

      官方API文档是上面这样描述的,他的大致含义是:这个注解用于声明具有双向联系的有关联关系的两个属性之间。@JsonBackReference注解的作用是“子”链接。(即该注解定义在子级角色中,下面会举个小栗子),他必须标注在一个bean上,并且不能是集合等容器。

    • Linkage is handled such that the property annotated with this annotation is not serialized; and during deserialization, its value is set to instance that has the “managed” (forward) link.(该段话会在下文@JsonBackReference和@JsonManagedReference的区别中统一解释)

  • @JsonManagedReference

    • Annotation used to indicate that annotated property is part of two-way linkage between fields; and that its role is “parent” (or “forward”) link. Value type (class) of property must have a single compatible property annotated with JsonBackReference.

      @JsonManagedReference注解的作用是“父”链接。(即该注解定义在父级角色中,下面会举个小栗子)和这种属性对应的值类型必须标注@JsonBackReference注解

    • Linkage is handled such that the property annotated with this annotation is handled normally (serialized normally, no special handling for deserialization); it is the matching back reference that requires special handling(该段话会在下文@JsonBackReference和@JsonManagedReference的区别中统一解释)

  • 为了更方便理解@JsonBackReference@JsonManagedReference注解,下面举一个简单的例子使用一下这两个注解(我上面采用的是自关联的方式构造的父子关系)

    // 这是一个 老师和学生 一对多的例子(一个老师有多个学生,多个学生对应一个老师。相当于老师是父,学生是子)
    
    public class Teacher {
        private String name;
        private Integer age;
        
        @JsonManagedReference // 定义在父级角色中
        private List<Student> students;
    }
    
    public class Student {
        private String name;
        private Integer age;
        
        @JsonBackReference // 定义在子级角色中,标注在父类属性上
        private Teacher teacher;
    }
    
@JsonBackReference@JsonManagedReference的区别

Jackson在序列化对象的时候,如果对象里面有循环依赖的情况(即向上面的老师和学生互相依赖的情况),会报栈溢出。使用@JsonBackReference@JsonManagedReference主要是为了解决一对多和多对一关联关系的循环依赖问题。这一对注解是解决父子间循环依赖的利器。

  • 从官方API文档中可以看到@JsonBackReference标注的属性,在进行序列化(即将对象转换为json数据)的时候会被忽略(即结果中的json数据不包含该属性的内容)。@JsonManagedReference标注的属性则会被序列化。

  • 只有当@JsonBackReference@JsonManagedReference放在一起使用,在进行反序列化时,@JsonBackReference标注的属性值才能被自动注入。

  • @JsonIgnore:直接忽略某个属性,以断开无限递归,序列化或反序列化均忽略。

  • @ManyToOne:表示当前实体为一对多关系的一端。

    • @JoinColumn:配置外键
      • name:外键字段名称,用来标识表中所对应的字段的名称
  • @OneToMany:表示当前实体为一对多关系的一端。

    • mappedBy:关系维护

      • mappedBy= “parent” 表示在Category类中的 parent 属性来维护关系,这个名称必须和Category中的parent属性名称完全一致
      • OneToMany必须写mappedBy,一对多与多对一关系也可能会多生成一张没用的中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键)

      cascade:级联操作

      • CascadeType. PERSIST 级联持久化 ( 保存 ) 操作
      • CascadeType. MERGE 级联更新 (合并 ) 操作
      • CascadeType. REFRESH 级联刷新操作,只会查询获取操作
      • CascadeType. REMOVE 级联删除操作
      • CascadeType. ALL 级联以上全部操作

      fetch:加载类型,默认情况一的方为立即加载,多的一方为延迟加载

      • FetchType.LAZY 懒加载
      • FetchType.EAGER 立即加载(缺省值)
  • @JsonInclude:该注解仅在序列化操作时有用,用于控制方法、属性等是否应该被序列化

      • ALWAYS:默认策略,任何情况都执行序列化
      • NON_NULL:非空
      • NON_EMPTY:null、集合数组等没有内容、空字符串等,都不会被序列化
      • NON_DEFAULT:如果字段是默认值,就不会被序列化
  • @JsonProperty:用在属性或方法上,把该属性的名称序列化为另外一个名称

  • @NotFound:找不到引用的外键数据时忽略

    many-to-one, one-to-one, 关系中,一方引入另一方的属性,如果引用属性值数据在数据库中不见,hibernate默认会抛出异常,解决此问题,加@NotFound注解即可

部分参考:
https://www.jianshu.com/p/e85c3dfba052

https://www.cnblogs.com/bolingcavalry/p/14360209.html

https://blog.csdn.net/qq_35357001/article/details/55505659

https://fasterxml.github.io/jackson-annotations/javadoc/2.6/com/fasterxml/jackson/annotation/JsonBackReference.html

http://www.manongjc.com/detail/24-riyzfvvluwotkdq.html

https://www.jianshu.com/p/27d9a42203be

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值