Spring Data JPA 投影:从数据库中选择字段

36

我正在测试Spring Data 1.10.4.RELEASE,按照Spring Data文档中的示例http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections进行。

我注意到了一些问题,有两个问题需要解决。

首先,假设我有以下这两个实体:

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
}

@Entity
public class Address {

  @Id @GeneratedValue
  private Long id;
  private String street, state, country;
}
  • 问题1:

对于以下的投影:

interface PersonLimited {  

  String getFirstName(); 

  AddressLimited getAddress();
}

interface AddressLimited {  

  String getCountry(); 
}

当我运行findPersonByFirstNameProjectedForLimitedData

interface PersonRepository extends CrudRepository<Person, Long> {

  @Query("select p from Person p where p.firstName = ?1")
  PersonLimited findPersonByFirstNameProjectedForLimitedData(String firstName);
}

它返回的正是预期的内容:

{
    firstName: 'Homer',
    address: {
        country: 'USA'
    }
}

现在如果我查看生成的 SQL,我得到如下结果:

SELECT person0_.firstName      AS col_0_0_, 
       address1_.id            AS id1_13_, 
       address1_.street        AS street2_13_, 
       address1_.state         AS state3_13_, 
       address1_.country       AS country4_13_
FROM   person person0_ 
       LEFT OUTER JOIN address address1_ 
                    ON person0_.addressId = address1_.id 
WHERE  person0_.firstName = ?  

对于“Person”实体的投影只选择了“fistName”,这是100%正确的,因为在PersonLimited接口中我仅定义了“getFirstName”。

但是对于“Address”实体,它选择了所有字段,这是错误的,因为在AddressLimited接口中我仅定义了“getCountry”,应该只选择“country”。

生成的查询应该类似于:

SELECT person0_.firstName      AS col_0_0_, 
       address1_.country       AS country4_13_
FROM   person person0_ 
       LEFT OUTER JOIN address address1_ 
                    ON person0_.addressId = address1_.id 
WHERE  person0_.firstName = ?  

那么问题是,为什么不仅选择地址“实体”的“国家”字段?为什么需要选择所有字段?这是Spring中的错误吗?

  • 问题2:

对于与上面相同的投影,当我运行findAllPersonsProjectedForLimitedData

interface PersonRepository extends CrudRepository<Person, Long> {

  @Query("select p from Person p")
  List<PersonLimited> findAllPersonsProjectedForLimitedData();
}

它返回了完全符合预期的内容:

[
     {
        firstName: 'Homer',
        address: {
            country: 'USA'
        }
     },
     {
        firstName: 'Maggie',
        address: {
            country: 'USA'
        }
     }
]

现在如果我查看生成的SQL,我会得到以下结果:

SELECT person0_.id                 AS id1_18_, 
       person0_.firstName          AS firstName2_18_, 
       person0_.lastName           AS lastName3_18_, 
       person0_.addressid          AS company4_18_
FROM   person person0_ 

SELECT address0_.id         AS id1_13_0_, 
       address0_.street     AS street2_13_0_, 
       address0_.state      AS state3_13_0_, 
       address0_.country    AS country4_13_0_
FROM   address address0_ 
WHERE  address0_.id = ? 

在这里,Person和Address实体的投影选择了所有字段,这是错误的,它应该只选择"firstName"和"country"。

生成的查询应该类似于:

SELECT person0_.firstName        AS firstName2_18_
FROM   person person0_ 

SELECT address0_.country    AS country4_13_0_
FROM   address address0_ 
WHERE  address0_.id = ? 

这是正常行为吗?难道我们不应该只选择需要的字段吗?

谢谢,


我不太明白你的问题:它正在精确地选择所需内容。 - Jens Schauder
1
@JensSchauder,我编辑了我的问题。 问题在于当使用“Closed Projection”时,就像我正在使用的那样,JPA应该只选择我在投影“接口”中定义的字段。 这就是Spring Data文档所说的: “封闭投影公开了一部分属性,因此它们可以用于优化查询,以减少从数据存储中选择的字段。” - arammal
我会选择标准的投影方式,即“SELECT new com.company.YourDto(p.firstName, p.address.country) FROM Person p”。这种方式在DTO中提供了比接口方式更多的可能性。 - Robert Niestroj
那么这是Spring Data的一个bug吗?如果是,是否已经有人提交了bug报告? - JoeG
15
两年过去了,没有人回答它 :( - cdxf
显示剩余2条评论
2个回答

33

如果你想在Spring Data Projections中使用注解@Query,你需要使用字段别名,并确保将投影字段的项目命名为别名。以下代码应该适用于问题1:

interface PersonRepository extends CrudRepository<Person, Long> {

  @Query("select p.firstName as firstname, p.address as address from Person p where p.firstName = ?1")
  PersonLimited findPersonByFirstNameProjectedForLimitedData(String firstName);
}

另一个可用的选择是使用属性表达式定义查询,只要可能就使用它:

interface PersonRepository extends CrudRepository<Person, Long> {

  List<PersonLimited> findByFirstName(String firstName);
}

6
很遗憾,第一种解决方案不起作用,你需要像这样调用构造函数:"select new org.example.PersonLimited(p.firstName, p.address) from Person p where p.firstName = ?1"。 - bekce
@Andrés-Cuadros-Suárez,实体中是否可以有非强制性字段?使用您的示例@Query("select p.firstName as firstname from Person p where p.firstName = ?1"),如果查询中未提及地址,如何使对象中的地址字段为空? - AbdelRahmane
4
第一个解决方案效果不错。PersonLimited 应该是一个接口(interface)。 - v.ladynev
在我的情况下,我的投影有太多的字段,无法使用@Query中的构造函数手动构建。因此,我最终添加了一个始终为真的谓词。例如,如果所有记录都保证具有非空的id字段,则可以执行PersonLimited findAllByIdNotNull(Pageable pageable)。这可能有点取巧,但仍然比查询包含20个字段要好。 - adarshr
如果我的数据类型是List<Address>,那么该如何编写正确的查询语句呢? - Matley
不幸的是,使用属性表达式的第二种解决方案也不起作用。 - Ratul Sharker

0

我曾经遇到过同样的问题,当我将投影从接口更改为POJO类时,它可以正确生成SQL。


1
你能在你的回答中插入一个例子吗?我遇到了同样的问题。 - rubinhos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接