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.
Related
I'm mapping sql view to entity class with hibernate #Subselect annotation.
Basically, it looks somewhat like this:
#Subselect(
"SELECT table1.*, table2.id as tid FROM "
+ "table1 INNER JOIN table2 on table2.field = table1.field"
)
#Entity
#Immutable
class Entity {
// fields
}
When the join works i may get something like the following
========================================
| table1.id | table1.field | table2.id |
========================================
| 1 | 1 | 1 |
========================================
| 1 | 1 | 2 |
========================================
So several records in table2 can join to one row in table1. This is fine, however in java Entity I want to map it as one to many relationship(one entity to many table2 enities), here's what I wrote, which worked for others kind of relationships:
#Subselect(
"SELECT table1.*, table2.id as tid FROM "
+ "table1 INNER JOIN table2 on table2.field = table1.field"
)
#Entity
#Immutable
class Entity {
#OneToMany
#JoinColumn(name = "tid", updatable = false, insertable = false)
private Set<Table2Entity> elements = new HashSet<>();
}
However, the set in the entity is always empty, why is that ?
The above approach works for one to one and many to one relationships.
Turns, it's not required to do join with sql, to bring one to many relationship for the view entity. I solved it like this:
Subselect(
"SELECT * from table1"
)
#Entity
#Immutable
class Entity {
#OneToMany
#JoinColumn(name = "field", updatable = false, insertable = false)
private Set<Table2Entity> elements = new HashSet<>();
}
I only needed to put column name, on which tables needed to be joined.
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
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.
I am using Java8 with Hibernate and MySQL.
I have the following tables with a join table:
+-------+ +----------------+ +------------+
| job | | person_job | | person |
+-------+ +----------------+ +------------+
| ID | | PER_ID | | ID |
| | | JOB_ID (PK) | +------------+
+-------+ +----------------+
(A PERSON can have many JOBs)
When I try save a new JOB, it has a foreign key join to and existing PERSON. It looks like Hibernate wants to also save a new PERSON, resulting in a duplicate entry. I thought Hibernate would be smart enough, that if there is already a matching PERSON, it won't try save it again.
Resulting in the following error when trying to save a row in the JOB table:
MySQLIntegrityConstraintViolationException: Duplicate entry '338-1688' for key 'PRIMARY'
SQL
SELECT * FROM ebdb.person_job;
PER_ID JOB_ID
338 16
and
SELECT * FROM ebdb.person
ID
338
and
SELECT * FROM ebdb.job;
ID
16
Job.java
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "person_job", joinColumns = {
#JoinColumn(name = "JOB_ID", referencedColumnName = "ID") }, inverseJoinColumns = {
#JoinColumn(name = "PER_ID", referencedColumnName = "ID") })
private Person person;
When I run in debug mode, I see the new Job row it is trying to merge has an ID of 26 and a PERSON with an ID of 338 as expected.
protected T merge(T entity) throws InstantiationException, IllegalAccessException {
T attached = null;
if (entity.getId() != null) {
attached = entityManager.find(entityClass, entity.getId());
}
if (attached == null) {
attached = entityClass.newInstance();
}
BeanUtils.copyProperties(entity, attached);
entityManager.setFlushMode(FlushModeType.COMMIT);
attached = entityManager.merge(attached);
return attached;
}
Question
How do you create a new entry on one table (JOB), that has a foreign key join to an existing entry (PERSON)?
i.e. I want to just maintain a #ManyToOne relationship.
Please can anyone assist.
UPDATE
If I try persist instead of merge, I get:
detached entity passed to persist: com.jobs.spring.domain.Person
SOLUTION
I update with the attached object.
Person attached = entityManager.find(Person.class, person.getId());
person = attached;
job.setPerson(person);
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.