A coleção está nullo e nao salva o id no banco de dados

Post:

Get:

Classe:

DB:

Alguem sabe dizer onde está o erro? Ao criar o post, salva direito deste o id, mas quando vai visualizar no banco de dados está null e ao dar get a coleção está vazia. Os id que possui identificação, foram criado na classe “TestConfig”.

Antes de salvar, vc precisa percorrer a lista de Order e setar em cada uma o Client manualmente. Algo assim:

client.getOrders().forEach(order -> order.setClient(client));

E vc vai ter que fazer o mesmo para os Product.

Aqui tem uma explicação melhor:

1 curtida

Obrigado pelo material, vou assistir!
Vou tentar acessar todos os pedidos, por que não estou entendendo por que está null.

Esta nulo porque quando ele transforma o JSON em objetos, ele não seta do campo client das suas orders, vc deve fazer isso manualmente.

Deixa ver se entendi então.
Preciso salvar manualmente as orders na classe client ou no endpoint client?

No endpoint que recebe aquele JSON, vc vai ter que percorrer o conjuto de orders e setar nelas o client.

O Spring, ao transformar seu JSON em objetos, ele seta o conjunto de orders para vc no client, mas ele não seta o client nas orders, vc tem que fazer isso manualmente. Assim:

@PostMapping
Client create(@RequestBody Client client) {
  client.getOrders().forEach(order -> {
    order.setClient(client);
    order.getProducts().forEach(product -> product.addOrder(order));
  });

  return repo.save(client);
}
O exemplo completo AQUI!
package com.example.demo;

import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;

@SpringBootApplication
class Application {
  public static void main(String... args) {
    SpringApplication.run(Application.class, args);
  }
}

@Entity
class Client {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String name;

  @OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
  private Set<Order> orders;

  public int getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public Set<Order> getOrders() {
    return orders;
  }
}

@Entity
class Order {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private Instant moment;

  @ManyToOne
  private Client client;

  @ManyToMany(mappedBy = "orders", cascade = CascadeType.ALL)
  private Set<Product> products;

  public void setClient(Client client) {
    this.client = client;
  }

  public int getId() {
    return id;
  }

  public Instant getMoment() {
    return moment;
  }

  public Set<Product> getProducts() {
    return products;
  }
}

@Entity
class Product {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String name;

  @ManyToMany
  private Set<Order> orders = new HashSet<>();

  public void addOrder(Order order) {
    this.orders.add(order);
  }

  public int getId() {
    return id;
  }

  public String getName() {
    return name;
  }
}

interface ClientRepository extends JpaRepository<Client, Integer> {
  @Query("FROM Client c LEFT JOIN FETCH c.orders o LEFT JOIN FETCH o.products")
  List<Client> findAll();
}

@RestController
@RequestMapping("/clients")
class ClientController {
  @Autowired
  private ClientRepository repo;

  @PostMapping
  Client create(@RequestBody Client client) {
    client.getOrders().forEach(order -> {
      order.setClient(client);
      order.getProducts().forEach(product -> product.addOrder(order));
    });

    return repo.save(client);
  }

  @GetMapping
  List<Client> list() {
    return repo.findAll();
  }
}
1 curtida

Preparei outro exemplo para ilustrar fora do ambiente Spring.

O Spring usa o Jackson para serializar e deserializar JSON, por isso eu o usei também.

Repare que no primeiro println o client aparece como null. Isso porque o JSON que a gente enviou não contem um valor para o campo “client” da Order e o Jackson simplesmente não tem como adivinhar o valor deste campo.

Neste exemplo o Jackson não acha o valor do campo “client” e por isso seta como null.

{
  "id": 1,
  "name": "João",
  "orders": [
    { "id": 1, "moment": "2019-01-21T05:47:26.853Z" }
  ]
}

Para ele achar, vc teria que fazer algo assim:

{
  "id": 1,
  "name": "João",
  "orders": [
    {
      "id": 1,
      "moment": "2019-01-21T05:47:26.853Z",
      "client": { "id": 1, "name": "João" }
    }
  ]
}

Já no segundo println o client aparece corretamente, isto porque eu o setei “manualmente”.

Eu criei o exemplo com Gradle, este é o build.gradle que usei:

plugins {
  id 'application'
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'
  implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0'
}

application {
  mainClass = 'Main'
}

Para executar, use o comando gradle run. Testei com Java 17.

Este é o código:

Obs: Repare no método toString() de Order que eu usei client == null ? client : client.getName(), isto porque senão dá aquele erro de referência ciclica e eu queria poder ver que o cliente está presente de alguma forma.

import java.time.LocalDate;
import java.util.Set;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

class Order {
  private int id;
  private LocalDate moment;
  private Client client;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public LocalDate getMoment() {
    return moment;
  }

  public void setMoment(LocalDate moment) {
    this.moment = moment;
  }

  public Client getClient() {
    return client;
  }

  public void setClient(Client client) {
    this.client = client;
  }

  @Override
  public String toString() {
    return "Order[id=%d, moment=%s, client=%s]".formatted(id, moment, client == null ? client : client.getName());
  }
}

class Client {
  private int id;
  private String name;
  private Set<Order> orders;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Set<Order> getOrders() {
    return orders;
  }

  public void setOrders(Set<Order> orders) {
    this.orders = orders;
  }

  @Override
  public String toString() {
    return "Client[id=%d, name=%s, orders=%s]".formatted(id, name, orders);
  }
}

class Main {
  public static void main(String... args) throws Exception {
    final var json = """
      {
        "id": 1,
        "name": "João",
        "orders": [
          { "id": 1, "moment": "2019-01-21T05:47:26.853Z" }
        ]
      }
      """;

    final Client client = new ObjectMapper().registerModule(new JavaTimeModule()).readValue(json, Client.class);

    System.out.println(client);

    client.getOrders().forEach(order -> order.setClient(client));

    System.out.println(client);
  }
}

Tenho novidades!

Para lidar com o problema em relações manytoone, vc pode usar a duplinha @JsonManagedReference e @JsonBackReference. Ficaria assim:

class Client {
  /* ... */
  @JsonManagedReference @OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
  private Set<Order> orders;
  /* ... */
}


class Order {
  /* ... */
  @JsonBackReference @ManyToOne
  private Client client; // não precisa mais do @JsonIgnore
  /* ... */
}

@PostMapping
Client create(@RequestBody Client client) {
  client.getOrders().forEach(order -> order.getProducts().forEach(product -> product.addOrder(order)));
  return repo.save(client);
}

Como o relacionamento entre Order e Product é manytomany vc ainda precisa setar manualmente as orders nos products, ainda não descobri se dá para fazer automaticamente também.

1 curtida

Entendi sobre o problema que estava passando. Muito obrigado pela atenção!!
Vou usar as anotações mencionadas para JsonIgnore.

Mas agora tenho outro problema kkk, não sei se o problema é a lambda ou a própria classe, mas um pedido apenas consegue salvar um produto e não um pedido consegue salvar mais de um produto.

Vou repassar a imagem e o código.

Post:

Salvamento

Tentei resolver, mas parece que minha lógica nao deu muito certo kkkk

Pior que eu não consegui identificar o erro porque eu fiz igualzinho vc e salvou todos os produtos.

Vc olhou certinho no DB e realmente só salvou 1?

Apenas o primeiro pedido “cake”, os “cookies” não foram salvo. Desenvolve essa mesma aplicação no TestConfig e salva os dois.

TestConfig:

Post:

É neste TestConfig vc salvou cada entidade usando seu próprio repositório.

Bom, só olhando não sei mesmo, mas se seu projeto estiver no GitHub eu posso testar aqui.

Pior que não coloquei no github ainda, mas vou tentar quebrar a cabeça nesse problema.

Então testa este código aqui e me diz se funcionou, por favor.

Se funcionar, compara com o seu e veja se tem algo diferente.

JSON do body do POST no Postman
{
    "name": "João",
    "orders": [
        {
            "moment": "2019-01-21T05:47:26.853Z",
            "products": [
                {
                    "name": "Pizza"
                },
                {
                    "name": "Hamburguer"
                },
                {
                    "name": "Lazanha"
                }
            ]
        }
    ]
}
build.gradle
plugins {
  id 'java'
  id 'org.springframework.boot' version '3.0.6'
  id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  runtimeOnly 'com.h2database:h2'
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
  useJUnitPlatform()
}
Aplicação Spring
package com.example.demo;

import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.Setter;

@SpringBootApplication
class Application {
  public static void main(String... args) {
    SpringApplication.run(Application.class, args);
  }
}

interface ClientRepository extends JpaRepository<Client, Integer> {
  @Query("FROM Client c LEFT JOIN FETCH c.orders o LEFT JOIN FETCH o.products")
  List<Client> findAll();
}

@RestController
@RequestMapping("/clients")
class ClientController {
  @Autowired
  private ClientRepository repo;

  @PostMapping
  Client create(@RequestBody Client entity) {
    entity.getOrders().forEach(order -> {
      order.setClient(entity);
      order.getProducts().forEach(product -> {
        order.getProducts().add(product);
        product.getOrders().add(order);
      });
    });

    return repo.save(entity);
  }

  @GetMapping
  List<Client> list() {
    return repo.findAll();
  }
}

@Entity
@Getter
@Setter
class Client {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String name;

  @JsonManagedReference @OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
  private Set<Order> orders;
}

@Entity
@Getter
@Setter
class Order {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private Instant moment;

  @JsonBackReference @ManyToOne
  private Client client;

  @ManyToMany(mappedBy = "orders", cascade = CascadeType.ALL)
  private Set<Product> products = new HashSet<>();
}

@Entity
@Getter
@Setter
@JsonIdentityReference
class Product {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String name;

  @JsonIgnore @ManyToMany
  private Set<Order> orders = new HashSet<>();
}

Problema foi algo nas classes mesmo, por que o seu deu certinho

estava pensando, sera que é o hashcode?

Consegui resolver! O problema era o hashcode da classe Product, que apenas possui o Equals do Id, ou seja, talvez estava se sobreescrevendo no JSON. Agora consegui.

Mais um problema que nem sabia que causaria kkkkk

Putz! Que legal, nem imaginei que poderia ser isso.

Mas então não era o hashCode() não, era o equals().

Eu fiz um teste com isto:

class Product {
  /* ... */
  
  @Override
  public int hashCode() {
    return this.id;
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof Product p && p.id == this.id;
  }
}

E realmente cadastrou só um. Então eu mudei só o equals() para isto:

@Override
public boolean equals(Object o) {
  return o instanceof Product p && p.name.equals(this.name);
}

E ele cadastrou de boa.

E é exatamente isto que está escrito na documentação, veja:

HashSet (Java SE 17 & JDK 17)
… adds the specified element e to this set if this set contains no element e2 such that Objects.equals(e, e2)

E o Objects.equals() chama o equals() dos argumentos internamente.

1 curtida

Entendi! Mas bah, nunca pensei que ate nisso poderia causar confusão, pois sempre aprendi a usar hashcode e equal, so com id, mas pelo visto tem que colocar todos os atributo (menos as coleções, se nao pode dar stackoverflow).

Mas muito obrigado!! Me ajudou bastante.

É que quando o assunto são entidades JPA o negócio é mais complicado, por causa do ciclo de vida delas.

No inicio, o id de todas é null ou outro default. Ela só vai ter um valor significativo depois que for salva e o banco gerar o id.

Depois dá uma olhada nisso:

1 curtida