简体   繁体   中英

JPA Criteria API Specification for Many to Many

I have got three classes as mentioned below. I am trying to create a specification to filter data where there is a match in the linked table.

public class Album {
    private Long id;
    private List<AlbumTag> albumTags;
}

public class Tag {
    private Long id;
    private String category;
}

public class AlbumTag{
    private Long id;
    private Album album;
    private Tag tag;
}

In the schema given above what I am trying to find is a list of all albums from Album table with the link in AlbumTag. The SQL that I want to achieve, doesn't have to be same, is below

select *
from Album A 
where (A.Id in (select [AT].AlbumId 
from AlbumTag [AT]))

What I have tried so far which is not working, of course, is below

public class AlbumWithTagSpecification implements Specification<Album> {

    @Override
    public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {

         final Subquery<Long> personQuery = cq.subquery(Long.class); 
         final Root<Album> album = personQuery.from(Album.class); 
         final Join<Album, AlbumTag> albumTags = album.join("albumTags");
         personQuery.select((albumTags.get("album")).get("id"));
         personQuery.where(cb.equal(album.get("id"), (albumTags.get("album")).get("id"))); 
         return cb.in(root.get("id")).value(personQuery);

    }
}

Using spring boot and spring data JPA, you can prefer entity relationship to fetch the data.

1.Annotate the domain class with the entity relationship which given below:

@Entity
@Table(name="Album")
public class Album {
    @Id
    @Column(name="id")
    private Long id;
    @OneToMany(targetEntity = AlbumTag.class, mappedBy = "album")
    private List<AlbumTag> albumTags;

    //getter and setter
}

@Entity
@Table(name="Tag")
public class Tag {
    @Id
    @Column(name="id")
    private Long id;
    @Column(name="category")
    private String category;

    //getter and setter
}

@Entity
@Table(name="AlbumTag")
public class AlbumTag{
    @Id
    @Column(name="id")
    private Long id;
    @ManyToOne(optional = false, targetEntity = Album.class)
    @JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
    private Album album;
    @ManyToOne(optional = false, targetEntity = Tag.class)
    @JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
    private Tag tag;

    //getter and setter
}

2.use the spring data to fetch the details using the below:

Album album = ablumRepository.findOne(1); // get the complete details about individual album.
List<AlbumTag> albumTags = ablum.getAlbumTags(); // get the all related albumTags details for particular album.

I hope this will help you to solve it.

Subqueries in JPA only really work with CriteriaBuilder.exists() so i would try:

public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {

     final Subquery<Long> subQuery = cq.subquery(Long.class); 
     final Root<AlbumTag> albumTag = subQuery.from(AlbumTag.class); 
     // it doesn't really matter what we select
     subQuery.select(cb.literal(1));
     subQuery.where(cb.equal(root.get("id"), (albumTag.get("album")).get("id"))); 

     return cb.exists(subQuery);

}

which is equivalent to

select *
from Album A 
where exists(
    select 1 from AlbumTag AT 
    where AT.AlbumId = A.Id
)
creteria query for join tables

CriteriaQuery<Album> query = cb.createQuery(Album.class);
Root<Album> album = query.from(Teacher.class);
Join<Album, AlbumTag> tag = teacher.join("id");
query.select(tag).where(cb.equal(album.get("album")));

List<Album> results = em.createQuery(query).getResultList();
for (Album al : results) {
    System.out.println("album-->+al.get(name));
}

Well, I wouldn't go for in operation in this case - it just complicates the query and the specification. The problem you described is actually matter of joining records from Table A with related records from Table B so the query in your case would be like:

SELECT a from Album a join AlbumTag at on a.id = at.albumId - as you needed it will return all albums that have album tags. Inner join explained

So in your case I would create this "factory" method that would create for you this specification.

public static Specification<Album> withTags() {
    return new Specification<Album>() {
        @Override
        public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            return root.join("albumTags").getOn();
    }
};

}

Also I would suggest you to have a look at static metamodel library from hibernate - link to introduction . It generates for you static model from your entity classes that helps you avoid creating queries/specifications using hardcoded strings.

This looks like a classic many to many example. The three classes you have map directly to the tables you would expect in the database. JPA is an Object Relational Mapping (ORM) library which means we can structure the classes in a more OO style and map to the underlying relational database.

The AlbumTag class can be omitted and the @ManyToMany relationship added to both Album and Tag .

public class Album {
    private Long id;

    @ManyToMany
    @JoinTable(name="AlbumTag",
        joinColumns=
            @JoinColumn(name="album", referencedColumnName="id"),
        inverseJoinColumns=
            @JoinColumn(name="tag", referencedColumnName="id"))
    private List<Tag> tags;
}

public class Tag {
    private Long id;
    private String category;

    @ManyToMany(mappedBy="tags")
    private List<Album> albums;
}

To find albums by Tag you would first retrieve the Tag from the repository using something like findById(1l); or findByCategory("Rock"); and then simply call getAlbums() on the Tag object.

Note: One slight difference here is that the AlbumTag table would have only two columns (album and tag). The extra id column on AlbumTag is unnecessary since the combination of album and tag would be a unique id and you would never need to find by id in this table anyway.

Since you are using spring-data-jpa you should really take advantage of the features it provides.

My first question is related to your entity classes. I do not understand why is it necesary to store a list of album tags in the album class. Since you have a join table this information is reduntant.

Secondly you should adnotate your entity clases:

@Entity
public class Album {
@Id
@Column
private Long id;
}

@Entity
public class Tag {
  @Id
  @Column
  private Long id;
  @Column
  private String category;
}

@Entity
@Table
public class AlbumTag{
  @Id
  @Column
  private Long id;
  @ManyToOne
  @JoinColumn
  private Album album;
  @ManyToOne
  @JoinColumn
  private Tag tag;
}

Next you should create repositories for your entity classes.

interface AlbumRepository extends JpaRepository<Album, Long>{

   @Query
   ("select DISTINCT(a) from AlbumTag at "+
    "join at.album a "
    "where at.tag is not null")
   List<Album> findAlbumWithTag();
}

Then simply call the repository function which will return a list of albums which have at least one tag.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM