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

Spring JPA(Hibernate)双向多对多映射JSON格式优化问题

解决Spring Data JPA双向多对多JSON序列化按需返回问题

针对你遇到的双向多对多映射JSON序列化递归、返回格式不符合需求的问题,以下是几种实用解决方案:

方案一:使用@JsonView指定序列化视图

这种方式通过定义不同视图,控制不同查询场景下的字段序列化范围,实现按需返回。

1. 定义视图接口

创建标记用的视图接口,区分不同的查询场景:

public interface Views {
    // 查询Artist时使用的视图
    interface ArtistDetail {}
    // 查询Song时使用的视图
    interface SongDetail {}
}

2. 实体类配置@JsonView

在实体的关联字段上标注对应的视图,未标注的字段在对应视图下不会被序列化:

@Entity
public class Artist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 仅在ArtistDetail视图下序列化songs字段
    @ManyToMany(mappedBy = "artists")
    @JsonView(Views.ArtistDetail.class)
    private List<Song> songs = new ArrayList<>();

    // getter、setter、构造方法省略
}
@Entity
public class Song {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    // 仅在SongDetail视图下序列化artists字段
    @ManyToMany
    @JoinTable(
        name = "artist_song",
        joinColumns = @JoinColumn(name = "song_id"),
        inverseJoinColumns = @JoinColumn(name = "artist_id")
    )
    @JsonView(Views.SongDetail.class)
    private List<Artist> artists = new ArrayList<>();

    // getter、setter、构造方法省略
}

3. Controller层指定视图

在查询接口上标注对应的@JsonView,触发对应视图的序列化规则:

@RestController
@RequestMapping("/artists")
public class ArtistController {
    @Autowired
    private ArtistRepository artistRepository;

    @GetMapping("/{id}")
    @JsonView(Views.ArtistDetail.class)
    public Artist getArtist(@PathVariable Long id) {
        return artistRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Artist not found"));
    }
}
@RestController
@RequestMapping("/songs")
public class SongController {
    @Autowired
    private SongRepository songRepository;

    @GetMapping("/{id}")
    @JsonView(Views.SongDetail.class)
    public Song getSong(@PathVariable Long id) {
        return songRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Song not found"));
    }
}

方案二:使用DTO投影解耦实体与返回格式

通过定义数据传输对象(DTO),完全控制返回的字段结构,彻底避免递归问题,同时解耦实体层与前端接口。

1. 定义DTO类

根据需求创建不同层级的DTO:

// 查询Artist时返回的DTO,包含关联的精简Song信息
public class ArtistDTO {
    private Long id;
    private String name;
    private List<SongSimpleDTO> songs;

    public ArtistDTO(Long id, String name, List<SongSimpleDTO> songs) {
        this.id = id;
        this.name = name;
        this.songs = songs;
    }

    // getter方法省略
}

// 精简版Song DTO,不含artists字段
public class SongSimpleDTO {
    private Long id;
    private String title;

    public SongSimpleDTO(Long id, String title) {
        this.id = id;
        this.title = title;
    }

    // getter方法省略
}
// 查询Song时返回的DTO,包含关联的精简Artist信息
public class SongDTO {
    private Long id;
    private String title;
    private List<ArtistSimpleDTO> artists;

    public SongDTO(Long id, String title, List<ArtistSimpleDTO> artists) {
        this.id = id;
        this.title = title;
        this.artists = artists;
    }

    // getter方法省略
}

// 精简版Artist DTO,不含songs字段
public class ArtistSimpleDTO {
    private Long id;
    private String name;

    public ArtistSimpleDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter方法省略
}

2. Repository层实现投影查询

在Repository中通过构造器投影或接口投影获取DTO数据:

public interface ArtistRepository extends JpaRepository<Artist, Long> {
    // 构造器投影查询
    @Query("SELECT new com.example.dto.ArtistDTO(a.id, a.name, new com.example.dto.SongSimpleDTO(s.id, s.title)) " +
           "FROM Artist a JOIN a.songs s WHERE a.id = :id")
    ArtistDTO findArtistWithSongs(Long id);
}

3. Controller层返回DTO

@GetMapping("/{id}")
public ArtistDTO getArtist(@PathVariable Long id) {
    return artistRepository.findArtistWithSongs(id);
}

方案三:使用@JsonFilter动态过滤字段

适合需要更动态控制序列化字段的场景,通过过滤器指定排除的字段。

1. 实体类标注@JsonFilter

@Entity
@JsonFilter("ArtistFilter")
public class Artist {
    // 字段、关联关系省略
}

@Entity
@JsonFilter("SongFilter")
public class Song {
    // 字段、关联关系省略
}

2. Controller层配置过滤器

在查询接口中动态设置过滤规则:

@GetMapping("/artists/{id}")
public MappingJacksonValue getArtist(@PathVariable Long id) {
    Artist artist = artistRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Artist not found"));
    
    MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(artist);
    // 给Song设置过滤器,排除artists字段
    FilterProvider filters = new SimpleFilterProvider()
            .addFilter("SongFilter", SimpleBeanPropertyFilter.serializeAllExcept("artists"));
    mappingJacksonValue.setFilters(filters);
    
    return mappingJacksonValue;
}
@GetMapping("/songs/{id}")
public MappingJacksonValue getSong(@PathVariable Long id) {
    Song song = songRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Song not found"));
    
    MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(song);
    // 给Artist设置过滤器,排除songs字段
    FilterProvider filters = new SimpleFilterProvider()
            .addFilter("ArtistFilter", SimpleBeanPropertyFilter.serializeAllExcept("songs"));
    mappingJacksonValue.setFilters(filters);
    
    return mappingJacksonValue;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 20:23:11