如何在 POST 请求中正确连接 SpringBoot JPA 实体?

How to connect the SpringBoot JPA Entities in a POST request properly?

我正在创建图书馆管理。我已经创建了实体之间的关系并创建了存储库。一切正常,但它仅适用于单独创建实体。例如,我想发送一个 POST 请求创建一本书,并创建作者和主题。但我只能创作这本书,我想知道实现这一目标的最佳方法是什么。我不知道我是否应该创建服务 类 或使用 DTO 我真的不知道。

@RestController
@RequestMapping(path ="/api/books")
public class BookController {

    @Autowired
    BookRepository bookRepository;

    @PostMapping
    public Book addBook(@RequestBody Book book) {
        bookRepository.save(book);
        return book;
    }
}
@Entity
@Table
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Book {

    public Book(String title) {
        this.title = title;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private int isbn;

    @Column
    private String title;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Author> authors = new ArrayList<>();

    @ManyToMany(fetch = FetchType.LAZY)
    private List<BookTheme> bookThemes = new ArrayList<>();
}

这是我的书籍控制器和实体(模型)存储库,它很简单我只是扩展了 CRUD 存储库接口...无论如何我怀疑它我知道如何创建和保存分离的对象但我不知道知道如何合并它们,如何使它们的关系起作用,比如在请求正文中将两位作者添加到一本书中……我设法将它们保存在一个唯一的请求中,但仅使用参数,有时我会创建一个循环,因为我保存了它们两个都。我知道这是错误的,但我不知道如何处理。

package com.msoftwares.librarymanager.models.Entities;
    
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

@Entity
@Table
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Author {

    public Author(String name) {
        this.name = name;
    }

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

    @Column
    private String name;

    @ManyToMany(mappedBy = "authors", cascade = CascadeType.ALL)
    private List<Book> book = new ArrayList<>();
}

这是我的 Author 实体:

@RestController
@RequestMapping(path ="/api/author")
public class AuthorController {

    @Autowired
    AuthorRepository authorRepository;

    @PostMapping
    public Author addAuthor(@RequestParam String name){
        Author author = new Author(name);
        authorRepository.save(author);
        return author;
    }
}

和作者控制器,其他实体与此类似,我不知道我应该怎么做,我看到了很多,但是较旧的教程,我不知道我是否应该使用那种方法,他们似乎没有回答我的问题如何使 post 请求中的对象与它们的关系进行交互,例如在同一请求中添加书籍和作者并保存在数据库中。

让我们在 BookAuthor 之间做一个 proof-of-concept 和 many-to-many,包括集成测试。我留给你完成 BookTheme 部分。

请注意,我稍微简化了您的代码,并将您的 collections 从 List 更改为 Set(后者是关于坚持 collections)。

作者实体

package no.mycompany.myapp.book;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

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

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Author {

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

    private String name;

    @ManyToMany(mappedBy = "authors")
    private Set<Book> books = new HashSet<>();

    public Author(String name) {
        this.name = name;
    }
}

AuthorVM (DTO)

package no.mycompany.myapp.book;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class AuthorVM {

    private long id;

    @NotNull
    private String name;

    public AuthorVM(Author author) {
        this.id = author.getId();
        this.name = author.getName();
    }
}

图书实体

package no.mycompany.myapp.book;

import lombok.Getter;
import lombok.Setter;

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

@Entity
@Getter
@Setter
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long isbn;

    private String title;

    @ManyToMany
    private Set<Author> authors = new HashSet<>();

    public void addAuthor(Author author) {
        authors.add(author);
    }
}

BookVM(DTO)。请注意 List<@Valid AuthorVM> 的使用。这是为了启用每个 AuthorVM 实例的验证。

package no.mycompany.myapp.book;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@Setter
@NoArgsConstructor
public class BookVM {
    private long isbn;

    @NotNull
    private String title;

    @Size(min = 1)
    private List<@Valid AuthorVM> authors = new ArrayList<>();

    public BookVM(Book book) {
        this.isbn = book.getIsbn();
        this.title = book.getTitle();
        this.authors = book.getAuthors().stream()
                .map(AuthorVM::new)
                .collect(Collectors.toList());
    }
}

BookRepo

package no.mycompany.myapp.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepo extends JpaRepository<Book, Long> {
}

AuthorRepo

package no.mycompany.myapp.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepo extends JpaRepository<Author, Long> {
}

图书服务

package no.mycompany.myapp.book;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepo bookRepo;
    private final AuthorRepo authorRepo;

    public Book saveBook(BookVM bookVM) {

        // create new  book instance
        var book = new Book();
        book.setTitle(bookVM.getTitle());

        // fetch existing authors from db, save new authors to db, attach author to book
        bookVM.getAuthors().forEach(authorVM -> book.addAuthor(
                authorVM.getId() > 0
                        ? authorRepo.getOne(authorVM.getId())
                        : authorRepo.save(new Author(authorVM.getName()))));

        // save book and return saved instance
        return bookRepo.save(book);
    }
}

最后,控制器

package no.mycompany.myapp.book;

import lombok.RequiredArgsConstructor;
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 javax.validation.Valid;

@RestController
@RequestMapping(path ="/api/books")
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;

    @PostMapping
    public BookVM addBook(@Valid @RequestBody BookVM book){
        return new BookVM(bookService.saveBook(book));
    }
}

以及一些验证一切正常的集成测试。

package no.mycompany.myapp.book;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;

import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class BookControllerTest {

    static final String API_BOOKS = "/api/books";

    @Autowired
    WebTestClient webTestClient;

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Autowired
    private BookRepo bookRepo;

    @Autowired
    private AuthorRepo authorRepo;

    @BeforeEach
    public void cleanupDb() {
        bookRepo.deleteAll();
        authorRepo.deleteAll();
    }

    @Test
    public void postBook_bookNotValid_receiveBadRequest() {
        var book = createValidBook();
        book.setTitle(null);

        webTestClient.post().uri(API_BOOKS)
                .bodyValue(book)
                .exchange()
                .expectStatus().isBadRequest();
    }

    @Test
    public void postBook_bookWitNoAuthors_receiveBadRequest() {
        var book = createValidBook();
        book.getAuthors().clear();

        webTestClient.post().uri(API_BOOKS)
                .bodyValue(book)
                .exchange()
                .expectStatus().isBadRequest();
    }

    @Test
    public void postBook_bookWithInvalidAuthor_receiveBadRequest() {
        var book = createValidBook();
        book.getAuthors().get(0).setName(null);

        webTestClient.post().uri(API_BOOKS)
                .bodyValue(book)
                .exchange()
                .expectStatus().isBadRequest();
    }

    @Test
    public void postBook_validBook_receiveOk() {
        webTestClient.post().uri(API_BOOKS)
                .bodyValue(createValidBook())
                .exchange()
                .expectStatus().isOk();
    }

    @Test
    public void postBook_validBook_receiveCreatedBookWithCreatedAuthor() {
        var book = createValidBook();

        webTestClient.post().uri(API_BOOKS)
                .bodyValue(book)
                .exchange()
                .expectBody(BookVM.class)
                .value(bookVM -> {
                    assertThat(bookVM.getIsbn()).isGreaterThan(0);

                    var bookInDb = entityManagerFactory.createEntityManager().find(Book.class, bookVM.getIsbn());
                    assertThat(bookInDb.getAuthors().size()).isEqualTo(1);
                    assertThat(bookInDb.getTitle()).isEqualTo(book.getTitle());

                    var authorInDb = entityManagerFactory.createEntityManager().find(Author.class, bookVM.getAuthors().get(0).getId());
                    assertThat(authorInDb.getBooks().size()).isEqualTo(1);
                    assertThat(authorInDb.getName()).isEqualTo(book.getAuthors().get(0).getName());
                });

    }

    private BookVM createValidBook() {
        var book = new BookVM();
        book.setTitle("book-title");

        var author = new AuthorVM();
        author.setName("author-name");

        book.getAuthors().add(author);
        return book;
    }
}

如果您的项目中尚未使用 WebTestClient,请将其添加到您的 POM 中:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
        <scope>test</scope>
    </dependency>

测试正在使用

spring:
  jpa:
    open-in-view: false