1

How can I write a JPA repository method with property expressions that check for the existence of multiple items, or properties on those items, in a list? I can lookup a single item in the list, see zip code below, but I'm trying to write a way to check for multiple zip codes, where each Person in the result set has both zip codes in their list of addresses.

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
    // Works
    Set<Person> findAllByAddresses_ZipCode(String zip);

    // Doesn't work - does not return expected results
    Set<Person> findAllByAddresses_ZipCode_And_Addresses_ZipCode(String zip1, String zip2);
}

My current hack is to fetch two sets for 2 zip codes, then find the intersection of the two sets:

public @ResponseBody
    Iterable<Person> personsWithBothZipCodes(@PathVariable("zip1") String zip1,
                                             @PathVariable("zip2") String zip2) {

    Set<Person> a = personRepository.findAllByAddresses_ZipCode(zip1);
    Set<Person> b = personRepository.findAllByAddresses_ZipCode(zip2);

    // Only return results that contain both zip1 and zip2.
    a.retainAll(b);

    return a;
}

The entity looks like this:

@Entity
public class Person
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    // zipcode is a String property on an Address.
    @OneToMany(targetEntity = com.data.Address.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<Address> addresses = new ArrayList<Address>();
    ...
}

Is there a way to write this as part of the method header? Relevant docs

Gaʀʀʏ
  • 4,372
  • 3
  • 39
  • 59
  • Can you show your entity? – Krzysztof Atłasik Feb 13 '18 at 20:11
  • Would you like to solve this with `spring-data` magic at all costs? I don't see its benefit over an old-school query. – Balázs Németh Feb 13 '18 at 20:21
  • @RandallFlagg Added a snippet from the entity. – Gaʀʀʏ Feb 13 '18 at 20:22
  • @BalázsMáriaNémeth If at all possible. I am trying to solve for nth number of variables to check against. My repositories and controllers are getting auto-generated using Javapoet. Trying to solve with as little code as I can. – Gaʀʀʏ Feb 13 '18 at 20:25
  • @Gaʀʀʏ if it is a pet project, then fine but otherwise I cannot see how it remains maintainable if it's so hard to find the solution for this trivial problem. – Balázs Németh Feb 13 '18 at 20:27
  • have you considered using @Query annotation inside the interface PersonRepository ? – nabeel Feb 13 '18 at 20:28
  • 1
    I think this should help: https://stackoverflow.com/questions/47527594/spring-jparepository-findby-incollection-returns-union-not-intersection – Vasan Feb 13 '18 at 20:30
  • @nabeel Using @Query would work. I previously solved it with `nativeQuery=true`, but I'm looking for a solution via keywords in the method name. – Gaʀʀʏ Feb 13 '18 at 20:31
  • 1
    clear, but @Query lets use HQL, you don't need to use native query – nabeel Feb 13 '18 at 20:33
  • I believe the method should be like this `personRepository.findAllByAddresses_ZipCodeInAndAddresses_ZipCodeIn(zipSet1, zipSet2)` – pvpkiran Feb 13 '18 at 20:57
  • @pvpkiran It looks good on paper, but after examining the generated query, it is doing the check on the same object: `... where (address2_.zipcode in (?)) and (address2_.zipcode in (?))`. It is doing a left join on Address. I don't think it is possible with keywords. Query might be the way to go here, as nabeel suggested. – Gaʀʀʏ Feb 13 '18 at 21:22
  • 1
    ya you are right, i just tested that. Check my answer here https://stackoverflow.com/questions/48424575/how-to-search-through-array-in-spring-boot-crudrepository/48425205#48425205. I think you can extend that – pvpkiran Feb 13 '18 at 22:02
  • Not as nice as I'd like, but I managed to solve it with @Query by comparing the size of matching zip codes: `@Query("select p from Person p where :size = (select count(addr.zip) from p.addresses addr where addr.zipcode in :zip)")` – Gaʀʀʏ Feb 13 '18 at 22:38

2 Answers2

3

Yes just add the word In to your query

Set<Person> findAllByAddresses_ZipCodeIn(Set<String> zip);

Then in your controller you can do something like:

public @ResponseBody Iterable<Person> personsWithBothZipCodes(@PathVariable("zip1") String zip1, @PathVariable("zip2") String zip2) {

    Set<String> zipSet = new HashSet<>();
    zipSet.add(zip1);
    zipSet.add(zip2);

    Set<Person> a = personRepository.findAllByAddresses_ZipCodeIn(zipSet);

    return a;
}

Dont know if this will work but can try

Set<Person> findAllByAddresses_ZipCodeInAndZipCodeIn(Set<String> zip1, Set<String> zip2);

public @ResponseBody Iterable<Person> personsWithBothZipCodes(@PathVariable("zip1") String zip1, @PathVariable("zip2") String zip2) {

    Set<String> zipSet1 = new HashSet<>();
    zipSet1.add(zip1);

    Set<String> zipSet2 = new HashSet<>();
    zipSet2.add(zip2);

    Set<Person> a = personRepository.findAllByAddresses_ZipCodeInAndZipCodeIn(zipSet1, zipSet2);

    return a;
}
locus2k
  • 2,802
  • 1
  • 14
  • 21
  • This doesn't quite solve my issue. I want to fetch Persons that have all of the given zips, not just one in the list, hence my intersection set in the controller. Clarified the question. – Gaʀʀʏ Feb 13 '18 at 20:03
  • See my edit. I dont know if itll work but can trying addin another in so it would be `InAndIn` – locus2k Feb 13 '18 at 20:17
  • `findAllByAddresses_ZipCodeInAndIn` doesn't quite work, I think the closest to that is `findAllByAddresses_ZipCodeInAndZipCodeIn`, but it is asserting the same address zip code is in both input parameters. Not quite the solution I need. I think I may have to stick with my initial hack, but thank you for sharing your ideas. I was unaware of the "in" keyword. – Gaʀʀʏ Feb 13 '18 at 20:27
  • Maybe `Person` needs to be joined to `Address` twice. So that a result set returns tuples like person, address1.zip, address2.zip. That might be tricky to solve with spring magic. – Balázs Németh Feb 13 '18 at 20:33
  • @Gaʀʀʏ double check you are setting your sets right. It shouldn't be inserting the same zip code if you have two distinct sets past in. See my edit. – locus2k Feb 13 '18 at 20:54
1

Maybe you can use JPQL query (as suggested in comments)?

@Query("from Person as person" +
" join person.addresses as address1 with address1.zipCode = ?1" +
" join person.addresses as address2 with address2.zipCode = ?2")
Set<Person> findByZipCodes(String zipCode1, String zipCode2);

Haven't really reproduced your case, but it should probably work.

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
  • 1
    This would work. I didn't mention in the question, but I was trying to achieve a solution for an unlimited number of zip codes. Here's the final solution I went with: `@Query("select p from Person p where :size = (select count(addr.zip) from p.addresses addr where addr.zipcode in :zip)")`, where `:size` represents the size of the List of zip codes `:zip`. – Gaʀʀʏ Feb 14 '18 at 16:25