简体   繁体   中英

java.lang.StackOverflowError ManyToMany: MapStruct - Spring Boot - Hibernate

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

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

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<>();*/
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM