Spring Data JPA Specification for a ManyToMany Unidirectional Relationship

匿名 (未验证) 提交于 2019-12-03 07:50:05

问题:

I have a setup where cats can be owned by many owners, and each owner can own several cats. Given this, I would like to write a specification to help me find all cats with a given owner name.

Here is a simple class setup.

@Entity public class Cat extends AbstractEntity {   @Column   private String name; } 

* No getters/setters for conciseness. Id field is in the superclass.

@Entity public class Owner extends AbstractEntity {   @Column   private String name;    @ManyToMany(fetch = FetchType.LAZY)   @JoinTable(name = "OWNER_2_CATS",          joinColumns = @JoinColumn(name = "OWNER_ID"),         inverseJoinColumns = @JoinColumn(name = "CAT_ID"))   @OrderColumn(name = "order_column")   private List<Cat> cats = Lists.newArrayList(); } 

* No getters/setters for conciseness. Id field is in the superclass.

And here is a repository with a query that does work and a spec that does not work.

public interface CatRepository extends AtomicsRepository<Cat, Long> {    // This query works.   @Query("SELECT c FROM Owner o INNER JOIN o.cats c WHERE o.name = ?")   List<Cat> findAllByOwner(String ownerName);    // But how do I accomplish this in a specification?   public static class Specs {     static Specification<Cat> hasOwnerName(final String ownerName) {       return (root, query, cb) -> {         // These next lines don't work! What do I put here?         Root<Owner> owner = query.from(Owner.class);         owner.join("cats");         return cb.equal(owner.get("name"), ownerName);       };     }   } } 

Please help me write the specification.

What I'm having trouble with is that this relationship seems to need to go against the grain of how the relationship is expressed: an owner has a cats list, but a cat does not have an owners list.

回答1:

Overview

The trickiness with this Spec is that you are querying the Cat with no direct relationship to Owner.

The general idea is to:

  • Get a hold of the Owner.
  • Relate the Owner with the Cat using Owner.cats relation membership. We can do this using just the entities, and like JPA handle the entity @Id correlation for us.
  • Mark the query on Cat as distinct. Why? Because this is a @ManyToMany relationship and there may be multiple matching Owner for any given Cat depending on the Owner criteria used.

Approach 1 - subquery

My preferred approach would be to use a subquery to introduce the Owner:

// Subquery using Cat membership in the Owner.cats relation public static class Specs {     static Specification<Cat> hasOwnerName(final String ownerName) {         return (root, query, cb) -> {             query.distinct(true);             Root<Cat> cat = root;             Subquery<Owner> ownerSubQuery = query.subquery(Owner.class);             Root<Owner> owner = ownerSubQuery.from(Owner.class);             Expression<Collection<Cat>> ownerCats = owner.get("cats");             ownerSubQuery.select(owner);             ownerSubQuery.where(cb.equal(owner.get("name"), ownerName), cb.isMember(cat, ownerCats));             return cb.exists(ownerSubQuery);         };     } } 

Which Hibernate 4.3.x generates SQL query like:

select cat0_.id as id1_1_ from cat cat0_ where      exists (         select owner1_.id from owner owner1_ where             owner1_.name=?             and (cat0_.id in (                 select cats2_.cat_id from owner_2_cats cats2_ where owner1_.id=cats2_.owner_id             ))     ) 

Approach 2 - cartesian product

An alternative is to use a cartesian product to introduce the Owner:

// Cat membership in the Owner.cats relation using cartesian product public static class Specs {     static Specification<Cat> hasOwnerName(final String ownerName) {         return (root, query, cb) -> {             query.distinct(true);             Root<Cat> cat = root;             Root<Owner> owner = query.from(Owner.class);             Expression<Collection<Cat>> ownerCats = owner.get("cats");             return cb.and(cb.equal(owner.get("name"), ownerName), cb.isMember(cat, ownerCats));         };     } } 

Which Hibernate 4.3.x generates SQL query like:

select cat0_.id as id1_1_ from cat cat0_ cross join owner owner1_ where     owner1_.name=?     and (cat0_.id in (         select cats2_.cat_id from owner_2_cats cats2_ where owner1_.id=cats2_.owner_id     )) 


回答2:

You could make a IN predicate getting the cat's id values with a sub-query.

public static class Specs {   static Specification<Cat> hasOwnerName(final String ownerName) {     return (root, query, cb) -> {       //EntityType<Cat> Cat_ = root.getModel();       final Subquery<Long> queryOwner = query.subquery(Long.class);// Check type of the Cat's ID attribute       final Root<Owner> aliasOwner = queryOwner.from(Owner.class);       //EntityType<Owner> Owner_ = aliasOwner.getModel();       //final Join<Owner, Cat> aliasCatsOwner = aliasOwner.join(Owner_.cats);       final Join<Owner, Cat> aliasCatsOwner = aliasOwner.join("cats");       //queryOwner.select(aliasCatsOwner.<Long> get(Cat_.id)));       queryOwner.select(aliasCatsOwner.<Long> get("id")));// Check type and name of the Cat's ID attribute       queryOwner.where(cb.equal(Owner.<String> get("name"), ownerName));       //return cb.in(root.get(Cat_.id).value(queryOwner);       return cb.in(root.get("id").value(queryOwner);//check the name of ID attribute!     };   } } 


回答3:

The short answer is in this way:

 Page<Cat> findByOwner_Name(String ownerName, Pageable p); 

But the difficulty is how to query those cats NOT owned by anyone, which equals

select * from cat where cat.id NOT IN (select cat.id from owner_has_cat); 

Does anyone have idea please?



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!