Использование Entity с OneToMany и HATEOAS RessourceAssembler приводит к бесконечной рекурсии - PullRequest
0 голосов
/ 13 октября 2019

Я использую две сущности JPA, аннотированные @OneToMany (parent) <-> @ManyToOne (child), и я также написал RessourceAssembler, чтобы превратить сущности в ресурсы в контроллере приложения Springboot (примеры кода см. Ниже)).

Без связи @OneToMany в родительском объекте сборка и сериализация Ressource работает просто отлично.

Как только я добавляю отношение OneToMany к родительскому объекту, сериализация разрывается с этим:

"Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.hateoas.Resource[\"content\"]->com.marcelser.app.entities.Storage[\"storageStock\"])"

Как вы можете видеть, бесконечный цикл исходит из ресурса hateoas, а не из самих сущностей.

Я уже пытался добавить @JsonManagedReference и @JsonBackReference для сущностей или @JsonIgnore для дочернего элемента, но на самом деле ничего не помогает. Hateoas RessourceAssembler всегда заканчивается бесконечным циклом, как только дочерняя сущность внедряется. Кажется, что shose @Json .... аннотации помогают в сериализации JSON самой сущности, но они не решают проблемы с RessourceAssembler

У меня есть эти 2 сущности (Storage & Stock)

@Entity
@Table(name = "storage")
@Data
public class Storage {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            mappedBy = "storage")
    private Set<Stock> storageStock = new HashSet<>();;
}

@Entity
@Table(name = "stock")
@Data
public class Stock {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "storage_id")
    private Storage storage;

    ... other fields ommitted
}

и я использую RessourceAssemlber, как показано ниже для родительской сущности 'Storage':

@Component
public class StorageResourceAssembler implements ResourceAssembler<Storage, Resource<Storage>>  {

    @Override
    public Resource<Storage> toResource(Storage storage) {

        return new Resource<>(storage,
                linkTo(methodOn(StorageController.class).one(storage.getId())).withSelfRel(),
                linkTo(methodOn(StorageController.class).all()).withRel("storages"));
    }
}

, и в контроллере у меня есть 2 класса get для вывода списка всех или только одного хранилища с егоchilds

public class StorageController {
    private final StorageRepository repository;
    private final StorageResourceAssembler assembler;

        @GetMapping
    ResponseEntity<?>  all() {

        List<Resource<Storage>> storages = repository.findAll().stream()
                .map(assembler::toResource)
                .collect(Collectors.toList());

        Resources<Resource<Storage>> resources = new Resources<>(storages,
                linkTo(methodOn(StorageController.class).all()).withSelfRel());
        return ResponseEntity.ok(resources);
    }

    private static final Logger log = LoggerFactory.getLogger(StorageController.class);

    StorageController(StorageRepository repository, StorageResourceAssembler assembler) {
        this.repository = repository;
        this.assembler = assembler;
    }


    @GetMapping("/{id}")
    ResponseEntity<?> one(@PathVariable Long id) {
        try {
            Storage storage = repository.findById(id)
                    .orElseThrow(() -> new EntityNotFoundException(id));
            Resource<Storage> resource = assembler.toResource(storage);
            return ResponseEntity.ok(resource);
        }
        catch (EntityNotFoundException e) {
            log.info(e.getLocalizedMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body (new VndErrors.VndError("Storage not found", "could not find storage with id " + id ));
        }
    }

    ... omitted Put/Post/Delete

}

Может кто-нибудь просветить меня, как я могу решить этот бесконечный цикл в HateOAS. Я хочу, чтобы внедренные дочерние записи просто не ссылались на родительский элемент (поэтому ссылки на родительский элемент не создаются) или содержали ссылку для одного уровня, но дальнейшая обработка не выполнялась.

1 Ответ

0 голосов
/ 14 октября 2019

Чтобы решить проблему, связанную с сериализацией модели с использованием API-интерфейса Jackson, когда атрибуты модели определены с отложенной загрузкой, мы должны указать сериализатору игнорировать цепочку или полезный мусор, который Hibernate добавляет в классы, чтобы он мог управлятьотложенная загрузка данных путем объявления аннотации @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}).

@Entity
@Table(name = "storage")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {...



@Entity
@Table(name = "stock")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {...

или вы можете просто объявить одностороннее сопоставление, комментируя объявление сущности Storage и изменяя private Storage storage; для получения EAGER @ManyToOne(fetch = FetchType.EAGER) в классе Stock.

@Entity
@Table(name = "storage")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;

    /*@OneToMany(cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            mappedBy = "storage")
    private Set<Stock> storageStock = new HashSet<>();;*/
}

@Entity
@Table(name = "stock")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "storage_id")
    private Storage storage;

    ... other fields ommitted
}
...