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

19 respostas Resolvido
javaspringmysqlprogramação
B

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”.

19 Respostas

W

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:

B

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

W

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.

B

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

W
Solucao aceita

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

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);
  }
}
W

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.

B

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

W

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?

B

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

TestConfig:

Post:

W

É 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.

B

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

W

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

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

B

estava pensando, sera que é o hashcode?

B

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

W

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.

B

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.

W

É 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:

Criado 27 de abril de 2023
Ultima resposta 28 de abr. de 2023
Respostas 19
Participantes 2