JPA Criteria Builder with join and dynamic parameters - java

I have 2 classes with One-to-Many relationship.
Customer (class) has many Transactions (class)
public class Customer {
#Id
private Long clientId;
private String name;
#OneToMany
private List<Transactions> transactions;
}
public class Transactions {
#JoinColumn(name = "clientId")
private Transactions transactions;
private int statusType;
private String amount;
}
int dynamicValue = 1003;
CriteriaQuery<Customer> criteriaQuery = getBuilder().createQuery(Customer.class);
Root<Customer> customersRoot = criteriaQuery.from(Customer.class);
Join<Customer, Transactions> transactions = customersRoot.join("transactions");
TypedQuery<Customer> query = em.createQuery(criteriaQuery.select(customerRoot).where(getBuilder().equal(transactions.get("statusType"), dynamicValue)));
List<Customer> customerList = (List<Customer>) query.getResultList();
I have 2 data from the DB:
Customer Table
ClientId | Name |
1 | James |
2 | Eli |
Transactions Table:
ClientId | Status Type| Amount| TransactionId |
1 | 1002 | 100 | 1 |
1 | 1003 | 200 | 2 |
I need to make my query above to accept multiple parameters (dynamic). These parameters will be coming from the Customer's attributes such as name, some parameters will be coming from the Transactions class. However, when I tried to execute my code above it always get the 1st record (1002) in my database which is incorrect.
Please give me somelight.
Questions:
How can I achieved to have multiple dynamic parameters in criteria builder?
What is wrong with my query why it always get the 1st record?

You are currently just passing in a Literal. This is not the same as a Parameter. See http://www.datanucleus.org:15080/products/accessplatform_5_2/jpa/query.html#_criteria_api_parameters
Change your code to
CriteriaQuery<Customer> criteriaQuery = getBuilder().createQuery(Customer.class);
Root<Customer> customersRoot = criteriaQuery.from(Customer.class);
Join<Customer, Transactions> transactions = customersRoot.join("transactions");
ParameterExpression param = getBuilder().parameter(int.class, "myParam");
TypedQuery<Customer> query = em.createQuery(criteriaQuery.select(customerRoot).where(getBuilder().equal(transactions.get("statusType"), param)));
// Execute with first parameter value
query.setParameter("myParam", 1003);
List<Customer> customerList = (List<Customer>) query.getResultList();
Then if you get a problem with the result, you look in the JPA providers log at the SQL that was executed, and can understand the problem better

Related

JPA (Hibernate) / QueryDSL left join with condition doesn't work

I have two tables - user and booking. Each user may have many bookings (one-to-many relationship).
user: booking:
id | name | id | country | user_id | price |
-------------| ------------------------------------|
1 | Alice | 1 | Italy | 1 | 2000 |
2 | Bob | 2 | France | 1 | 2500 |
3 | Spain | 1 | 3000 |
I want to select all users and all bookings where booking's price is greater than 2000 using Query DSL. If a user doesn't have any bookings or bookings don't match the condition I still want to select this user.
First, let's have a look at how it would look like using a simple SQL left join query:
SELECT u.*, b.* FROM user u LEFT JOIN booking b ON u.id = b.user_id AND b.price > 2000
The above query should provide the following result:
id | name | id | country | user_id | price |
-------------|----------------------------------------|
1 | Alice | 2 | France | 1 | 2500 |
1 | Alice | 3 | Spain | 1 | 3000 |
2 | Bob | null | null | null | null |
Now I want to do it using JPA with Query DSL
JPA-related stuff:
#Entity
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
private List<Booking> bookings;
// getters and setters
}
#Entity
public class Booking {
#Id
private Long id;
private String name;
private Integer price;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "user_id")
private User user;
// getters and setters
}
Query DSL:
public List<User> getUsersAndBookings() {
QUser user = QUser.user;
QBooking booking = QBooking.booking;
JPAQuery<User> jpaQuery = new JPAQuery(entityManager);
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).on(booking.price.gt(2000)).fetchJoin().fetch();
return result;
}
In fact, this code is not working and I get the following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause not allowed on fetched associations; use filters [select user from com.example.demo.entity.User user left join fetch user.bookings as booking with booking.price > ?1]
The problem is that the condition clause is specified in on method - on(booking.price.gt(2000)).
After some research I found that this condition should be specified in where method and should look like this:
List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).where(booking.price.gt(2000)).fetchJoin().fetch();
This works, but not how I would expect it to work, since it doesn't return ALL users, it returns only one user (Alice), which has some bookings, matching the condition clause. Basically, it just filters the merged table (result table after left join operation) and that's not what I'm looking for.
I want to retrieve all users, and if there are no any bookings for a specific user, then just have null instead of booking list for this user.
Please help, been struggling for hours without any success.
Versions used:
Spring Boot 2.0.2
Spring Data JPA 2.0.7
Hibernate 5.2.16.Final
QueryDSL 4.1.4
You can use isNull expression in where clause to get the rows that have null values.
Your query should be like this:
jpaQuery.from(user)
.leftJoin(user.bookings, booking)
.fetchJoin()
.where(booking.price.gt(2000).or(booking.id.isNull())).fetch();
Hibernate produced query:
select
user0_.id as id1_1_0_,
bookings1_.id as id1_0_1_,
user0_.name as name2_1_0_,
bookings1_.country as country2_0_1_,
bookings1_.price as price3_0_1_,
bookings1_.user_id as user_id4_0_1_,
bookings1_.user_id as user_id4_0_0__,
bookings1_.id as id1_0_0__
from
user user0_
left outer join
booking bookings1_
on user0_.id=bookings1_.user_id
where
bookings1_.id is null
or bookings1_.price>?
It seems there is no JPA way for this. But I got it fixed in Hibernate way, using Filters org.hibernate.annotations.Filter.
#Entity
#FilterDef(name = "anyName", parameters = {
#ParamDef(name = "price", type = "integer")
})
public class User {
#Id
private Long id;
private String name;
#OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
#Filter(name = "anyName", condition = "price > :inputPrice")
private List<Booking> bookings;
}
Before querying the db, you must enable this filter.
Session session = enityManager.unwrap(Session.class);
session.enableFilter("anyName").setParameter("inputPrice", 2000);
// fetch using hql or criteria; but don't use booking.price.gt(2000) or similar condition there
session.disableFilter("anyName");
Now the result will have a User even if all of his booking prices are below 2000 and bookings list will be empty as expected.
NOTE: The word price in condition should be exactly same as the db column name; not as the model property name.

Use Hibernate Criteria API to return first row of each group

I am new to Hibernate and I am trying to write a criteria query to return the latest status of employee on a given date
id | Status | status_date
1 | Active | 1/10/2017
2 | Active | 1/10/2017
...
1 | Inactive| 5/10/2017
...
1 | Active | 9/10/2017
So I will be passing a date to the query and want to find the latest status of every employee on that date
The expected result will be something like this
Example: For date 6/1/2017, this will be the returned data
id | Status | Date
1 | Inactive| 5/10/2017
2 | Active | 1/10/2017
I was able to add group by with id and order the rows by status date in descending order. Is there a way I can select only the top row for each group? I tried to use the max function on status_date but that does not work.
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> cq = builder.createQuery(Employee);
Root<Employee> root = cq.from(Employee.class);
cq.select(root);
cq.groupBy(root.get("id"));
cq.orderBy(builder.desc(root.get("status_date")));
cq.having(builder.max(root.get("status_date")));
Since you want to output aggregation, not use aggregation as condition so you should not place it in having clause. You must add the aggregation to selection list instead.
First you must create aggregation result class (It's usual to different to your entity class):
public static class StatusEntityResult {
private String userId;
private String status;
private Date statusDate;
// getter, setter, constructor with all parameters here
}
Then create a query using it as result:
public List<StatusEntityResult> queryStatus() throws ParseException {
// Date condition
Date targetDate = new SimpleDateFormat("yyyy-MM-dd").parse("2017-10-06");
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
// Use StatusEntityResult as result
CriteriaQuery<StatusEntityResult> cq = builder.createQuery(StatusEntityResult.class);
Root<Employee> root = cq.from(Employee.class);
// Select `id`, `status` and `max(status_date)`
cq.multiselect(root.get("id"), root.get("status"), builder.max(root.get("statusDate")))
.where(builder.lessThanOrEqualTo(root.get("statusDate"), targetDate))
.groupBy(root.get("id"))
.orderBy(builder.desc(root.get("statusDate")));
return this.entityManager.createQuery(cq).getResultList();
}
And the result is:
Side note
From what you wrote, you was attempting to use JPA Criteria, not Hibernate Criteria. For Hibernate Criteria solution, you can try to read #default locale suggestion

Hibernate HQL Join Fetch returns duplicate rows

ProcessSolution Entity :
#Entity
#Table(name="process_solution")
public class ProcessSolution implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="process_id", columnDefinition="INT(10) UNSIGNED")
private Integer processId;
#Column(name="process_name", length=120, nullable=false)
private String processName;
#ElementCollection(fetch=FetchType.LAZY)
//#LazyCollection(LazyCollectionOption.FALSE)
//#Fetch(FetchMode.Select)
#JsonIgnore
#CollectionTable(name="process_solution_step",
joinColumns=#JoinColumn(name="process_id"),
foreignKey=#ForeignKey(name="fk_process_solution_step_process_id")
)
#Column(name="solution_step", length=200, nullable=false)
private List<String> processSolutionSteps = new ArrayList<>();
#ManyToOne
#JoinColumn( name="category_id", columnDefinition="INT(10) UNSIGNED",nullable=false,
foreignKey=#ForeignKey(name="fk_process_solution_category")
)
private Category category;
#ManyToMany(fetch=FetchType.LAZY)
#JoinTable(name="process_solution_employee",
joinColumns={#JoinColumn(name="process_id")},
inverseJoinColumns={#JoinColumn(name="emp_id",columnDefinition="INT(10) UNSIGNED")},
foreignKey=#ForeignKey(name="fk_process_employee_process_solution_process_id"),
inverseForeignKey=#ForeignKey(name="fk_process_employee_employee_emp_id")
)
private Set<Employee> employees = new HashSet<>();
// Getters/Setters
}
And I'm Executing HQL Query in DAO as:
#Override
public ProcessSolution getProcessSolution(Integer processId) {
Session session = this.sessionFactory.openSession();
final String GET_PS = "SELECT ps FROM ProcessSolution ps JOIN FETCH ps.processSolutionSteps JOIN FETCH ps.employees WHERE ps.processId = :processId";
//ProcessSolution processSolution = session.get(ProcessSolution.class, processId);
ProcessSolution processSolution = ( ProcessSolution ) session.createQuery(GET_PS)
.setInteger("processId", processId).uniqueResult();
session.close();
return processSolution;
}
My Problem is I'm Getting ElementCollection i.e. processSolutionSteps repeated (Multiple Rows).
So I changed it From List<> to Set<>, now I'm getting correct result but its order is not preserved.
What I have tried:
For Set I have tried LinkedHashSet but problem still persist.
#LazyCollection(LazyCollectionOption.FALSE) from here
#Fetch(FetchMode.Select) from another SO source
Any Idea how to solve this problem.
Updated :
Sample Data :
**process_solution**
+---------------+----------------+
| process_id | process_name |
+---------------+----------------+
| 3 | process 1 |
+---------------+----------------+
**process_solution_step**
+---------------+----------------+
| process_id | solution_step |
+---------------+----------------+
| 3 | step 1 |
+---------------+----------------+
| 3 | step 2 |
+---------------+----------------+
If I print Process Solution Steps I get the result as
step 1
step 1
step 2
step 2
If I print Employee Lenth I got correct result.

JPA Critera API Join 3 Tables get 1 Type

I´m trying to join 3 tables with JPA Critera API and get the result as a list of type other than the relation table.
The Entities are:
| Employee | | Contract | | Company |
|----------| |----------| |---------|
| id | | Company | | id |
| age | | Employee | | name |
A Contract is the relationship between a Company and Employee
An employee may belong to one or more Companies
A company has one or more employees
I try now to get all Employees that work for Company A like so:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Contract> query = cb.createQuery(Contract.class);
Root<Contract> contracts = query.from(Contract.class);
Join<Contract, Company> companyJoin = contracts.join("company");
Join<Contract, Employee> employeeJoin = contracts.join("employee");
List<Predicate> conditions = new ArrayList<Predicate>();
conditions.add(cb.equal(companyJoin.get("name"), "Company A"));
TypedQuery<Practice> typedQuery = em.createQuery(query
.select(contracts)
.where(conditions.toArray(new Predicate[conditions.size()]))
);
typedQuery.getResultList();
This gives me a List of Contracts with Empoyees that work in "Company A".
How can I write the Query to get a List of Employees instead of Contracts?
Start with a Root of Employees and make a chain of joins:
CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
Root<Employee> employee = query.from(Employee.class);
Join<Employee, Contract> contractJoin = employee.join("contracts"); // assuming that Employee has a collection property named contracts
Join<Contract, Company> companyJoin = contractJoin.join("company");
This is the correct Awnser with the following addition:
The Types "Employee" and "Company" have to have a field "companies" / "employees" with the #JoinTable annotation like follows:
Employee:
...
#OneToMany
#JoinTable(name="Contract" ...)
private List<Company> companies;
...
Company
...
#OneToMany
#JoinTable(name="Contract" ...)
private List<Employee> employees;
...
The "#JoinTable" annotation prevents hibernate to create a relation table on its own.
See the comments for more info.

Hibernate native sql join query

I have a problem with hibernate native sql join query. My query is below and works on Mysql db.
SELECT c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c JOIN
sandbox.user u
ON u.id = c.placedBy
I am using hibernate in code and encountered an exception
java.sql.SQLException: Column 'id' not found.
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1055)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:926)
com.mysql.jdbc.ResultSetImpl.findColumn(ResultSetImpl.java:1093)
Query in code here
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
query.addEntity(OrderPerUser.class);
return query.list();
Table column name
Cart
| cart_id | placedBy | totalPrice
User
| id | email | name | surname
My mapped class is
#Entity
public class OrderPerUser {
#Id
private long id;
private String name;
private String surName;
private long cartId;
private double totalPrice; }
You need to remove the line:
query.addEntity(OrderPerUser.class);
After that, you need to rewrite the code and map your object manually, because your OrderPerUser is not an entity:
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
List<OrderPerUser> returnList new ArrayList<>();
for(Object[] row : query.list()){
OrderPerUser orderPerUserObj = new OrderPerUser();
oderPerUserObj.setCartId(Long.parseLong(row[0].toString()));
//put other properties here
returnList.add(orderPerUserObj);
}
return returnList;
Edit1: Now I see that you added the mapped class, but OrderPerUser should not be an entity in your case, but a regular DTO. An entity requires an ID, but you can't select the ID in this case, because OrderPerUser is not part of a table, it is just some selected data that you want in your memory and not in the database. So you should make your OrderPerUser a regular data transfer object.
Please read about entities, data transfer objects, data access objects to see what each object should do.
My guess is that your OrderPerUser class which you try to use for collecting the result is expecting a column with name id, and you have no such column in your query...
Try using the query:
SELECT u.id, c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c
JOIN sandbox.user u ON u.id = c.placedBy

Categories