You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA多对多关系配置及JSON序列化异常排查求助

问题

正在给Customer与Category实体配置多对多关联,通过中间表CustomerCategory实现,但执行JPQL查询SELECT c FROM customer c LEFT JOIN FETCH c.customerCategories时,出现JSON序列化错误:

JSON Binding serialization error jakarta.json.bind.JsonbException: Unable to serialize property 'customerCategories' from nl.project.entities.Customer

相关代码

Category 实体

package nl.project.entities;

import jakarta.persistence.*;

import java.math.BigDecimal;
import java.util.List;

@Entity(name = "category")
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;

    @Column
    private BigDecimal price;

    @Column
    private int content;

    @OneToMany(mappedBy = "category", fetch = FetchType.LAZY)
    private List<CustomerCategory> customerCategories;

    // Getters and Setters
    // Generated toString() method
}

Customer 实体

package nl.project.entities;

import jakarta.persistence.*;

import java.util.List;

@Entity(name = "customer")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String email;

    @Column
    private String password;

    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
    private List<CustomerCategory> customerCategories;

    // Getters and Setters
    // Generated toString() method
}

CustomerCategory 中间表实体

package nl.project.entities;

import jakarta.persistence.*;

import java.math.BigDecimal;
import java.time.LocalDate;

@Entity(name = "customer_category")
public class CustomerCategory {
    @EmbeddedId
    private CustomerCategoryKey id;

    @ManyToOne
    @MapsId("customerId")
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @ManyToOne
    @MapsId("categoryId")
    @JoinColumn(name = "category_id")
    private Category category;

    @Column
    private BigDecimal price;

    @Column(name = "start_date", columnDefinition = "DATE")
    private LocalDate startDate;

    // Getters and Setters
}

CustomerCategoryKey 复合主键

package nl.project.entities;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;

import java.io.Serializable;
import java.util.Objects;

@Embeddable
public class CustomerCategoryKey implements Serializable {
    @Column(name = "customer_id")
    private Long customerId;

    @Column(name = "category_id")
    private Long categoryId;

    // Getters and Setters

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CustomerCategoryKey that = (CustomerCategoryKey) o;
        return Objects.equals(customerId, that.customerId) && Objects.equals(categoryId, that.categoryId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(customerId, categoryId);
    }
}

DebugResource 接口

package nl.project;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.metamodel.EntityType;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import nl.project.entities.Category;
import nl.project.entities.Customer;

import java.util.List;

@Path("/debug")
public class DebugResource {
    @PersistenceContext(unitName = "InMemoryPU")
    private EntityManager em;

    @GET
    @Path("/customers")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Customer> customers() {
        TypedQuery<Customer> query = em.createQuery("SELECT c FROM customer c LEFT JOIN FETCH c.customerCategories", Customer.class);
        return query.getResultList();
    }
}
解决方案

这个错误是循环引用导致的JSON序列化死循环:Customer包含CustomerCategory列表,而CustomerCategory又持有Customer引用,序列化时Jsonb会无限递归遍历这些关联对象,最终抛出异常。可以通过以下几种方式解决:

方法一:添加序列化忽略注解

在循环引用的字段上添加@JsonbTransient注解,告诉Jsonb序列化时忽略该字段:

  • 若需要在返回的Customer中保留CustomerCategory数据,就在CustomerCategory类的关联字段上添加:
@ManyToOne
@MapsId("customerId")
@JoinColumn(name = "customer_id")
@JsonbTransient
private Customer customer;

@ManyToOne
@MapsId("categoryId")
@JoinColumn(name = "category_id")
@JsonbTransient
private Category category;
  • 若不需要返回CustomerCategory数据,就在Customer类的关联字段上添加:
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
@JsonbTransient
private List<CustomerCategory> customerCategories;

方法二:使用DTO(数据传输对象)

不直接返回JPA实体类,而是创建只包含对外暴露字段的DTO,避免循环引用:

public class CustomerDTO {
    private Long id;
    private String email;
    private List<CategoryDTO> categories;

    // 构造方法、getters、setters
}

public class CategoryDTO {
    private Long id;
    private String name;
    private BigDecimal price;

    // 构造方法、getters、setters
}

然后在接口中完成实体到DTO的转换:

@GET
@Path("/customers")
@Produces(MediaType.APPLICATION_JSON)
public List<CustomerDTO> customers() {
    TypedQuery<Customer> query = em.createQuery("SELECT c FROM customer c LEFT JOIN FETCH c.customerCategories", Customer.class);
    List<Customer> customers = query.getResultList();
    
    return customers.stream().map(customer -> {
        CustomerDTO dto = new CustomerDTO();
        dto.setId(customer.getId());
        dto.setEmail(customer.getEmail());
        
        List<CategoryDTO> categoryDTOs = customer.getCustomerCategories().stream()
                .map(cc -> {
                    CategoryDTO catDto = new CategoryDTO();
                    catDto.setId(cc.getCategory().getId());
                    catDto.setName(cc.getCategory().getName());
                    catDto.setPrice(cc.getPrice());
                    return catDto;
                })
                .toList();
        dto.setCategories(categoryDTOs);
        
        return dto;
    }).toList();
}

方法三:修改toString方法

如果实体的toString方法包含关联对象,可能会间接触发循环引用,建议修改toString方法,只保留基本类型字段,不要包含关联对象列表。


内容的提问来源于stack exchange,提问作者Tjeu Foolen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 04:15:39