英文:
java.lang.StackOverflowError ManyToMany: MapStruct - Spring Boot - Hibernate
问题
我在Spring Boot中进行API Rest开发,使用MapStruct进行DTO和实体之间的转换。问题是在ManyToMany关系中会抛出StackOverflowError异常。能帮我解决一下吗?
演员实体(Actor Entity)
```java
@Getter @Setter
@NoArgsConstructor
@Entity
@Table(name = "Actor")
@EqualsAndHashCode(exclude = "films")
public class Actor implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "name is required")
private String name;
@ManyToMany(mappedBy = "actors")
private Set<Film> films;
}
电影实体(Film Entity)
@Getter @Setter
@NoArgsConstructor
@Entity
@Table(name = "Film")
@EqualsAndHashCode(exclude = "actors")
public class Film implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "Director_id")
private Director director;
@ManyToMany()
@JoinTable(
name = "Actor_has_Film",
joinColumns = @JoinColumn(name = "Film_id"),
inverseJoinColumns = @JoinColumn(name = "Actor_id"))
private Set<Actor> actors;
public void addActor(Actor actor) {
this.actors.add(actor);
}
}
演员DTO(ActorDTO)
@Getter @Setter
@NoArgsConstructor
public class ActorDTO {
private Long id;
private String name;
private Set<FilmDTO> films;
}
电影DTO(FilmDTO)
@Getter @Setter
@NoArgsConstructor
public class FilmDTO {
private Long id;
private DirectorDTO director;
private Set<ActorDTO> actors;
}
数据映射器(DataMapper)
public interface DataMapper<D, E> {
E toEntity(D dto);
D toDto(E entity);
List<E> toEntity(List<D> dtoList);
List<D> toDto(List<E> entityList);
}
演员映射器(ActorMapper)
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
}
电影映射器(FilmMapper)
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
}
电影服务(FilmServices)
@Service("filmServices")
public class FilmServices implements Services<FilmDTO> {
@Autowired @Qualifier("filmRepository")
private FilmRepository filmRepository;
@Autowired @Qualifier("actorRepository")
private ActorRepository actorRepository;
private FilmMapper filmMapper;
public FilmServices(FilmMapper filmMapper) {
this.filmMapper = filmMapper;
}
public FilmDTO addActorToFilm(Long filmId, Long actorId) {
Optional<Film> filmByIdOptional = filmRepository.findById(filmId);
Optional<Actor> actorByIdOptional = actorRepository.findById(actorId);
FilmDTO filmDtoWithNewActor = null;
if (!filmByIdOptional.isPresent())
throw new RuntimeException("The Film with id '" + filmId + "' does not exist");
if (!actorByIdOptional.isPresent())
throw new RuntimeException("The Actor with id '" + actorId + "' does not exist");
Film film = filmByIdOptional.get();
Actor actorToAdd = actorByIdOptional.get();
boolean hasActorInFilm = film.getActors().stream()
.anyMatch(actor -> actor.getName().equals(actorToAdd.getName()));
if (!hasActorInFilm) {
film.addActor(actorToAdd);
Film filmWithNewActor = filmRepository.save(film);
filmDtoWithNewActor = filmMapper.toDto(filmWithNewActor);
} else {
throw new RuntimeException("The Actor with id '" + actorId + "' already exists in the film");
}
return filmDtoWithNewActor;
}
}
输出日志:
Hibernate: select films0_.Actor_id as actor_id2_1_0_, films0_.Film_id as film_id1_1_0_, film1_.id as id1_3_1_, film1_.Director_id as director4_3_1_, director2_.id as id1_2_2_ from Actor_has_Film films0_ inner join Film film1_ on films0_.Film_id=film1_.id left outer join Director director2_ on film1_.Director_id=director2_.id where films0_.Actor_id=?
2020-04-07 15:27:26.296 ERROR 742 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorSetToActorDTOSet(FilmMapperImpl.java:188) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.toDto(FilmMapperImpl.java:53) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.filmSetToFilmDTOSet(FilmMapperImpl.java:165) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorToActorDTO(FilmMapperImpl.java:182) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorSetToActorDTOSet(FilmMapperImpl.java:194) ~[classes/:na]
非常感谢您的帮助!
<details>
<summary>英文:</summary>
I'm doing an api rest on spring boot and I'm using MapStruct for the transformation between DTO's and Entities. The problem is that it launches an exception StackOverflowError in the relationship of ManyToMany. Could you help me?
Actor Entity
@Getter @Setter
@NoArgsConstructor
@Entity()
@Table(name = "Actor")
@EqualsAndHashCode(exclude = "films")
public class Actor implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "name is required")
private String name;
@ManyToMany(mappedBy = "actors")
private Set<Film> films;
}
Film Entity
@Getter @Setter
@NoArgsConstructor
@Entity @Table(name = "Film")
@EqualsAndHashCode(exclude = "actors")
public class Film implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "Director_id")
private Director director;
@ManyToMany()
@JoinTable(
name = "Actor_has_Film",
joinColumns = @JoinColumn(name = "Film_id"),
inverseJoinColumns = @JoinColumn(name = "Actor_id"))
private Set<Actor> actors;
public void addActor(Actor actor) {
this.actors.add(actor);
}
}
ActorDTO
@Getter @Setter
@NoArgsConstructor
public class ActorDTO {
private Long id;
private String name;
private Set<FilmDTO> films;
}
FilmDTO
@Getter @Setter
@NoArgsConstructor
public class FilmDTO {
private Long id;
private DirectorDTO director;
private Set<ActorDTO> actors;
}
DataMapper
public interface DataMapper<D, E> {
E toEntity(D dto);
D toDto(E entity);
List<E> toEntity(List<D> dtoList);
List<D> toDto(List<E> entityList);
}
ActorMapper
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
}
FilmMapper
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
}
FilmServices
@Service("filmServices")
public class FilmServices implements Services<FilmDTO> {
@Autowired @Qualifier("filmRepository")
private FilmRepository filmRepository;
@Autowired @Qualifier("actorRepository")
private ActorRepository actorRepository;
private FilmMapper filmMapper;
public FilmServices(FilmMapper filmMapper) {
this.filmMapper = filmMapper;
}
public FilmDTO addActorToFilm(Long filmId, Long actoId) {
Optional<Film> filmByIdOptional = filmRepository.findById(filmId);
Optional<Actor> actorByIdOptional = actorRepository.findById(actoId);
FilmDTO filmDtoWithNewActor = null;
if (!filmByIdOptional.isPresent())
throw new RuntimeException("The Film with id '" + filmId + "' does not exist");
if (!actorByIdOptional.isPresent())
throw new RuntimeException("The Actor with id '" + actoId + "' does not exist");
Film film = filmByIdOptional.get();
Actor actorToAdd = actorByIdOptional.get();
boolean hasActorInFilm = film.getActors().stream()
.anyMatch(actor -> actor.getName().equals(actorToAdd.getName()));
if (!hasActorInFilm) {
film.addActor(actorToAdd);
Film filmWithNewActor = filmRepository.save(film);
filmDtoWithNewActor = filmMapper.toDto(filmWithNewActor); // HERE THROW EXCEPTION
} else {
throw new RuntimeException("The Actor with id '" + actoId + "' already exist in the film");
}
return filmDtoWithNewActor;
}
}
output logs:
Hibernate: select films0_.Actor_id as actor_id2_1_0_, films0_.Film_id as film_id1_1_0_, film1_.id as id1_3_1_, film1_.Director_id as director4_3_1_, director2_.id as id1_2_2_ from Actor_has_Film films0_ inner join Film film1_ on films0_.Film_id=film1_.id left outer join Director director2_ on film1_.Director_id=director2_.id where films0_.Actor_id=?
2020-04-07 15:27:26.296 ERROR 742 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorSetToActorDTOSet(FilmMapperImpl.java:188) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.toDto(FilmMapperImpl.java:53) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.filmSetToFilmDTOSet(FilmMapperImpl.java:165) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorToActorDTO(FilmMapperImpl.java:182) ~[classes/:na]
at ar.com.ada.sb.relationship.model.mapper.FilmMapperImpl.actorSetToActorDTOSet(FilmMapperImpl.java:194) ~[classes/:na]
I would very much appreciate your help
</details>
# 答案1
**得分**: 0
以下是您提供的内容的翻译:
我在这篇帖子上遇到了一个问题,并找到了解决方法,解决了使用MapStruct的@ManyToMany JPA关系时出现的循环依赖或StackOverflowError的问题。
这里我仅提出了一种从实体到DTO方向的解决方案,即`List<D> toDto(List<E> entityList);`,`List<E> toEntity(List<D> dtoList);`对我来说不是必需的,但需要记住的是可级联的操作,这些操作会传播到关联的实体,如PERSIST、MERGE、REMOVE、REFRESH、DETACH。
```java
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
@Mapping(target = "films", source = "films", qualifiedByName = "filmIdSet") // 循环依赖
ActorDTO toDto(Actor s);
@Named("filmId")
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = "id")
@Mapping(target = "name", source = "name")
ActorDTO toDtoFilmId(Film film);
@Named("filmIdSet")
default Set<ActorDTO> toDtoFilmIdSet(Set<Film> films) {
return films.stream().map(this::toDtoFilmId).collect(Collectors.toSet());
}
}
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
@Mapping(target = "actors", source = "actors", qualifiedByName = "actorIdSet") // 循环依赖
FilmDTO toDto(Film s);
@Named("actorId")
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = "id")
ActorDto toDtoActorId(Actor actor);
@Named("actorIdSet")
default Set<ActorDto> toDtoActorIdSet(Set<Actor> actors) {
return actors.stream().map(this::toDtoActorId).collect(Collectors.toSet());
}
}
简而言之,思路是默认情况下忽略所有属性,以避免循环引用,并仅添加非循环引用的属性。
另一种方法如下,但这会在保留哪个属性方面提供更少的选择:
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
@Mapping(target = "films", source = "films", ignore = true) // 循环依赖
ActorDTO toDto(Actor s);
}
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
@Mapping(target = "actors", source = "actors", ignore = true) // 循环依赖
FilmDTO toDto(Film s);
}
我使用的MapStruct版本:1.5.2.Final
英文:
I stumble on this post and found a solution to the same problem of Circular dependencies or StackOverflowError with a @ManyToMany JPA relation using MapStruct.
The solution @AfterMapping
proposed in java.lang.StackOverflowError if Mappers have circular dependencies didn't fit me for now.
Here I propose only a solution from Entity to Dto direction ie List<D> toDto(List<E> entityList);
, the List<E> toEntity(List<D> dtoList);
elude me as not needed for moment but something to keep in mind would be the cascadable operations that are propagated to the associated entity such as PERSIST, MERGE, REMOVE, REFRESH, DETACH.
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
@Mapping(target = "films", source = "films", qualifiedByName = "filmIdSet") // circular dependencies
ActorDTO toDto(Actor s);
@Named("filmId")
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = "id")
@Mapping(target = "name", source = "name")
ActorDTO toDtoFilmId(Film film);
@Named("filmIdSet")
default Set<ActorDTO> toDtoFilmIdSet(Set<Film> films) {
return films.stream().map(this::toDtoFilmId).collect(Collectors.toSet());
}
}
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
@Mapping(target = "actors", source = "actors", qualifiedByName = "actorIdSet") // circular dependencies
FilmDTO toDto(Film s);
@Named("actorId")
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = "id")
ActorDto toDtoActorId(Actor actor);
@Named("actorIdSet")
default Set<ActorDto> toDtoActorIdSet(Set<Actor> actors) {
return actors.stream().map(this::toDtoActorId).collect(Collectors.toSet());
}
}
In a nutshell the idea is by default ignore all properties to avoid the circular reference and add only the non circular ones.
Another way is the following, but it gives less choice on what property we want to keep eventually
@Mapper(componentModel = "spring", uses = { })
public interface ActorMapper extends DataMapper<ActorDTO, Actor> {
@Mapping(target = "films", source = "films", ignore = true) // circular dependencies
ActorDTO toDto(Actor s);
}
@Mapper(componentModel = "spring", uses = { })
public interface FilmMapper extends DataMapper<FilmDTO, Film> {
@Mapping(target = "actors", source = "actors", ignore = true) // circular dependencies
FilmDTO toDto(Film s);
}
I use Mapstruct Version: 1.5.2.Final
答案2
得分: -2
尝试这个版本,在property的HashSet<>()和actor的HashSet<>()中进行实例化,只需要一个方向的映射,将actor一侧的映射保留为注释。
电影
@Entity @Table(name = "Film")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Film implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Film_id", updatable = false, nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "Director_id")
private Director director;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.ALL
},
targetEntity= ActorModel.class
)
@JoinTable(name = "Actor_has_Film",
joinColumns = { @JoinColumn(name = "Film_id") },
inverseJoinColumns = { @JoinColumn(name = "Actor_id") })
@JsonProperty("actors")
private Set<Actor> actors = new HashSet<>();
public void addActor(Actor actor) {
this.actors.add(actor);
}
}
演员
@Entity()
@Table(name = "Actor")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Actor implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Actor_id", updatable = false, nullable = false)
private Long id;
@NotBlank(message = "名字是必需的")
private String name;
/*@ManyToMany(mappedBy = "actors")
@JsonIgnore
private Set<Film> films = new HashSet<>();*/
}
英文:
Try this one, instantiate the HashSet<>() of both property and you need only one direction mapping, leave the actor side mapping commented.
Movie
@Entity @Table(name = "Film")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Film implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Film_id", updatable = false, nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "Director_id")
private Director director;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.ALL
},
targetEntity= ActorModel.class
)
@JoinTable(name = "Actor_has_Film",
joinColumns = { @JoinColumn(name = "Film_id") },
inverseJoinColumns = { @JoinColumn(name = "Actor_id") })
@JsonProperty("actors")
private Set<Actor> actors = new HashSet<>();
public void addActor(Actor actor) {
this.actors.add(actor);
}
}
Actor
@Entity()
@Table(name = "Actor")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Actor implements Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Actor_id", updatable = false, nullable = false)
private Long id;
@NotBlank(message = "name is required")
private String name;
/*@ManyToMany(mappedBy = "actors")
@JsonIgnore
private Set<Film> films = new HashSet<>();*/
}
专注分享java语言的经验与见解,让所有开发者获益!
评论