Java中一对多关系中的where子句是如何工作的

imzjd6km  于 2024-01-05  发布在  Java
关注(0)|答案(1)|浏览(199)

我有两张tableA和B。

  1. @Table(name = "table_a")
  2. @Data
  3. public class A {
  4. @Column(name = "id")
  5. private Integer id;
  6. @OneToMany(mappedBy = "a", cascade = CascadeType.ALL)
  7. private List<B> bs;
  8. }
  9. @Table(name = "table_b")
  10. @Data
  11. public class B {
  12. @Column(name = "id")
  13. private Integer b_id;
  14. @Column(name = "a_id)
  15. private Integer a_id;
  16. @Column(name = "status")
  17. private Integer status;
  18. @ManyToOne
  19. @JoinColumn(name = "a_id", insertable = false, updatable = false)
  20. private A a;
  21. }

字符串
现在的问题是,当我尝试运行下面这个查询的JPQL版本时,

  1. select a.* from table_a a
  2. inner join table_b b on a.id = b.a_id
  3. where b.status = -1;


它返回所有行,即使状态不是-1,即它返回table_b中与table a的id相关联的所有行。我知道我使用它不正确,我应该怎么做才能查询table_b中存在的状态

oknrviil

oknrviil1#

我建了一个复制器,它是available at github.com

技术清理

在此期间,我注意到以下几点:
1.实体AB缺少@Entitiy-注解
1.实体AB在主键上缺少@Id注解
1.实体B中的属性a_id是多余的
这导致实体AB的以下重构:
A.java

  1. package de.turing85.spring.join;
  2. import com.fasterxml.jackson.annotation.JsonIgnore;
  3. import jakarta.persistence.CascadeType;
  4. import jakarta.persistence.Column;
  5. import jakarta.persistence.Entity;
  6. import jakarta.persistence.Id;
  7. import jakarta.persistence.OneToMany;
  8. import jakarta.persistence.Table;
  9. import lombok.Data;
  10. import java.util.List;
  11. @Entity
  12. @Table(name = "table_a")
  13. @Data
  14. public class A {
  15. @Id
  16. @Column(name = "id")
  17. private Integer id;
  18. @OneToMany(mappedBy = "a", cascade = CascadeType.ALL)
  19. @JsonIgnore // This is for testing only so Jackson does not run into an endless recursion
  20. private List<B> bs;
  21. }

字符串
B.java

  1. package de.turing85.spring.join;
  2. import jakarta.persistence.Column;
  3. import jakarta.persistence.Entity;
  4. import jakarta.persistence.Id;
  5. import jakarta.persistence.JoinColumn;
  6. import jakarta.persistence.ManyToOne;
  7. import jakarta.persistence.Table;
  8. import lombok.Data;
  9. @Entity
  10. @Table(name = "table_b")
  11. @Data
  12. public class B {
  13. @Id
  14. @Column(name = "id")
  15. private Integer id;
  16. @Column(name = "status")
  17. Integer status;
  18. @ManyToOne
  19. @JoinColumn(name = "a_id", insertable = false, updatable = false)
  20. A a;
  21. }

查询构造

有了这些变化,我们就可以解决实际问题了。JPQL(jakarta.ee)中join的语法如下:

  1. SELECT <root-alias-name> FROM <root-type-name> <root-alias-name>
  2. INNER JOIN <root-alias-name>.<field-to-join-on> <join-alias-name>
  3. WHERE ...


翻译成我们的例子,这导致

  1. SELECT a FROM A a
  2. INNER JOIN a.bs b
  3. WHERE ...


注意,我们写了a.bs b。我们访问一个集合,但将其命名为singular。从查询的Angular 来看,该集合对我们来说是透明的,我们只能看到单数B-实体(想象查询是一个循环,对bs中的每个B执行)。
有了这些知识,我们可以用公式表示WHERE条件,然后简单明了:

  1. SELECT a FROM A a
  2. INNER JOIN a.bs b
  3. WHERE b.status = -1


我们现在可以在ARepository中定义这个查询:

  1. package de.turing85.spring.join;
  2. import org.springframework.data.jpa.repository.JpaRepository;
  3. import org.springframework.data.jpa.repository.Query;
  4. import java.util.List;
  5. public interface ARepository extends JpaRepository<A, Integer> {
  6. @Query("""
  7. SELECT a FROM A a
  8. INNER JOIN a.bs b
  9. WHERE b.status = -1
  10. """)
  11. List<A> findByBStatusMinusOne();
  12. }

测试

使用Controller.java访问实体:

  1. package de.turing85.spring.join;
  2. import lombok.RequiredArgsConstructor;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.List;
  7. @RestController
  8. @RequestMapping(Controller.PATH)
  9. @RequiredArgsConstructor
  10. public class Controller {
  11. public static final String PATH = "as";
  12. private final ARepository repository;
  13. @GetMapping
  14. List<A> getAllAs() {
  15. return repository.findAll();
  16. }
  17. @GetMapping(path = "bMinusOne")
  18. List<A> getAllAsWithBMinusOne() {
  19. return repository.findByBStatusMinusOne();
  20. }
  21. }


一些测试数据:

  1. DELETE FROM table_b;
  2. DELETE FROM table_a;
  3. INSERT INTO table_a(id)
  4. VALUES
  5. (0),
  6. (1),
  7. (2);
  8. INSERT INTO table_b(id, status, a_id)
  9. VALUES
  10. (0, -1, 0),
  11. (1, 0, 1),
  12. (2, -1, 1),
  13. (3, 0, 2);


我们现在可以运行./mvnw clean package来运行集成测试:
ControllerTest.java

  1. package de.turing85.spring.join;
  2. import io.restassured.RestAssured;
  3. import org.junit.jupiter.api.DisplayName;
  4. import org.junit.jupiter.api.Test;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import org.springframework.boot.test.web.server.LocalServerPort;
  7. import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.test.context.jdbc.Sql;
  10. import org.testcontainers.containers.PostgreSQLContainer;
  11. import org.testcontainers.junit.jupiter.Container;
  12. import org.testcontainers.junit.jupiter.Testcontainers;
  13. import static org.hamcrest.Matchers.*;
  14. @Testcontainers
  15. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  16. @DisplayName("Controller Tests")
  17. @Sql({ "/data.sql" })
  18. class ControllerTest {
  19. @LocalServerPort
  20. private Integer port;
  21. @Container
  22. @ServiceConnection
  23. static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:16.1-alpine3.19");
  24. @Test
  25. @DisplayName("Get All A's")
  26. void getAll() {
  27. // @formatter: off
  28. RestAssured
  29. .when().get("http://localhost:%d/%s".formatted(port, Controller.PATH))
  30. .then().assertThat()
  31. .statusCode(HttpStatus.OK.value())
  32. .body("size()", is(3))
  33. .body("collect { it.id }", hasItems(0, 1, 2));
  34. // @formatter: on
  35. }
  36. @Test
  37. @DisplayName("Get All A's with at least one B in status -1")
  38. void getBMinusOne() {
  39. // @formatter: off
  40. RestAssured
  41. .when().get("http://localhost:%d/%s/bMinusOne".formatted(port, Controller.PATH))
  42. .then().assertThat()
  43. .statusCode(HttpStatus.OK.value())
  44. .body("size()", is(2))
  45. .body("collect { it.id }", hasItems(0, 1));
  46. // @formatter: on
  47. }
  48. }


注意,集成测试使用了testcontainers,因此需要一个与docker或docker兼容的环境来执行。

备注

我将database-entity 1:1传播到rest-endpoint。这样做是为了简洁。特别注意,我必须在A@JsonIgnorebs。如果我们不这样做,Jackson将由于双向关系而陷入无限递归。对于生产级解决方案,我建议隔离数据库和响应实体。

展开查看全部

相关问题