JPA 单表双向自关联、转dto、jackson序列化循环引用问题

博客围绕双向自关联实体 Organization 展开,介绍了解决循环引用问题的方法。提到 @OneToMany 默认 fetch 策略及使用 lombok @Data 的问题,采用 mapstruct 工具进行 po 到 do、do 到 dto 的转换,还利用 jackson 注解解决序列化阶段的循环引用,思路是切断循环。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先看实体,Organization

package com.example.organization.domain.repo.po;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.Objects;
import java.util.Set;

@Entity
@Table(name = "organization")
@Getter
@Setter
public class OrganizationPo {
    @Id
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(name = "org_id")
    private String orgId;

    @Column(name = "org_name")
    private String orgName;

    @ManyToOne
    @JoinColumn(name = "parent_id", referencedColumnName = "org_id")
    private OrganizationPo parent;

    @OneToMany(mappedBy = "parent")
    private Set<OrganizationPo> children;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        OrganizationPo that = (OrganizationPo) o;
        return orgId.equals(that.orgId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orgId);
    }
}

这是一个双向自关联的实体。表数据如下:

对于@OneToMany 默认fetch策略是 Lazy,也就是在实际访问到 children 属性时才会发出数据库请求,比如你打了断点调试,查看 children 属性时,或者 @ResponseBody 序列化到前端时才会发出新的查询。

注意不要使用 lombok 的 @Data,会有带上所有字段的 toString() 方法,此时debug的时候会因为调用 toString() 而导致循环引用从而抛出 stackoverflowError,使用默认的 toString() 就好,默认的 toString() 采用的是 hashCode 的16进制。

 

接下来需要做 po(persistence object)到do(domain object)、do到dto(data transfer object) 的转换,此时依旧会出现循环引用、堆栈溢出的问题。do实体如下:

package com.example.organization.domain.aggregate;

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;
import java.util.Set;

@Getter
@Setter
public class Organization {
    private String orgId;
    private String orgName;
    private Organization parent;
    private Set<Organization> children;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Organization that = (Organization) o;
        return orgId.equals(that.orgId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orgId);
    }
}

我采用的 bean mapping 工具是 mapstruct ,各方面都较优的一个映射工具,它提供的解决方案是:

CycleAvoidingMappingContext 如下:
public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();

    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return (T) knownInstances.get(source);
    }

    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put(source, target);
    }
}

使用方式: 

private static Organization toDo(OrganizationPo po) {
        return OrganizationMapper.INSTANCE.toDo(po, new CycleAvoidingMappingContext());
    }

原理就是以已经映射过的 po 为 key,do 为 value,存到一个 map 里。在递归设值的过程中,再遇到同一个 po,就可以直接从 map 中取出对应的 do,从而避免了无限循环。

 

最后到了序列化的阶段,此时仍然有循环引用的问题。springboot 默认采用的是 jackson 框架,jackson 提供了一个注解可以解决此问题:

意思就是对于相同的对象,直接使用 orgId 属性表示。最后假如执行 dao.findById("1"),返回的结果如下:

{
  "orgId": "1",
  "orgName": "研发部",
  "children": [
    {
      "orgId": "2",
      "orgName": "河北项目组",
      "parent": "1",
      "children": [
        {
          "orgId": "4",
          "orgName": "北京小分队",
          "parent": "2",
          "children": [
            {
              "orgId": "5",
              "orgName": "销售组",
              "parent": "4",
              "children": [
                
              ]
            }
          ]
        }
      ]
    },
    {
      "orgId": "3",
      "orgName": "福建项目组",
      "parent": "1",
      "children": [
        
      ]
    }
  ]
}

可以看到解决循环引用的思路都是想办法切断循环引用,一个是返回已有的对象,一个是用其他属性表示,都可以终止递归调用。

{"text":"success","data":[{"id":1,"id":1,"caizhi":{"id":1},"mupi1":{"id":1},"mupi2":{"id":1},"houdu":15.0,"kucun":{"id":1}},{"id":2,"id":2,"caizhi":{"id":1},"mupi1":{"id":2},"mupi2":{"id":2},"houdu":15.0,"kucun":{"id":2}}],"status":200}package com.kucun.data.entity; import java.lang.annotation.Annotation; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToOne; import javax.persistence.Table; import com.kucun.data.entity.DTO.*; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonManagedReference; import com.fasterxml.jackson.databind.annotation.JsonSerialize; /** * 板材 * @author Administrator * */ @Entity @Table(name="bancai") public class Bancai implements EntityBasis { @Id private int id; @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "caizhi_id") // @JsonManagedReference // 标记为“主”关联方 @JsonSerialize(using = IdOnlySerializer.class) private Caizhi caizhi; @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "mupi1_id") @JsonSerialize(using = IdOnlySerializer.class) private Mupi mupi1; @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "mupi2_id") @JsonSerialize(using = IdOnlySerializer.class) private Mupi mupi2; private Double houdu; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "kucun_id", referencedColumnName = "id") private Kucun kucun; public Kucun getKucun() { return kucun; } public void setKucun(Kucun kucun) { this.kucun = kucun; } public Integer getId() { return id; } public void setId(int id) { this.id = id; } public Caizhi getCaizhi() { return caizhi; } public void setCaizhi(Caizhi caizhi) { this.caizhi = caizhi; } public Mupi getMupi1() { return mupi1; } public void setMupi1(Mupi mupi1) { this.mupi1 = mupi1; } public Mupi getMupi2() { return mupi2; } public void setMupi2(Mupi mupi2) { this.mupi2 = mupi2; } public Double getHoudu() { return houdu; } public void setHoudu(Double houdu) { this.houdu = houdu; } public Bancai(int id, Caizhi caizhi, Mupi mupi1, Mupi mupi2, Double houdu) { super(); this.id = id; this.caizhi = caizhi; this.mupi1 = mupi1; this.mupi2 = mupi2; this.houdu = houdu; } public Bancai() { super(); } } @RequestMapping("/all/{e}") public Information getAllDataWithChildIds(@PathVariable String e) { return appService.getAllDataWithChildIdse(e); } public Information getAllDataWithChildIdse(String e) { JpaRepository<?, ?> jp = repositoryService.getJpaRepository(e); System.out.println(jp); return Information.NewSuccess(jp.findAll()); } package com.kucun.data.entity; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * 通信类 * @author Administrator * */ public class Information { private static final ObjectMapper mapper = new ObjectMapper(); private Integer Status ; private String text; private Object data; public Integer getStatus() { return Status; } public void setStatus(Integer status) { Status = status; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public Information(Integer status, String text, Object data) { super(); Status = status; this.text = text; this.data = data; } @SuppressWarnings({"unchecked","rawtypes"}) public Information(Integer status, String text, String data, Class T) throws Exception { super(); Status = status; this.text = text; this.data = fromJson(data,T); } public Information() { super(); // TODO Auto-generated constructor stub } public String DataJson() throws JsonProcessingException { // Java对象JSON return mapper.writeValueAsString(this); } @SuppressWarnings("unchecked") public <T> T fromJson(String json, Class<T> clazz) throws Exception { data= mapper.readValue(json, clazz); return (T) data; } public static Information NewSuccess(Object data) { return new Information(200, "success", data); } public static Information NewSuccess(String data) { return new Information(200, "success", data);a } public static Information Newfail(Integer status,String text,Object data) { return new Information(status, "success", data); } public static Information NewFail(String string) { // TODO Auto-generated method stub return new Information(400,string,null); } } package com.kucun.data.entity.DTO; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.kucun.data.entity.EntityBasis; import java.io.IOException; public class IdOnlySerializer extends JsonSerializer<EntityBasis> { @Override public void serialize(EntityBasis entity, JsonGenerator gen, SerializerProvider provider) throws IOException { if (entity == null) { gen.writeNull(); } else { gen.writeStartObject(); gen.writeNumberField("id", entity.getId()); gen.writeEndObject(); } } }package com.kucun.data.entity.DTO; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import com.kucun.data.entity.EntityBasis; // 2. 配置 ObjectMapper @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); // 基本配置 mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 注册自定义模块 SimpleModule module = new SimpleModule(); module.addSerializer(EntityBasis.class, new EntityBasisSerializer()); // 添加Hibernate支持 module.setSerializerModifier(new HibernateSerializerModifier()); mapper.registerModule(module); // 禁用Jackson的FAIL_ON_UNKNOWN_PROPERTIES功能 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 关闭对同一对象的循环引用检测 mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false); return mapper; } // 3. Hibernate序列化特殊处理 private static class HibernateSerializerModifier extends BeanSerializerModifier { @Override public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc, BeanSerializerBuilder builder) { // 添加对Hibernate代理类的过滤 List<BeanPropertyWriter> properties = builder.getProperties(); properties.removeIf(property -> property.getName().equals("handler") || property.getName().equals("hibernateLazyInitializer")); return builder; } } }package com.kucun.data.entity.DTO; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.BeanSerializer; import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import com.kucun.data.entity.EntityBasis; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collection; import java.util.Date; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; // 1. 创建自定义序列化器 public class EntityBasisSerializer extends JsonSerializer<EntityBasis> { @Override public void serialize(EntityBasis entity, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeStartObject(); gen.writeNumberField("id", entity.getId()); // 获取所有字段并处理 Field[] fields = entity.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); try { Object value = field.get(entity); if (value != null) { // 处理关联实体 if (value instanceof EntityBasis) { gen.writeObjectFieldStart(field.getName()); gen.writeNumberField("id", ((EntityBasis) value).getId()); gen.writeEndObject(); } // 处理集合类型 else if (value instanceof Collection) { gen.writeArrayFieldStart(field.getName()); for (Object item : (Collection<?>) value) { if (item instanceof EntityBasis) { gen.writeStartObject(); gen.writeNumberField("id", ((EntityBasis) item).getId()); gen.writeEndObject(); } } gen.writeEndArray(); } // 处理基本类型 else if (isSimpleType(field.getType())) { gen.writeObjectField(field.getName(), value); } } } catch (IllegalAccessException ignored) {} } gen.writeEndObject(); } private boolean isSimpleType(Class<?> type) { return type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type) || type == Boolean.class || type == Date.class; } } 怎么输出是两个id属性
最新发布
06-13
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值