Referência circular em JSON com relacionamento JPA Bidirecional com Chave Primária Composta

Como resolver o problema de referência circular ao Serializar e Desserializar JSON de classes POJO’s quando estas são referente à 2 tabelas com relacionamento bidirecional (OneToMany e Many To One) e ainda possui chave primária composta, segue abaixo minhas classes:
Obs.: No JPA esta tudo correto, gravando e consultando, o problema é só no JSON.

@Embeddable
public class RoteiroPK implements Serializable {
	
	private static final long serialVersionUID = 1L;

	@ManyToOne
	@JoinColumn(name = "produto_id")
	private Produto produto;
	
	@Column(name = "codigo")
	private String codigo;
}
@Entity
@Table(name = "tb_roteiro")
public class Roteiro implements Serializable {

	private static final long serialVersionUID = 1L;
	
	@Id
	private RoteiroPK roteiroPK;
	
	@Column(name = "descricao", nullable = true, length = 30)
	private String descricao;

	@OneToMany(cascade = CascadeType.ALL, targetEntity = Operacao.class, fetch = FetchType.EAGER)
	@JoinColumns({
		@JoinColumn(name = "roteiro_produto_codigo", referencedColumnName = "produto_codigo"),
		@JoinColumn(name = "roteiro_codigo", referencedColumnName = "codigo")
	})
	@JsonManagedReference
	private List<Operacao> operacoes;
}
@Embeddable
public class OperacaoPK implements Serializable {
	
	private static final long serialVersionUID = 1L;
	
	@ManyToOne()
	@JoinColumns({
		@JoinColumn(name = "roteiro_produto_codigo", nullable = false, referencedColumnName = "produto_codigo"),
		@JoinColumn(name = "roteiro_codigo", nullable = false, referencedColumnName = "codigo")
	})
	@JsonBackReference
	private Roteiro roteiro;
	
	@Column(name = "codigo")
	private String codigo;
}
@Entity
@Table(name = "tb_operacao")
public class Operacao implements Serializable {

	private static final long serialVersionUID = 1L;
	
	@Id
	private OperacaoPK operacaoPK;

	private int quant;
}

Veja que a Classe “Roteiro” possui uma lista de Operações (OneToMany), e a classe “Operacao” tem como chave primária composta a chave primária de Roteiro com relação (ManyToOne) + o código da operação, e ai que esta o problema, não consigo usar corretamente as anotações @JsonManagedReference e @JsonBackReference, também tentei @JsonIdentifyInfo, segue abaixo o arquivo JSON que preciso que seja desserealizado é o seguinte.

{
    "roteiroPK": {
        "produto": {
            "codigo": "P001"
        },
        "codigo": "R1"
    },
    "descricao": "ROTEIRO TESTE",

    "operacoes": [
        {
            "operacaoPK": {
                "roteiro": {
                    "roteiroPK": {
                        "produto": {
                            "codigo": "P001"
                        },
                        "codigo": "R1"
                    }
                },
                "codigo": "O1"
            },
            "descricao": "OPERACAO TESTE 1"
        },
        {
            "operacaoPK": {
                "roteiro": {
                    "roteiroPK": {
                        "produto": {
                            "codigo": "P001"
                        },
                        "codigo": "R1"
                    }
                },
                "codigo": "O2"
            },
            "descricao": "OPERACAO TESTE 2"
        }
    ]
}

Mas ocorre o seguinte erro:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed&#x2F;back reference
&#x27;defaultReference&#x27;: no back reference property found from type [collection type; class java.util.List,
contains [simple type, class br.com.baterax.bateraxmes.model.Operacao]]
at [Source: (io.undertow.servlet.spec.ServletInputStreamImpl); line: 1, column: 1]

Quanto tento serializar, não ocorre erro, gerando o seguinte JSON:

{
    "roteiroPK": {
        "produto": {
            "codigo": "P001"
        },
        "codigo": "R1"
    },
    "descricao": "ROTEIRO TESTE",
    "operacoes": [
        {
            "operacaoPK": {
                "codigo": "O1"
            },
            "descricao": "OPERACAO TESTE 1"
        },
        {
            "operacaoPK": {
                "codigo": "O2"
            },
            "descricao": "OPERACAO TESTE 2"
        }
    ]
}

Nele foi omitido o seguinte trecho que vai dentro de operacaoPK, isto devido ao JsonBackReference, quanto a isso não vejo problemas, mas preciso resolver o problema de não conseguir desserializar o JSON que envio, se eu tentar copiar este JSON gerado e enviar pelo POSTMAN, ocorre erro do mesmo jeito, mesmo ainda se eu tentar inserir o trecho faltante ou não.

               "roteiroPK": {
                    "produto:" {
                        "codigo": "050210001"
                    },
                    "codigo": "01"
                },

Geralmente, gosto muito de ter classes para separar o mundo http e as entidades da aplicação. Com isso, ganho flexibilidade ao transformar os dados que entram na aplicação, assim como os dados que são retornados via json.

No seu caso, eu teria algo assim:

Para a classe Roteiro eu teria outra equivalente RoteiroResource

public class RoteiroResource {
	
	// aqui vc define os campos que vc quer retornar na requisição http
	
	public RoteiroResource(Roteiro roteiro) {
		// aqui vc faz o de-para dos campos que retornar do roteiro
	}

	// métodos getter apenas (não precisa criar setters aqui)
}

Com essa classe, vc consegue facilmente (e sem ficar misturando anotações do jackson nas suas entidades) contornar esse problema da serialização.

A ideia que passei faz sentido pra vc?

1 curtida

Olá @Lucas_Camara, obrigado pelo retorno, faz muito sentido sim, vou tentar aplicar aqui na minha situação, mas deixa eu ver se entendi bem:

  • Vou criar uma nova Classe RoteiroResource que vai representar o meu JSON recebido pelo WebService REST pelo método “POST” .
  • Vou ter que converter ele para a entidade Roteiro para persistir os dados no banco conforme exemplo abaixo do meu serviço?
@POST
@Path("/importar")
@Consumes({MediaType.APPLICATION_JSON})
public Response importar(RoteiroResource roteiroResource) {
	
	Roteiro roteiro = new Roteiro();
	roteiro.setRoteiroPK(new RoteiroPK(roteiroResource.getProduto(), roteiroResource.getCodigo()));
	roteiro.setDescricao(roteiroResource.getdescricao());
		
	try {
		roteiroService.merge(roteiro);
		return Response.ok().build();
	} catch (Exception e) {
		e.printStackTrace();
		return Response.serverError().build();
	}
}

@Lucas_Camara, e como posso resolver isso sem essa Classe RoteiroResource, usando as anotações do Jackson, é possivel fazer isso com chaves compostas?

Recomendo fortemente seguir o formato que o @Lucas_Camara recomendou!

Mas caso prefira sofrer, talvez o uso de @JsonIgnoreProperties(value = {"roteiro"}) em operacoes resolva momentâneamente o seu problema.

Uma boa forma seria vc dar a responsabilidade de criar o Roteiro para a classe RoteiroResource. Poderia ter um método assim:

public class RoteiroResource {
	
	// aqui vc declara os dados recebidos no request
	
	public Roteiro criar() {
		Roteiro roteiro = new Roteiro();
		// popula os parâmetros do roteiro com os dados recebidos no request
		
		return roteiro;
	}
}

Uma dica que te dou, seria dá um nome para essa classe que indique melhor a responsabilidade dela, como por exemplo: ImportarRoteiroResource, com isso, ela fica mais coerente com a funcionalidade em que ela está sendo usada, e vc soh vai precisar mexer nela caso seja algo envolvendo a importação do roteiro, sacou? (mas isso é soh uma dica ok, não se trata de ser certo ou errado)

Obrigado @Lucas_Camara, ontem implementei com sucesso a dica que você me passou e consegui contornar o problema da referência circular do Jackson, veja como ficou a implemantação:

Classe RoteiroResource

public class RoteiroResource implements Serializable {

	private static final long serialVersionUID = 1L;
	private RoteiroPK roteiroPK;
	private String descricao;	
	private List<Operacao> operacoes;

	public RoteiroResource() {
		
	}
	public RoteiroResource(RoteiroPK roteiroPK, String descricao, List<Operacao> operacoes) {
		this.roteiroPK = roteiroPK;
		this.descricao = descricao;
		this.operacoes = operacoes;
	}
	
	public Roteiro toRoteiro() {
		Roteiro roteiro = new Roteiro();
		roteiro.setRoteiroPK(roteiroPK);
		roteiro.setDescricao(descricao);
		roteiro.setOperacoes(operacoes);
		
		return roteiro;
	}

e no método POST

@POST
	@Path("/importar")
	@Consumes({MediaType.APPLICATION_JSON})
	public Response importar(RoteiroResource roteiroResource) {
		
		Roteiro roteiro = roteiroResource.toRoteiro();
		List<Operacao> operacoes = roteiro.getOperacoes();
		
		try {
			roteiroService.merge(roteiro);
			
			for (Operacao operacao : operacoes) {
				try {
					operacaoService.merge(operacao);
				} catch (Exception e) {
					e.printStackTrace();
					return Response.serverError().build();
				}
				
			}
			
			return Response.ok().build();
		} catch (Exception e) {
			e.printStackTrace();
			return Response.serverError().build();
		}
	}

@Jonathan_Medeiros, muito obrigado! Era só por curiosidade mesmo, gosto de aprender desde a forma fácil até a difícil, mas como você disse, prefiro não sofrer, kkk.