0

I'm beginner with JPA and Spring. Also my first question in here. So, sorry for my mistakes. I'm practicing with simple scenario for beginning. I have two entities as Product and Category with bi-directional many-to-one/one-to-many association.

Category class :

@Entity
@Table(name = "categories")
@NoArgsConstructor
@AllArgsConstructor
@Data
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Long.class)

public class Category implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "name", length = 50, nullable = false)
    private String name;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
    private List<Product> products = new ArrayList<Product>();
}

Product class :

@Table(name = "products")
@NoArgsConstructor
@AllArgsConstructor
@Data
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Long.class)

public class Product implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "title", nullable = false, length = 50)
    private String title;

    @Column(name = "price", precision = 4, scale = 2, nullable = false)
    private Double price;

    @Column(name = "quantity", nullable = false)
    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", foreignKey = @ForeignKey(name = "FK_product_category"))
    private Category category;
}

Both of CategoryRepository and ProductRepository are implementing from JpaRepository. There are few method signatures with @Query annotations.

Services using repositories with @Autowired. There aren't any business logic.

CategoryService class :

@Transactional
public class CategoryService implements IRepositoryService<Category, Long> {

    private ICategoryRepository categoryRepository;

    @Autowired
    public CategoryService(ICategoryRepository categoryRepository) {
        this.categoryRepository = categoryRepository;
    }

    @Override
    public List<Category> findAllOrderById() {
        return categoryRepository.findAllOrderById();
    }

    @Override
    public Category findById(Long id) {
        return categoryRepository.findById(id).orElseThrow(EntityNotFoundException::new);
    }

    @Override
    public List<Category> findByForeignKey(Long categoryId) {
        return null;
    }

    @Override
    public void add(Category category) {
        category.getProducts().forEach(product -> product.setCategory(category));
        categoryRepository.save(category);
    }

    @Override
    public void update(Category category) {
        categoryRepository.save(category);
    }

    @Override
    public void deleteById(Long id) {
        categoryRepository.deleteById(id);
    }

ProductService class :

@Transactional
public class ProductService implements IRepositoryService<Product, Long>{

    @Autowired
    private IProductRepository productRepository;

    @Override
    public List<Product> findAllOrderById() {
        return productRepository.findAllOrderById();
    }

    @Override
    public Product findById(Long id) {
        return productRepository.findById(id).orElseThrow(EntityNotFoundException::new);
    }

    @Override
    public List<Product> findByForeignKey(Long categoryId) {
        return productRepository.findByCategoryId(categoryId);
    }

    @Override
    public void add(Product entity) {
        productRepository.save(entity);
    }

    @Override
    public void update(Product entity) {
        productRepository.save(entity);
    }

    @Override
    public void deleteById(Long id) {
        productRepository.deleteById(id);
    }
}

At final, both entities have seperate restcontroller classes. I'm adding a category from Postman with only name, so product is null (This part is normal). But when I add a product from Postman or another frontend app, product's category is not set (I'm setting category_id with json). But in database, products table's category_id column values are null. json string

Also I've problem with lazy fetch type and json problem. But main problem comes first.

Thank you for any help.

2 Answers2

0

The above JPA entity mapping should work as long as you are passing correct json request body. Also, you might need to use @JsonBackReference/@JsonManagedReference to avoid stackoverflow exception (see JsonManagedReference vs JsonBackReference for more info)

Requests: POST: localhost:/cat

{ "name" :"Construction" }

POST: localhost:/prod, here we are using category id = 1. change it to match the id of 'Construction' category

{
"title" :"Bricks",
"price" :"1.2",
"quantity": "100",
"category": {
        "id": 1 
    }
}

Add this class to jpademo package and run.

package jpademo;

import com.fasterxml.jackson.annotation.*;
import lombok.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.web.bind.annotation.*;

import javax.persistence.*;
import java.util.*;

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

@RestController
@RequiredArgsConstructor
class Ctrl {

    final CategoryRepo categoryRepo;
    final ProductRepo prodRepo;

    @PostMapping("/cat")
    void cat(@RequestBody Category cat) {
        categoryRepo.save(cat);
    }

    @PostMapping("/prod")
    void prod(@RequestBody Product p) {
        prodRepo.save(p);
    }

    @GetMapping("/cat")
    List<Category> cats() {
        return categoryRepo.findAll();
    }

    @GetMapping("/prod")
    List<Product> prods() {
        return prodRepo.findAll();
    }

}

interface CategoryRepo extends JpaRepository<Category, Long> {}

interface ProductRepo extends JpaRepository<Product, Long> {}

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
    @JsonManagedReference //to avoid Stackoverflow
    private List<Product> products = new ArrayList<>();
}

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private Double price;

    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id", foreignKey = @ForeignKey(name = "FK_product_category"))
    @JsonBackReference
    private Category category;
}
gtiwari333
  • 24,554
  • 15
  • 75
  • 102
  • Thank you for your help. I realized that my json body was wrong. I saw my fault, thanks to you. Now, I have another problem with perfomance. When I send a one request(such as findAllProducts or Categories) hibernate produces many sql queries(sql count is equal with entity number). I could handle with jackson-datatype-hibernate module but this time, fetched fields come with null value. I think I need to configure this module. – chevalier_32 Sep 30 '20 at 09:28
  • You might need to do join-fetch so that the required data is fetched in single query. See this for an example of @EntityGraph to fetch the records https://stackoverflow.com/questions/64013319/spring-data-jpa-hibernate-handling-associations/64013435#64013435 – gtiwari333 Sep 30 '20 at 13:32
  • Since this is a different question.. feel free to ask a new question.. if https://stackoverflow.com/questions/64013319/spring-data-jpa-hibernate-handling-associations/64013435#64013435 doesn't help – gtiwari333 Sep 30 '20 at 13:34
  • It doesn't seem something like my problem. Also, JsonBackReferance and JsonManagedReferance make me lose the fetched fields. Like when I listed categories, just id and name fields come in json (no products column). So I'm searching for fixing this with jackson module but still couldn't find a solution. – chevalier_32 Oct 03 '20 at 07:00
0

This is the only configuration for using jackson datatype module.

@Configuration
public class JacksonConfig {

    @Bean
    public Hibernate5Module hibernate5Module() {
        return new Hibernate5Module();
    }

    @Bean
    public AfterburnerModule afterburnerModule() {
        return new AfterburnerModule();
    }
}

And the response like that : json response