[JPA] counting @ManyToMany relations
[JPA] counting rows in @ManyToMany relations
Three tables are given
SQLCREATE TABLE posts (
seq INT(11) NOT NULL AUTO_INCREMENT,
title VARCHAR(256) NOT NULL,
PRIMARY KEY (`seq`),
...
);
CREATE TABLE tags (
seq INT(11) NOT NULL AUTO_INCREMENT,
tag VARCHAR(64) NOT NULL,
PRIMARY KEY (`seq`),
...
)
;
CREATE TABLE `tag_per_post` (
post INT(11) NOT NULL,
tag INT(11) NOT NULL,
CONSTRAINT fk_post FOREIGN KEY (post) REFERENCES posts (seq),
CONSTRAINT fk_tag FOREIGN KEY (tag) REFERENCES tags (seq)
)
;
- posts : tags = M : N
- tag_per_post(association table)

And two entities, Post
and Tag
are
JAVA@Entity
@Table(name="posts")
public class Post {
@ManyToMany
@JoinTable(
name="tag_per_post",
joinColumns = @JoinColumn(name="post"), //means tag_per_post.post
inverseJoinColumns =@JoinColumn(name="tag")) //means tag_per_post.tag
Set<Tag> tags;
}
@Entity
@Table(name="tags")
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer seq;
@Column(name="tag")
String text;
@ManyToMany(mappedBy = "tags")
Set<Post> posts;
}
- Post.tags - defines @ManyToMany relation
- Tag.posts - references mapping from
Post.tags(mappedBy="tags")
JPQL to count the number of posts per tag
ITagDao.java
will count the number of posts for all tags
ITagDao.java@Repository
public interface ITagDao extends JpaRepository<Tag, Integer>{
@Query("SELECT "
+ " new blog.domain.tag.Tag(t.seq, t.text, COUNT(p)) "
+ " FROM Post p "
+ " JOIN p.tags t"
+ " WHERE p.authorSeq = ?1"
+ " GROUP BY t.seq"
)
Set<Tag> countByPosts(Integer authorSeq);
}
- Tag(t.seq, t.text, COUNT(p)) - call constructor with seq, tagname(t.text) and the number of posts
JPQL @Query("SELECT ... GROUP BY t.seq")
generates native query like this
SQL select
tag2_.`seq` as col_0_0_,
tag2_.`tag` as col_1_0_,
count(post0_.`seq`) as col_2_0_
from
`posts` post0_
inner join
`tag_per_post` tags1_
on post0_.`seq`=tags1_.`post`
inner join
`tags` tag2_
on tags1_.`tag`=tag2_.`seq`
where
post0_.`author`=?
group by
tag2_.`seq`
- generated by hibernate
For this to work, class Tag
should have an constructor(Integer, String, Long)
Tag.javapublic class Tag {
// (1) the number of posts, no corresponding column in table tags
@Transient
Long count;
// (2) default constructor
public Tag() {
}
// (3) another constructor for rows per tag
public Tag(Integer seq, String text, Long count) {
this.seq = seq;
this.text = text;
this.count = count;
}
}
- property
Tag.count
is not a column in tabletags
. It need to be@Transient
not to be mapped by hibernate. - default constructor
(2)
is required becauseTag(Integer, String, Long)
is defined explicitly.
Code
Tag.java
package blog.domain.tag;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import blog.domain.post.dto.Post;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name="tags")
@Getter
@Setter
@ToString
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer seq;
@Column(name="tag")
String text;
@ManyToMany(mappedBy = "tags")
Set<Post> posts;
@Transient
Long count;
public Tag() {
}
public Tag(Integer seq, String text, Long count) {
this.seq = seq;
this.text = text;
this.count = count;
}
public static Tag withText(String tagText) {
Tag tag = new Tag();
tag.setText(tagText);
return tag;
}
}
Post.javapackage blog.domain.post.dto;
import java.time.Instant;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Type;
import blog.BlogException;
import blog.ErrorCode;
import blog.domain.tag.Tag;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name="posts")
@Getter
@Setter
@DynamicUpdate
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Integer seq;
...skip properties....
@ManyToMany
@JoinTable(
name="tag_per_post",
joinColumns = @JoinColumn(name="post"),
inverseJoinColumns =@JoinColumn(name="tag"))
Set<Tag> tags;
...
}
ITagDao.javapackage blog.domain.tag;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ITagDao extends JpaRepository<Tag, Integer>{
@Query("SELECT "
+ " new github.yeori.blog.domain.tag.Tag(t.seq, t.text, COUNT(p)) "
+ " FROM Post p "
+ " JOIN p.tags t"
+ " WHERE p.authorSeq = ?1"
+ " GROUP BY t.seq"
)
Set<Tag> countByPosts(Integer authorSeq);
}