HQL checking if object contains all element s of requested set - java

I have two entities. For example, posts and tags to it.
I have to write a method that will take only posts, which have all of tags, mentioned in query.
I tried
#Query("select distinct p from posts p join p.tags t where t in ?1")
Page<Post> findDistinctByTagsIn(Set<Tag> tagSet, Pageable pageable);
However it takes Post if at least one of its tags included in tagSet.
How can I solve this using only HQL and JPA Repositories?
UPD:
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "posts_tags", joinColumns = {
#JoinColumn(name = "post_id", nullable = false, updatable = false)},
inverseJoinColumns = {#JoinColumn(name = "tag_id")})
public Set<Tag> getTags() {
return tags;
}

Add to you Tag class next ManyToOne relation:
#Entity
public class Tag{
...
private Post post;
#ManyToOne
#JoinTable(name = "posts_tags",
joinColumns = {#JoinColumn(name = "tag_id")},
inverseJoinColumns = {#JoinColumn(name = "post_id")})
public Post getPost(){
return post;
}
...
}
Let's try to build query.
We don't need posts that have tags that are out of our list of tags. We will select them with next query:
select t.post from Tag t where t not in (:tagSet) and t.post is not null
And we don't need posts, that have not any tags at all. Let's select them as well:
select p from Post p where p.tags is empty
Now let's join our queries together:
select p from Post p where
p not in (select t.post from Tag t where t not in (:tagSet) and t.post is not null)
and p not in (select p2 from Post p2 where p2.tags is empty)
And you can use named parameter to bind this query:
Page<Post> findDistinctByTagsIn(#Param("tagSet") Set<Tag> tagSet, Pageable pageable);

Related

How to get only ids in #ManyToMany mapping, without referencing the whole object?

I am following the tutorial from baeldung:
https://www.baeldung.com/jpa-many-to-many
The first example is what I need:
The example shows how to add relationship in User class:
#ManyToMany
#JoinTable(
name = "course_like",
joinColumns = #JoinColumn(name = "student_id"),
inverseJoinColumns = #JoinColumn(name = "course_id"))
Set<Course> likedCourses;
The difference in my case is that I do not need to access the whole object when getting the data.
I only need to return the set of Ids.
1.) I have tried the option with #ElementCollection:
#ManyToMany
#JoinTable(
name = "course_like",
joinColumns = #JoinColumn(name = "student_id"),
inverseJoinColumns = #JoinColumn(name = "course_id"))
Set<Course> likedCourses;
#ElementCollection
#CollectionTable(
name = "course_like",
joinColumns = #JoinColumn(name = "student_id"))
Set<Long> likedCourses;
2.) Also, returning the whole object, iterating and storing ids in,
and
3.) There is a third option with #Transient and #PostLoad
public Set<Course> likedCourses;
#Transient
public Set<Long> courseIds;
#PostLoad
private void postLoad() {
courseIds = likedCourses.stream().map(Course::getId).collect(Collectors.toSet());
}
I am looking for the most optimized way, it seems to me option #3 is not good since it could call #Postload potentially more than once.
How would you go about this? Thank you!
You can do it with the following JPQL query:
SELECT c.id
FROM Student s JOIN s.likedCourses c
WHERE s.id = :id
However, I see that Hibernate generates an extra join with the courses table which is unfortunate. If you have problems with that, you can use a native SQL query as #TimBiegeleisen suggested.

How can i get value from ManyToMany table?

I have two classes: Child and Guardian
in Guardian class i have that field:
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "guardian_child", schema = "schema",
joinColumns = #JoinColumn(name = "guardianid"),
inverseJoinColumns = #JoinColumn(name = "childid"))
private List<Child> children = new ArrayList<>();
I have table guardian_child in my Postgres. And now i need to get all children by id of guardian? Need i create special entity and repository for this table? Or how can i do it?
You can get data using JPQL query like this: #Query(SELECT g FROM Guardian JOIN g.children c)... if it doesn't work let me know once again

JPA Query annotation over ManyToMany relationship works not properly

I am trying to implement an example like this: A Person class has a list of places that it likes. But when I want to query it, I want result as each person with only the most favorite place(just the first one not all of them). So I have done this:
#Entity
class Person{
...
#ManyToMany(cascade = {CascadeType.REFRESH,CascadeType.PERSIST}, fetch = FetchType.EAGER)
#JoinTable(name = "person_favorite_place",
joinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "place_id", referencedColumnName = "id")
)
#OrderColumn(name="favorite_place_order")
List<Place> favoritePlaces;
}
And in repository, I did:
public interface PersonRepository extends JpaRepository<Person, Long> {
#Query(value = "select person0_.id as id1_0_0_, person0_.age as age2_0_0_, person0_.name as name3_0_0_, favoriteme1_.person_id as person_id1_1_1_, place2_.id as place_id2_1_1_, favoriteme1_.favorite_place_order as favorite3_1_, place2_.id as id1_3_2_, place2_.invented as invented2_3_2_, place2_.name as name3_3_2_ from person person0_ left outer join person_favorite_place favoriteme1_ on person0_.id=favoriteme1_.person_id left outer join place place2_ on favoriteme1_.place_id=place2_.id where person0_.id=:personId and favoriteme1_.favorite_place_order = 0", nativeQuery = true)
Person getPersonWithFavoritePlace(#Param("personId") Long personId);
}
As expected, the query needs to return 1 Place for each Person but it always returns all the places that Person likes. What am I doing wrong in here?

Spring + Hibernate: How do I efficiently chain two link tables and include resulting data in single entity? (user-role-right)

Short version
I have a basic setup where a user table is linked to a role table and a role table is linked to a right. These are both Many-to-Many relations. The roles are a dynamic entity and not of interest for the application (only for visual aspects). When I fetch a user I want to return the data in the user table including a list of the names of all rights.
To clarify, this is what I want the solution to do:
I managed to get the rights in my user object and return them, but it's inefficient due to the extra query calls hibernate makes after the original query was called.
Detailed version
Let me first give you some information on how to entities are linked and what the code looks like. My (simplified) database table structure looks like this:
User.java
#Entity
#Table(name = "user")
public class User {
#Id
#Column(name = "user_id", columnDefinition = "user_id")
private Long userId;
#Transient
private List<String> rights
#ManyToMany
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"))
#JsonIgnore
private List<Role> roles;
//Getters and setters
}
Role.java
#Entity
#Table(name = "role")
public class Role {
#Id
#Column(name = "role_id", columnDefinition = "role_id")
private Long roleId;
#ManyToMany
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"),
inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName = "user_id"))
#JsonIgnore
private List<Employee> employees;
#ManyToMany
#JoinTable(
name = "role_right",
joinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"),
inverseJoinColumns = #JoinColumn(name = "right_id", referencedColumnName = "right_id"))
#JsonIgnore
private List<Right> rights;
//Getters and setters
}
Right.java
#Entity
#Table(name = "right")
public class Right {
#Id
#Column(name = "right_id", columnDefinition = "right_id")
private Long rightId;
#Column(name = "name", columnDefinition = "name")
private String name;
#ManyToMany
#JoinTable(
name = "role_right",
joinColumns = #JoinColumn(name = "right_id", referencedColumnName = "right_id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "role_id"))
#JsonIgnore
private List<Role> roles;
//Getters and setters
}
It's important to know that I use the Java Specifications API to join the tables:
return (root, query, cb) -> {
query.distinct(true);
Join rolesJoin = root.join("roles", JoinType.LEFT);
Join rightsJoin = rolesJoin.join("rights", JoinType.LEFT);
return cb.conjunction();
};
This creates the correct query:
select <columns go here>
from employee user0_
left outer join user_role roles1_ on user0_.user_id=roles1_.user_id
left outer join role role2_ on roles1_.role_id=role2_.role_id
left outer join role_right rights3_ on role2_.role_id=rights3_.role_id
left outer join right right4_ on rights3_.right_id=right4_.right_id
Everything looked to good to me till now. But when I tried to fetch the names of all roles, there where more than two queries (count for page and the original one) being executed
//The original code uses lambda for this
for(Role role : user.getRoles()){
for(Right right: role.getRights()){
user.addRight(right.getName());
}
}
The extra query looks like:
select <column stuff>
from role_right rights0_
inner join right right1_ on rights0_.right_id=right1_.right_id
where rights0_.role_id=?
This makes the call very inefficient to me. In this case it's a single user, but with multiple users it adds up.
Is there a way to have a single query put the names of all rights in the user entity, without adding extra query executions?
Things I tried so far:
Using #SecondaryTable to directly define column from the Right table in my User entity. I could not get to first link the Role to the User and then use fields from the Role table to link the Right table. So in the end I would have to #SecondaryTable annotation on top of my User object and define columns of the Right object below.
Using #Formula in the User entity to insert a native call into the query. This did also not work as the annotation did not understand how to map everything into a list of rights.
There might be other options here, or I did something horribly wrong with implementing the ones above. But for now I don't which way to go in finding a solution for my problem. If someone could tell me, that would be great.
Thanks in advance,
Robin
You are using Root.join which does just the joining of tables for the purposes of the query; lazy associations in the loaded entities will still not be initialized.
As I see, your intention is to initialize the lazy collections as well. For that you have to use Root.fetch (defined in the interface method inherited from the FetchParent):
Create a fetch join to the specified collection-valued attribute using
the given join type.
However, your intention is not a good practice; do not join multiple collections in one query, otherwise the query result set will explode with full Cartesian product between the joined collections. Your result set contains <num of users> * <num of roles per user> * <num of rights per role> rows. So, each user data is repeated <num of roles per user> * <num of rights per role> times in the generated result set.
The approach I find to be the best and most straightforward is to specify batch size on lazy associations.

JPA Shared Entity mapping

I have a scenario where I have a Object called Page, and another object called Tag, relationship between these two is Page has Tags(many to many), but the same Tags can also shared with Product, here also the relationship is same Product has Tags (many to many).
In normal scenario I will create a type column in Tag where type may be Enum value (product, page) and use query like SELECT * from Tags where parent_id = page_id and type = page.
How to do this in JPA (how to create this relationship and how to query data)
I think you should create two tables of associations. One to associate Pages with tags, one to associate Products with tags. The code:
#Entity
#Table(name = "page")
class Page {
....
#OneToMany
#JoinTable(name = "jnd_pages_tags", joinColumns = #JoinColumn(name = "page_fk"),
inverseJoinColumns = #JoinColumn(name = "tag_fk"))
private Set<Tag> tags;
}
#Entity
#Table(name = "page")
class Product {
....
#OneToMany
#JoinTable(name = "jnd_products_tags", joinColumns = #JoinColumn(name = "products_fk"),
inverseJoinColumns = #JoinColumn(name = "tag_fk"))
private Set<Tag> tags;
}
#Entity
#Table(name = "tags")
class Tag {
.....
}
to select corresponding tags for page, use
SELECT p.t FROM Page p WHERE p.id = :id
to select corresponding tags for product, use
SELECT p.t FROM Product p WHERE p.id = :id

Categories