How to avoid joining table when querying by a foreign key? - java

I have a Comment class with a user property defined as:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id")
#NonNull
private User user;
I have a CommentRepository:
public interface CommentRepository extends CrudRepository<Comment, Integer> {
List<Comment> findByUserId(Integer userId);
}
I want to query a particular user's comments by his id.
I'm doing this:
commentRepository.findByUserId(userId);
everything works fine except the query looks like:
select
comment0_."id" as id1_1_,
comment0_."text" as url2_1_,
comment0_."user_id" as user_id3_1_
from
"comments" comment0_
left outer join
"users" user1_
on comment0_."user_id"=user1_."id"
where
user1_."id"=?
I want to avoid this join as I can query directly by the user_id column in a comments table.
I don't want to use a #Query annotation, I think there should be a smarter way.

The default value for #ManyToOne(optional = true) and #JoinColumn(nullable = true) causes this extra join. You may try,
#ManyToOne(fetch=FetchType.LAZY, optional = false)
#JoinColumn(name="`user_id `", nullable = false)

Related

QueryDsl - Exclude results based on nested array contents

I'm trying to get all Posts which don't contain certain category using QueryDsl
My models are defined as:
Post
#QueryEntity
#Table(name = "posts")
public class PostEntity implements {
#Id
#Column(name = "id")
private String id;
#OneToMany
#JoinTable(
name = "post_categories",
joinColumns = #JoinColumn(name = "post_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "category_id", referencedColumnName = "id")
)
private List<CategoryEntity> categories;
}
Category
#QueryEntity
#Table(name = "categories")
public class CategoryEntity {
#Id
#Column
private String id;
}
(some Lombok annotations omitted for brevity)
The two are related through post_categories join table to tag posts with categories.
I've tried using the query similar to this one, to exclude posts categorised as news:
var query = QPostEntity
.postEntity
.categories.any().id.notIn("news");
However that still returns posts in that category - only way I got it to work properly is to include all post categories in notIn statement.
Question: How do I query for Posts which don't contain specific category?
Update #1
Seems the query above generates subquery similar to
where exists(
select 1 from post_categories where category_id not in ('news')
)
which also includes all the posts with other categories. I found the following query does produce correct results (not moved before exists statement):
where not exists(
select 1 from post_categories where category_id in ('news')
)
Which can be done by rewriting querydsl as:
.categories.any().id.in("news").not();
However that seems to be very confusing. Any better way of doing it?
I would try to solve this with subqueries. Can you try the following?
SubQueryExpression<String> subquery = JPAExpressions.select(QCategoryEntity.categoryEntity.id)
.from(QCategoryEntity.categoryEntity)
.where(CategoryEntity.categoryEntity.eq("news"));
return new JPAQueryFactory(em)
.select(QPostEntity.postEntity)
.from(QPostEntity.postEntity)
.innerJoin(QPostEntity.postEntity.categories)
.where(QCategoryEntity.categoryEntity.id.notIn(subquery));
Probably you are not using the JPAQueryFactory... if not, could you share how you are actually performing the query?

Too many queries problem with JPA + Hibernate even when using #Fetch(FetchMode.JOIN)

I am developing REST application using spring boot and I am trying to optimize the performance of the queries. I am currently using findAll from the repositories which is causing performance issues. Code is given below:
Person Entity
#Entity
#Table(name = "cd_person")
#Data
#NoArgsConstructor
public class Person {
....
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "password_id")
#Fetch(FetchMode.JOIN)
private Password password;
....
#ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name = "cd_person_role",
joinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "role_id", referencedColumnName = "id"))
#Fetch(FetchMode.JOIN)
private Set<Role> roles = new HashSet<>();
}
Password Entity
#Entity
#Table(name = "cd_password")
#Data
#NoArgsConstructor
public class Password {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
private Long id;
#Column(name = "password_hash", nullable = false)
private String passwordHash;
.......
}
Role Entity
#Entity
#Table(name = "cd_role")
#Data
#NoArgsConstructor
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "role_type")
#Enumerated(EnumType.STRING)
private RoleType roleType;
....
}
Person Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
Optional<Person> findByEmail(String email);
}
When I do a personRepository.findAll() there are select queries fired for each row in the person table to fetch the password and roles when I access the person. I know I can use #Query annotation with JOIN FETCH in the repository to make it force generate the single query but I was wondering if there was any other way to do so. I am looking for something which we can do at the entity level to reduce queries.
Using spring boot 2.1.5-RELEASE version and related dependencies.
PS. The #Data and #NoArgsConstructor are Lombok annotations.
The most minimal code change is to use the ad-hoc EntityGraph feature from spring data . Just override PersonRepository 's findAll() and use #EntityGraph to configure the graph. All entities in this graph will be fetched together.
public interface PersonRepository extends CrudRepository<Person, Long> {
#EntityGraph(attributePaths = { "password", "roles" })
public List<Person> findAll();
}
Behind scene it works like JOIN FETCH. Only single SQL with LEFT JOIN will be generated.
I would leave the Entity as is and override the findAll method in the repository with an #Query annotation.
This way, the code refactor is minimal (only one repository change instead of an entity change).
The unsatisfying answer to your question is: no, there's no way to annotate/configure the entities so that the fetch mode applies to a query as well.
As you correctly found yourself, you can manipulate the query itself. Alternatives to this are using Hibernate's fetch profiles or leveraging JPA entity graphs - but all of them require programmatic intervention at the query/session level as well.
You should place #BatchSize on top of Password class
#Entity
#Table(name = "cd_password")
#Data
#NoArgsConstructor
#BatchSize(size = 50)
public class Password {
...
}
Here are the queries with #BatchSize:
Hibernate:
select
person0_.id as id1_1_,
person0_.password_id as password2_1_
from
cd_person person0_
Hibernate:
select
password0_.id as id1_0_0_,
password0_.password_hash as password2_0_0_
from
cd_password password0_
where
password0_.id in (
?, ?, ?, ?, ?
)
Can't you use lazy fetch and remove the #Fetch ? Using #NamedQuery on top of your entity and using an hibernate session to call session.createNamedQuery in a custom service would do it.
If you can afford to not use the default personRepository.findAll() but this custom service you would run an optimized query. I get that it does not exactly answer your question but my team and I faced the exact same issue and this is how we did it.
My suggestions would be:
Try to refactor and use lazy fetching.
I might not understand this part well, but why do you need personRepository.findAll() exactly? I think you would merely need something like personRepository.findById(), so you could fetch the roles and other data easily. Selecting all persons seems to be a huge overload here.
You might need the extended functions of JpaRepository later, so it might be worth changing it now instead of working a little bit more later.
This should works:
public interface PersonRepository extends CrudRepository<Person, Long> {
#Override
#Query("SELECT p FROM Person p JOIN FETCH p.roles JOIN FETCH p.password ")
Iterable<Person> findAll();
}

how to construct spring JPA/JPQL query in JpaRepository

I've got a DB structure like this:
topics 1:n posts 1:n comments and users n:n roles.
I want to get all comments that user have an access.
Giving access for me means (when I create post object I automaticly create role object called PostName with prefix role_comment_ e.g. post called abc have role called: role_comment_abc)
Now I try to create jpa/jpql query like below:
find all comments by User where user_id is =:1 and role_name contaings =:2
findByUserIdAndRoleNameContaining(Integer userId, String roleName);
This is how my User, Role and comment tables looks like:
Roles table:
#Entity
#Table(name = "roles")
public class Role {
#Id
private Integer id;
private String name;
#ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
Users and user_role tables:
#Entity
#Table(name = "users")
public class User {
#Id
private Integer id;
private String name;
#ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
#JoinTable(
name = "user_role",
joinColumns = { #JoinColumn(name = "user_id") },
inverseJoinColumns = { #JoinColumn(name = "role_id") }
)
private Set<Role> roles = new HashSet<>();
And this is comments table:
#Entity
#Table(name = "comments")
public class Comments{
#Id
private Integer id;
private String name;
private String description;
#ManyToOne(optional = false)
#JoinColumn(nullable = false, name = "user_id")
private User user
Unfortunetly if I create query in JpaRepository called:
List<Comments> findByUserId(Integer id);
If i'm not wrong it will print comments created by that specific user.
So what I really want to achive? Let me show you this on example data:
roles:
100;"role_comment_ab"
101;"role_comment_cd"
102;"role_comment_ef"
103;"something_else"
Comments in post with name ab:
1;"Test1";"Test description";10
2;"Test2";"Test description";10
comments in post with name cd:
3;"Test3";"Test description";10
4;"Test4";"Test description";10
comments in post with name ef:
5;"Test5";"Test description";10
6;"Test6";"Test description";10
users:
10;"Thomas" (logged user)
11;"John"
users_roles:
10;100
11;101
10;102
10;103
input:
findByUserIdAndRoleNameContaining(10, "role_comment_");
output:
1;"Test1";"Test description";10
2;"Test2";"Test description";10
5;"Test5";"Test description";10
6;"Test6";"Test description";10
I'm really out of clue how my query should look like. Pleast atleast give me a small hint.
UPDATE:
After adding #Bohdan Petrenko solution:
#Query("select c from Comment c join c.user u join u.roles r where u.id = :userId and lower(r.name) like lower(:roleName)")
List<Comment> findByUserIdAndRoleNameContaining(#Param("userId") Integer userId, #Param("roleName") String roleName);
roleName = "%" + roleName.trim() + "%";
I noticed that this solution prints all comments if #Param roleName contains "roleName" String.
So if I have role_postName1 and role_postName2
it prints:
comment1FromPost1
comment2FromPost1
comment1FromPost2
comment2FromPost2
comment1FromPost1
comment2FromPost1
comment1FromPost2
comment2FromPost2
It would've be great to find solution to print comments from posts only if user have role called role_postName.
#Query("select t from Topic t
join t.user u
where u.id = :userId
and u.roles in :roleNames")
List<Topic> findByUserIdAndRoleNameContainedIn(#Param("userId") Integer userId, #Param("roleNames") List<String> roleNames);
Please be more specific in your questions.
If I understood you correctly - You want to select Topics by user id and Role name. If yes you may try something like this:
#Query("select t from Topic t join t.user u join u.roles r where u.id = :userId and lower(r.name) like lower(:roleName)")
List<Topic> findByUserIdAndRoleNameContaining(#Param("userId") Integer userId, #Param("roleName") roleName);
But you'll also need to change role name before passing it to the repository method findByUserIdAndRoleNameContaining:
roleName = "%" + roleName.trim() + "%";
UPDATE
This will work without any custom SQL and roleName modifications
List<Comment> findByUser_IdAndUser_Roles_NameContainingIgnoreCase(Integer userId, String roleName);
And also I don't understand the trouble you are faced with now. So, provide us some examples with a test data (from the data base), correct and incorrect expected results of the query. As I don't understand the reason of your problem I can't help you to solve it.

Workaround for HHH-2772 (hibernate querydsl)

I'm trying to perform a query to find cars by their foo property. The properties are stored in a different table.
I have two classes
#Embeddable
#Table(name = "PROPERTY")
public class Property {
#Column(name = "type", nullable = false)
private String type;
#Column(name = "string_value", nullable = true)
private String stringValue;
...
}
#Entity
#Table(name = "CAR")
public class Car {
#Id
...
private String id;
#ElementCollection(fetch = FetchType.EAGER)
#Fetch(FetchMode.SUBSELECT)
#CollectionTable(name = "PROPERTY", joinColumns = #JoinColumn(name = "car_ID") )
private Set<Property> properties = new HashSet<Property>();
...
}
I'm trying to perform a query
QueryDsl:
.from(car)
.leftJoin(car.properties, foo)
.on(foo.type.eq("foo"))
.where(predicate)
Resulting HQL:
select
car
from
com....Car car
left join
car.properties as foo with foo.type = :a1
where
...
This doesn't work because of: https://hibernate.atlassian.net/browse/HHH-2772
Before that, it was possible to write HQL:
SELECT cat FROM Cat cat LEFT JOIN cat.kittens as kitten WITH kitten.owner=:owner
Now the HQL is raising an exception:
org.hibernate.hql.ast.InvalidWithClauseException: with clause can only reference columns in the driving table
Workaround is to explicitly use primary key (ownerId):
SELECT cat FROM Cat cat LEFT JOIN cat.kittens as kitten WITH kitten.owner.ownerId=:ownerId
The problem is that I don't have the ownerId, or an owner, since it's an element collection.
If I were to turn the element collection into a #oneToMany #manyToOne, the property could not longer be embeddable and would require an id. This is not an option. (I can't define a composite ID (this is a requirement), and I don't want to add a new column )
What do you recommend?
Is it possible to add the Car or Car Id as a field into an embaddable class?
Can I create the criteria in a different way?
I'm interested in any workaround that doesn't require database changes. (Hibernate changes or ok)
Thank you

Hibernate nested JoinColumns results in a big query from the database with unnecessary data

I'm working on some personal project but i have a question about hibernate.
I have a class structure like this :
#Entity
public class User {
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "fkIdCompanyUser")
private Company company = new Company();
}
But inside the company i have another join.
#Entity
public class Company {
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "fkIdCompanyEstimateOption")
private EstimateOptions estimateOptions = new EstimateOptions();
}
Now i do a query to get the estimate options.
But if i do it like this it loads lots of unnecessary stuff .
#RequestMapping(value = "/estimateoptions")
public EstimateOptions getCompanyEstimateOptions(#AuthenticationPrincipal Principal user) {
User getuser = userDao.findByEmail(user.getName());
EstimateOptions estimateOptions = getuser.getCompany().getEstimateOptions();
return estimateOptions;
}
is there a better approach for this ?
There are a lot of ways to do such optimization. The simplest one is add bidirectional associations to Company and EstimateOptions with lazy loading.
An example for Company ( I don't test. It is just a sketch.)
#Entity
public class Company {
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "fkIdCompanyEstimateOption")
private EstimateOptions estimateOptions = new EstimateOptions();
#OneToOne(mappedBy="company", fetch = FetchType.LAZY)
private User user;
}
And do something like this (this is HQL but you can use a criteria API too)
from EstimateOptions options inner join options.company company inner join company.user user where user.name = :userName
You can see HQL joined query to eager fetch a large number of relationships for additional thoughts.
Updated
I am not sure but may be you can do something like this (without additional associations)
select options from User user inner join user.company company inner join company.estimateOptions options where user.name = :userName

Categories

Resources