36

I was testing Spring Data 1.10.4.RELEASE, following the example in Spring Data Docs http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

and I noticed some issues for which I have 2 questions.

First let's suppose I have these 2 entities:

@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;
}
  • Question 1:

for the following projections:

interface PersonLimited {  

  String getFirstName(); 

  AddressLimited getAddress();
}

interface AddressLimited {  

  String getCountry(); 
}

when I run findPersonByFirstNameProjectedForLimitedData

interface PersonRepository extends CrudRepository<Person, Long> {

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

it returns exactly what expected :

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

now if I look into the generated SQL, this is what I have:

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 = ?  

The projection for the "Person" entity is selecting only "fistName", which is 100% correct because in the PersonLimited interface I've only defined "getFirstName".

But for the "Address" entity, it selects all the fields, which is wrong because in the AddressLimited interface I've only defined "getCountry", It should only select "country".

The generated query should be something like:

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 = ?  

so the question is, why it is not selecting only the "country" field for the Address "entity"? why it needs to select all the fields? is it a bug in Spring?

  • Question 2:

for the same projection as above,

when I run findAllPersonsProjectedForLimitedData

interface PersonRepository extends CrudRepository<Person, Long> {

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

it returns exactly what expected :

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

now if I look into the generated SQL, this is what I have:

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 = ? 

here, the projection for both the Person and the Address entities is selecting all the fields which is wrong, it should only select "firstName" and "country".

The generated query should be something like:

SELECT person0_.firstName        AS firstName2_18_
FROM   person person0_ 

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

is this the normal behavior, shouldn't select only the fields that we need?

Thanks,

Martin Schröder
  • 4,176
  • 7
  • 47
  • 81
arammal
  • 393
  • 1
  • 4
  • 11
  • I don't understand your questions: it is selecting exactly what it need. – Jens Schauder Oct 23 '16 at 11:37
  • 1
    @JensSchauder, I edited my questions. the thing is when using "Closed Projection", like the one I am using, JPA should only select the fields that I have defined in my Projection "interfaces". this is what the Spring Data Doc says: _Closed projections expose a subset of properties hence they can be used to optimize the query in a way to reduce the selected fields from the data store_ – arammal Oct 23 '16 at 14:56
  • ok, now I understand your question. Very interesting. – Jens Schauder Oct 23 '16 at 18:14
  • I would go with the standard way od doing projections meaning "SELECT new com.company.YourDto(p.firstName, p.address.country) FROM Person p". It gives you more possibilities in your DTO than in this interface approach. – Robert Niestroj Oct 24 '16 at 12:57
  • So is this a bug in Spring Data? If so, has one been entered? – JoeG Apr 18 '17 at 11:50
  • how do you map interface to json without implementation? – minizibi Mar 01 '18 at 20:24
  • 15
    2 years after and no one answers it :( – cdxf Jul 07 '18 at 23:55

2 Answers2

33

If you want use the annotation @Query with Spring Data Projections you have to use field alias and you need to make sure you alias the projects matching the projection fields. The following code should work for question 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);
}

Another alternative that you can use is define your queries with Property Expressions. whenever is possible:

interface PersonRepository extends CrudRepository<Person, Long> {

  List<PersonLimited> findByFirstName(String firstName);
}
Andrés Cuadros
  • 1,962
  • 1
  • 21
  • 22
  • 6
    First solution does not work unfortunately, you have to call the constructor like: `"select new org.example.PersonLimited(p.firstName, p.address) from Person p where p.firstName = ?1"`. – bekce Jun 07 '18 at 14:46
  • @Andrés-Cuadros-Suárez Is it possible to have a non mandatory field in the Entity. Using your example, @Query("select p.firstName as firstname from Person p where p.firstName = ?1"), how can I have, if the address is not mentioned in the query, the address field in the object should be null ? – AbdelRahmane Sep 22 '19 at 19:01
  • 4
    @bekce The first solution works well. `PersonLimited` should be an interface. – v.ladynev Aug 23 '20 at 21:10
  • In my case, my projection had too many fields to construct manually using a constructor in the `@Query`. So I ended up appending a predicate that will always be true. For example, if all records are guaranteed to have a non-null `id` field, you can do `PersonLimited findAllByIdNotNull(Pageable pageable)`. It's arguably hacky but still better than having a query comprising of 20 fields. – adarshr Jun 04 '21 at 12:18
  • And what if I have List
    ??? How to write correct query in this case?
    – Matley Aug 02 '21 at 09:59
  • Unfortunately second solution using Property Expression does not work too. – Ratul Sharker Feb 02 '22 at 11:07
0

I had the same issue and when I changed projections from interfaces to pojo classes it generated SQL properly.

Admir Sabanovic
  • 645
  • 1
  • 11
  • 18