Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data JPA Specification for a ManyToMany Unidirectional Relationship

Tags:

java

spring

jpa

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.

like image 774
MrQBerrt Avatar asked Aug 05 '15 19:08

MrQBerrt


People also ask

What is unidirectional mapping in JPA?

One-To-One mapping is an association between one persistence object and another one related persistence object. If one persistence object uses other and in back if other is not using the first persistence object then it becomes unidirectional.

What is Spring JPA specification?

Specifications provide us with a way to write reusable queries and also fluent APIs with which we can combine and build more sophisticated queries. All in all, Spring JPA Specifications is a great tool whether we want to create reusable predicates or want to generate typesafe queries programmatically.

Can many to many be unidirectional?

A Course can have many Students and a Student can take many courses. This creates a Many to Many relationship. The following example shows a unidirectional Many to Many mapping in Hibernate.

What is the return type of findById in JPA?

Its findById method retrieves an entity by its id. The return value is Optional<T> . Optional<T> is a container object which may or may not contain a non-null value. If a value is present, isPresent returns true and get returns the value.


1 Answers

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
    ))
like image 184
Ryan Gustafson Avatar answered Oct 20 '22 17:10

Ryan Gustafson