如何在 Spring JPA 存储库中加入多个表的结果

Posted

技术标签:

【中文标题】如何在 Spring JPA 存储库中加入多个表的结果【英文标题】:How to join results of multiple tables in Spring JPA repository 【发布时间】:2019-06-27 16:28:43 【问题描述】:

我是 Spring 新手,不知道如何连接多个表以返回一些结果。我尝试实现一个小型库应用程序,如下所示。

我的实体类 - Book、Customer、Bookings


Book.java - 图书馆中可用的书籍

@Entity
@Table(name = "books")
public class Book 

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

    @NotNull(message = "Book name cannot be null")
    @Column(name = "book_name", columnDefinition = "VARCHAR(255)")
    private String bookName;

    @Column(name = "author", columnDefinition = "VARCHAR(255)")
    private String author;

    // getters and setters

    public Book() 

    public Book(String bookName, String author) 
        this.bookName = bookName;
        this.author = author;
    

Customer.java - 在库中注册的客户

@Entity
@Table(name = "customer", uniqueConstraints = @UniqueConstraint(columnNames = "phone"))
public class Customer 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", columnDefinition = "int")
    private int id;

    @NotNull(message = "Customer name cannot be null")
    @Column(name = "name", columnDefinition = "VARCHAR(255)")
    private String name;

    @Column(name = "phone", columnDefinition = "VARCHAR(15)")
    private String phone;

    @Column(name = "registered", columnDefinition = "DATETIME")
    private String registered;

    // getters and setters

    public Customer() 

    public Customer(String name, String phone, String registered) 
        this.name = name;
        this.phone = phone;
        this.registered = registered;
    

Booking.java - 客户进行的所有预订

@Entity
@Table(name = "bookings")
public class Booking 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", columnDefinition = "int")
    private int id;

    @NotNull(message = "Book id cannot be null")
    @Column(name = "book_id", columnDefinition = "int")
    private int bookId;

    @NotNull(message = "Customer id cannot be null")
    @Column(name = "customer_id", columnDefinition = "int")
    private int customerId;

    @Column(name = "issue_date", columnDefinition = "DATETIME")
    private String issueDate;

    @Column(name = "return_date", columnDefinition = "DATETIME")
    private String returnDate;

    // getters and setters

    public Booking() 

    public Booking(int bookId, int customerId, String issueDate) 
        this.bookId = bookId;
        this.customerId = customerId;
        this.issueDate = issueDate;
    

现在各个实体的表架构如下:

图书: +-----------+-------------+------+-----+--------- +----------------+ |领域 |类型 |空 |钥匙 |默认 |额外 | +-----------+-------------+------+-----+--------- +----------------+ |编号 |整数(11) |否 |优先级 |空 |自动增量 | |书名 | varchar(255) |否 | |空 | | |作者 | varchar(255) |是 | |空 | | +-----------+-------------+------+-----+--------- +----------------+ id - 主键 顾客: +------------+--------------+------+-----+-------- -----------+-------------------+ |领域 |类型 |空 |钥匙 |默认 |额外 | +------------+--------------+------+-----+-------- -----------+-------------------+ |编号 |整数(11) |否 |优先级 |空 |自动增量 | |姓名 | varchar(255) |否 | |空 | | |已注册 |日期时间 |是 | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | |电话 | varchar(15) |是 |统一 |空 | | +------------+--------------+------+-----+-------- -----------+-------------------+ id - 主键 预订: +-------------+----------+------+-----+----------- --------+--------------------+ |领域 |类型 |空 |钥匙 |默认 |额外 | +-------------+----------+------+-----+----------- --------+--------------------+ |编号 |整数(11) |否 |优先级 |空 |自动增量 | | book_id |整数(11) |否 |穆尔 |空 | | |客户 ID |整数(11) |否 |穆尔 |空 | | |问题日期 |日期时间 |是 | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | |返回日期 |日期时间 |是 | |空 | | +-------------+----------+------+-----+----------- --------+--------------------+ id - 主键 book_id - 外键参考书目.id customer_id - 外键引用 customer.id

现在我要做的是给出一些预订条件,例如客户电话或作者姓名等,我想退回与该订单相关的所有预订。我将展示一个示例 Booking api 来解释。

预订控制器:

@RestController
@RequestMapping("/bookings")
public class BookingController 
    @Autowired
    BookingService bookingService;

    // some booking apis which return Booking objects

    @GetMapping
    public List<Booking> getAllBookingsBy(@RequestParam("phone") String phone,
                                         @RequestParam("authors") List<String> authors) 
        return bookingService.getAllBy(phone, authors);
    

    @PostMapping
    public Booking addBooking(@RequestBody Booking booking) 
        bookingService.saveBooking(booking);
        return booking;
    

预订服务类:

@Service
public class BookingService 
    @Autowired
    private BookingRepository bookingRepository;

    // some booking service methods

    // get all bookings booked by a customer with matching phone number and books written by a given list of authors
    public List<Booking> getAllBy(String phone, List<String> authors) 
    return bookingRepository.queryBy(phone, authors);
    

    public void saveBooking(Booking booking) 
        bookingRepository.save(booking);
    

预订存储库类:

@Repository
public interface BookingRepository extends JpaRepository<Booking, Integer> 
    // some booking repository methods

    @Query(value = "SELECT * FROM bookings bs WHERE " +
            "EXISTS (SELECT 1 FROM customer c WHERE bs.customer_id = c.id AND c.phone = :phone) " +
            "AND EXISTS (SELECT 1 FROM books b WHERE b.id = bs.book_id AND b.author IN :authors)",
            nativeQuery = true)
    List<Booking> queryBy(@Param("phone") String phone,
                            @Param("authors") List<String> authors);

现在点击显示的预订控制器将返回一个预订对象,如下所示:

[
    
        "id": 3,
        "book_id": 5,
        "customer_id": 2,
        "issue_date": "2019-02-04 01:45:21",
        "return_date": null
    
]

但我不希望这样,我想连同他们一起返回该预订的客户姓名以及书名。所以我希望控制器返回的预订对象如下所示:

[
    
        "id": 3,
        "book_id": 5,
        "customer_id": 2,
        "issue_date": "2019-02-04 01:45:21",
        "return_date": null,
        "customer_name": "Cust 2",
        "book_name": "Book_2_2",
    
]

有人可以帮忙吗?我被卡住了,因为我无法从这里继续。

################### 编辑: 我在 Booking 类中添加了这些单向的一对一关联:

@OneToOne
@JoinColumn(name = "book_id", insertable = false, updatable = false)
private Book book;

@OneToOne
@JoinColumn(name = "customer_id", insertable = false, updatable = false)
private Customer customer;

但是现在当我点击我的控制器时,我会在我的 Booking 对象中获得整个 Book 和 Customer 对象。那么我该怎么做才能在预订对象中返回书名和客户姓名?这是我返回的 Booking 对象现在的样子:

[
    
        "id": 3,
        "book_id": 5,
        "book": 
            "id": 5,
            "book_name": "Book_2_2",
            "author": "author_2"
        ,
        "customer_id": 2,
        "customer": 
            "id": 2,
            "name": "Cust 2",
            "phone": "98765431",
            "registered": "2019-02-04 01:13:16"
        ,
        "issue_date": "2019-02-04 01:45:21",
        "return_date": null
    
]

现在我的预订控制器中的 save() api 无法正常工作,因为当我向其发送 Booking 类型的对象时,bookId 和 customerId 不知何故变成了 0,这在我之前没有发生添加了这些更改。

【问题讨论】:

我认为问题在于如何在不创建新实体的情况下获得各种结果集。应该只是存储库级别的更改吗? 【参考方案1】:

你做错了。您正在返回 Booking,并且您希望它神奇地反序列化为一个包含诸如 Book Name 之类的连接信息的实体。但是在您对存储库的选择查询中,您选择了预订。在您的实施过程中,Booking 不包含有关图书的信息。

首先,您需要将反序列化为 JSON 的内容和将用作 Spring 数据的持久层的内容分开。

    从 Booking 到 Book 建立 @OneToOne/@OneToMany 关系作为开始。 更改您的查询以对您已映射为 Book 的实体/集合进行预取。 创建一个 POJO 并使用 JSON 注释以您希望控制器返回的方式对其进行注释。 在您的持久性对象/预订与 Book 上的隐藏集合和您新创建的 POJO 之间映射

实际上,如果您映射为 OneToOne,则默认初始化将变为 EAGER,因此您的查询变得有点不必要。

如果我们假设您的映射位于持久层中,您的查询将如下所示:

@Query(value = "SELECT * FROM bookings bs WHERE " +
            "bs.customer.phone = :phone) " +
            "AND  bs.book.author IN :authors)")

这是来自 Hibernate 的映射文档> http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#associations

【讨论】:

我不希望它“神奇地反序列化”到另一个实体,我知道它会返回 Booking 对象。我只想知道除了预订对象之外,我可以添加哪些更改来获取客户姓名和书名。 阅读上面 :) 解释问你是否不懂某些东西。 @user3248186 您的主要错误是您没有使用 Hibernate 来实现它的本质 - 对象关系映射。使用适当的注释映射所有关系。 您能否添加任何资源,我可以从这些资源中了解更多信息? 添加了官方文档的链接。【参考方案2】:

您的查询并不是连接表的最佳方式。 更直观的方式是这样的

SELECT * FROM bookings
WHERE customer_id in (SELECT id FROM customer WHERE phone = :phone)
 AND book_id in (SELECT id FROM books WHERE author IN :authors)

【讨论】:

他在问如何获得最终的 json 表示。他既没有正确的 json 映射,也没有正确的休眠映射。 sql查询是他在代码中的最后一个问题。【参考方案3】:

您可以按照以下步骤实现它。

    使用 getter 为您需要响应的所有字段创建一个新接口。 在@Query 内的查询字符串中,您需要在选择中为您的列提供名称。注意:这些名称需要与您在界面中创建的 getter 保持同步。 将此接口用作存储库方法的返回类型。

有关更多信息,您可以参考 Spring data rest 中的 Projections。 https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

【讨论】:

以上是关于如何在 Spring JPA 存储库中加入多个表的结果的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Boot JPA 中加入两个表,我的代码出错了

如何在jpa中加入具有多个主键或特定主键的列

管理来自多个表的数据 Spring data JPA

Spring Data JPA 中的多个基础存储库

如何在 R2DBC 和 Spring WebFlux 中加入多个表?

在 JPA 存储库(Spring Data Jpa)中执行自定义查询