How to avoid infinite recursion without using the Jsonignore annotation in Springboot

Asked

Viewed 1,584 times

5

I have a relationship @OneToMany amid Produto and TipoProduto where a TipoProduto may have several Produtos and a Produto may have only one TipoProduto.

I’d like to list all the Produtos of a TipoProduto when researching about it and also would like to list the TipoProduto of a Produto when searching for it. I tried to overwrite the method toString() but without success. I had some ideas but I do not know if they are valid, so I wonder if there is a way to avoid this or so the problem is in the design of my application.

TipoProduto:

public class TipoProduto {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String descricao;
    @OneToMany(mappedBy = "tipoProduto", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JsonIgnore
    private Set<Produto> produtos;
    ....
}

Produto:

public class Produto {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String nome;
    @ManyToOne
    @JoinColumn(name = "tipo_produto_id", nullable = true)
    private TipoProduto tipoProduto;
    ...
}

2 answers

4


Why it’s not a good idea to turn entities into JSON directly

The error is in wanting to turn your JPA entities into JSON. I’ve been a part of a project that’s been months behind just because of this.

Transforming JPA entities into JSON is problematic because of:

  • Bond problems - it’s hard to know who the father is and who the son is. Sometimes, in some cases you will want the children telling you who the parents are and sometimes you will want the parents telling you who the children are.

  • The JPA entity format is not always the same as the format desired in JSON. For example, if you have an entity Usuario with a field senha, will not want the field senha appearing in JSON. If you have an entity Vendedor and you wonder how many sales he made in the month, JSON would get huge and polluted with all the sales information when the only thing you want is to know how many sales are.

  • You cannot contextualize JSON so that you have a set of certain information in one context and a different set of information in another context.

  • There are cases where the data you need to export or import does not correspond directly to any of your entities.

All of these problems stem from the fact that JPA mapping serves to make the application’s object-relational mapping, while JSON-object mapping serves a completely different purpose. By placing the two in the same classes, you end up mapping the database tables to JSON, which is usually not what you want to do.

The solution

The solution is to create a group of classes in parallel to represent your JSON. In another project other than the one mentioned above, when using this approach we had virtually no problems with JSON and this part of the project was extremely quiet and simple to work with.

In the middle of the project, we have separated these classes that represent JSON into a separate library so as not to run the risk of mixing (occasionally by accident). In separating them, any attempt to mix them with the entities became a compilation error, as the package with the entities was dependent on the package of classess representing JSON, but the reverse was not.

For example:

public final class ProdutoJSON {
    private final String nome;
    private final Long id;
    private final String descricaoTipo;
    private final Long idTipo;

    public ProdutoJSON(String nome, Long id, String descricaoTipo, Long idTipo) {
        this.nome = nome;
        this.id = id;
        this.descricaoTipo = descricaoTipo;
        this.idTipo = idTipo;
    }

    // Acrescente os getters aqui.
}
public final class TipoProdutoJSON {
    private final String descricao;
    private final Long id;
    private final List<ProdutoPorTipoJSON> produtosListados;

    public TipoProdutoJSON(String descricao, Long id, List<ProdutoPorTipoJSON> produtosListados) {
        this.descricao = descricao;
        this.id = id;
        this.produtosListados = produtosListados;
    }

    // Acrescente os getters aqui.
}
public final class ProdutoPorTipoJSON {
    private final String nome;
    private final Long id;

    public ProdutoPorTipoJSON(String nome, Long id) {
        this.nome = nome;
        this.id = id;
    }

    // Acrescente os getters aqui.
}

Note that the above classes are just a bunch of raw data and they have an immutable structure. This is because their only purpose is to only be used to structure Jsons and nothing more. They should have the desired JSON structure with nothing more and nothing less, so don’t worry too much about reusing them. They can be modeled to the liking of your tool, be it Jackson, GSON or any other you are using, leaving these classes free of any restrictions or rules of JPA.

Here’s how you instantiate these classes from their entities:

public class TipoProduto {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String descricao;

    @OneToMany(mappedBy = "tipoProduto", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<Produto> produtos;

    // ...

    public TipoProdutoJSON criarJsonComProdutos() {
        List<ProdutoPorTipoJSON> p = produtos
                .stream()
                .map(Produto::criarJsonSemTipoProduto)
                .collect(Collectors.toList());

        return new TipoProdutoJSON(descricao, id, p);
    }
}
public class Produto {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String nome;

    @ManyToOne
    @JoinColumn(name = "tipo_produto_id", nullable = true)
    private TipoProduto tipoProduto;

    // ...

    public ProdutoPorTipoJSON criarJsonSemTipoProduto() {
        return new ProdutoPorTipoJSON(nome, id);
    }

    public ProdutoJSON criarJsonComTipoProduto() {
        return new ProdutoJSON(
                nome,
                id,
                tipoProduto == null ? null : tipoProduto.getDescricao(),
                tipoProduto == null ? null : tipoProduto.getId());
    }
}
  • Your example hurts some REST design guidelines, such as when you cite the Vendedor with Venda. Although there is a relationship between the resources, if you want to visualize the sales of a seller, the most efficient way to work this would be through the way /vendedores/{id}/vendas, not returning objects implicitly. This avoids various problems of both memory waste and data traffic on the network.

  • @Weslleytavares No. When you use the way vendedores/{id}/vendas, a service will answer for that URL, search for the corresponding seller with Vendedor v = em.find(id); and then produce the list of objects to be serialized with List<VendaPorVendedorJSON> lista = v.listarJsonVendas();. There is no increase in network traffic with this and the REST mapping remains unchanged.

  • As for memory consumption, it depends on the case. But in this above approach, consumption tends to be lower due to the fact that by transforming the entity into a JSON, you’ll probably end up accessing several parts of the object graph brought by JPA, producing a swollen JSON that occupies more memory and tends to be heavier in traffic. It is true that this is not necessarily going to happen, but it is very easy to control this by mapping the JSON into separate objects, while it is very difficult to control this by mapping the JSON directly into the entities.

  • Thanks @Victorstafusa, had already read something about it, it resembles the right DTO standard?

  • @Diegoaugusto It resembles so much that it is exactly what we are talking about. : ) - These classes are nothing less than Dtos.

  • haha, I get it. thanks! The answer was very enlightening.

Show 1 more comment

2

Quite possibly your application has a design problem, as it is suffering from this circular reference problem (or circular dependency, as some call it).

If this were the case to restructure the design, it would fit the old question:

In the case of the two entities, in a scenario of exclusion of one of them, who remains alive without relying on the other?

Now if the solution implemented by you is inherent in the application business, little can be done about the design.

For these cases, there are several "contouring solutions" (famous workarounds).

In the context of Spring, take a look in this excellent material. Among the many options he shows, choose one that best suits your scenario.

Of the cases cited by the author, I have used the reference method set to make the injection of the Beans.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.