Cannot map joined table data using Projection, etc. in Spring Data JPA - java

I am trying to retrieve name, surname, sum of balance (by grouping by customer) and transaction list by using a single database call by using Spring Data JPA #Query and Projection as shown below:
Here is the related entities; Customer has many Accounts, and Account has many Transactions (relations are defined properly using Hibernate).
public class Customer {
private Long id;
private String name;
private String surname;
}
public class Account {
private Long id;
private BigDecimal balance;
}
public class Transaction {
private Long id;
private Long accountId;
private LocalDateTime date;
}
I tried to map the Customer entity that has also Account and Transaction data to a DTO, but I could not map properly.
Then I tried to use the following query and then map the result to a projection, but the problem is that, it return records for each transaction.
#Query(value = "SELECT c.id AS id, c.name AS name, c.surname as surname, a.balance AS balance, " +
"t.id AS transactionId, t.description AS description, t.date AS date " +
"FROM Customer c " +
"LEFT JOIN Account a ON a.customer.id = c.id " +
"LEFT JOIN Transaction t ON a.id = t.account.id " +
"ORDER BY c.id")
List<CustomerDetailsResponse> findAllWithDetails();
Here is the returned result:
|id |name |surname |balance |t_id |date |
|---|-----------|-----------|-----------|-------|------------------------
|1 |Thorsten |Canto |100 |1 |2023-02-19 20:47:18.212|
|1 |Thorsten |Canto |200 |2 |2023-02-19 20:47:21.425|
|1 |Thorsten |Canto |300 |3 |2023-02-19 20:47:23.296|
|2 |Tailor |Bingham |300 |4 |2023-02-19 20:47:25.350|
|2 |Tailor |Bingham |500 |5 |2023-02-19 20:47:38.589|
|2 |Tailor |Bingham |600 |6 |2023-02-19 20:47:40.916|
|3 |Benson |Wilkowski |600 |7 |2023-02-19 20:47:42.675|
|3 |Benson |Wilkowski |800 |8 |2023-02-19 20:47:44.635|
|3 |Benson |Wilkowski |900 |9 |2023-02-19 20:47:47.436|
|3 |Benson |Wilkowski |1000 |10 |2023-02-19 20:47:51.328|
|4 |Pryce |Gorriessen | | | |
|5 |Melita |Griffoen | | | |
So, how can I achieve this by using Projection or ModelMapper or Java Stream?

If you don't want all the transactions, but just the one associated to an account, I think u just have to substitute the left join with an inner join.
#Query(value = "SELECT c.id AS id, c.name AS name, c.surname as surname, a.balance AS balance, " +
"t.id AS transactionId, t.description AS description, t.date AS date " +
"FROM Customer c " +
"LEFT JOIN Account a ON a.customer.id = c.id " +
"JOIN Transaction t ON a.id = t.account.id " +
"ORDER BY c.id")
List<CustomerDetailsResponse> findAllWithDetails();

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.

Optimization: Getting SQL Query out of loop

I have two tables with a 1 to n relationship.
table hobbies has a foreign key to table users called "userid"
SELECT * FROM hobbies WHERE userid = 7
can have multiple results.
table users contains a list of user profile data and table hobbies contains a list of user hobbies. I want to print a list of profile info of multiple users with hobbies of each user. (hobbies concatenated into one String)
i currently have the following sql queries: (pseudocode):
ResultSet result = executeQuery("SELECT * FROM users;");
for each returned row: executeQuery("SELECT * FROM hobbies WHERE userid =" + result.getInt("ref"));
how do I get the SQL queries out of the loop to optimize performance with a big list of users?
I was trying to do a LEFT JOIN and then doing the WHERE ref= check in java instead of in SQL
problem is that I then get duplicate users and i only want to print one row for each user when processing the result set
also Im not sure if the JOIN is really an improvement in performance, because I have to process more rows.
table users
+--------+------+---------+--------+
| userid | name | country | telno |
+--------+------+---------+--------+
| 1 | John | USA | 123456 |
| 2 | Max | Germany | 345678 |
+--------+------+---------+--------+
+--------------+------------+
| userid |hobby |
+--------------+------------+
| 1 | football |
| 1 | basketball |
| 2 | TV |
| 2 | Music |
| 2 | football |
+--------------+------------+
example output:
John, USA, 123456, {football, basketball}
Max, Germany, 345678, {TV, Music, football}
This is most probably the fastest solution
SELECT name, country, telNo, GROUP_CONCAT(hobby)
FROM users u LEFT JOIN hobbies h ON u.id = h.userId
GROUP BY name, country, telNo
See https://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat for formatting options.
It's only good for the final output, e.g., if your hobbies contain commas, you wont't be able to parse them uniquely.
You can try :
SELECT h.userid, h.allyouneed FROM hobbies h, users u WHERE h.userid =u.userid
and get the info in one query
"SELECT * FROM hobbies h join users u on h.userid = u.userid WHERE userid = ?"
preparedStatement.setInt(result.getInt("ref"));
You can then save all the hobbies to a list similar to the 'Book' example below.
public static List<String> selectAll() throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
// THE LIST OF BOOKS YOU WILL RETURN
List<String> books = new ArrayList<>();
String sql = "SELECT BOOK FROM BOOKs";
try(Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bookshop", "root", "");){
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while(rs.next()){
String book= rs.getString ("BOOK");
// ADD BOOK TO THE LIST
books.add(book);
}
}
finally{
ps.close();
rs.close();
}
// RETURN LIST OF ALL BOOKS
return books;
}
SELECT u.userid,u.name,u.country GROUP_CONCAT(hobby SEPARATOR ',')
FROM hobbies as h join users as u on h.userid=u.userid
where h.userid = ? GROUP BY u.userid,u.name,u.country;
assumming you have relationship on hobbies to users. the query will concat the columns of hobby of the join tables, and there will be one unique user per row.

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 Join Mapping

I have two db tables:
TARGET_TABLE (composite key on USER_ID and TARGET_ID)
USER_ID | TICKET_NO | TARGET_USER
---------------------------------
A11 | 12345 | A22
A11 | 12346 | A33
A44 | 12347 | A55
USER_DETAILS_TABLE
CORP_ID | USER_NAME
------------------
A11 | Steve
A22 | Jon
A33 | Paul
A44 | Dave
A55 | James
I want to be able to join these tables when I'm using select statements only.
For example I would like to do:
Select USER_ID, USER_NAME, TICKET_NO FROM TARGET_TABLE INNER JOIN USER_DETAILS ON TARGET_TABLE.USER_ID = USER_DETAILS_TABLE.CORP_ID
I can't seem to find the best way to do this. I have looked at the Hibernate examples for mapping but these are examples on how to write to both tables I simply want to get a user name from a table I can't touch!
I currently have two separate mappings for each table and run a separate query to get the user name but this doesn't seem like the best way to do it.
This HQL would work select tt.userId, tt.ticketNo, u.userName from TargetTable tt, User u where tt.userId = u.corpId.
However, this would return a List<Object[]> where each list element represents one row with 3 columns. You could extract these manually, or for example have some my.package.UserVO object with constructor public UserVO(String userId, String ticketNo, String userName) { ... }. In that case, this
session.createQuery("select new my.package.UserVO(tt.userId, tt.ticketNo, u.userName) from TargetTable tt, User u where tt.userId = u.corpId", UserVO.class).list()
would return instances of UserVO.

problem with mapping and created tables when using playframework

I tried to create a webapp consisting of shoppingcart and cartitems using playframework.I created the following mappings and when I tried to run the webapp,I found that the postgres db tables created does not have the values ,which creates a bidirectional association.
#Entity
public class CartItem extends Model implements Comparable<CartItem>
#OneToOne
public Product pdt;
#ManyToOne
public ShoppingCart cart;
public int quantity;
...
}
#Entity
public class ShoppingCart extends Model {
#OneToOne
public MyUser customer;
#OneToMany(mappedBy="cart", cascade=CascadeType.ALL)
public Set<CartItem> cartItems;
public ShoppingCart(MyUser customer) {
super();
this.customer = customer;
this.cartItems = new TreeSet<CartItem>();
}
...
}
When I added the cartitem to cart,
public static void addItemToCart(Long productId,Long cartId,String quantity) {
Product pdt = Product.findById(productId);
ShoppingCart cart = ShoppingCart.findById(cartId);
int qty = Integer.parseInt(quantity);
System.out.println("create cartitem from "+qty +" copies of product="+pdt.getName()+" for user="+cart.getCustomer().getEmail());
CartItem cartItem = new CartItem(pdt,qty);
cart.addItem(cartItem);
cart.save();
redirect("/");
}
When this method was executed during post, the println() statement produced
create cartitem from 3 copies of product=Product1 for user=jon#gmail.com
The tables created showed this data
select * from shoppingcart ;
id | customer_id
-----+-------------
191 | 151
(1 row)
select * from cartitem ;
id | quantity | product_id | cart_id
-----+----------+------------+---------
192 | 3 | 168 |
(1 row)
The cart_id column has no value.Is there some problem in the way I defined my mappings? Can someone help me resolve this?
below is the table schema as given by \d in psql
\d cartitem
Table "public.cartitem"
Column | Type | Modifiers
----------+---------+-----------
id | bigint | not null
quantity | integer | not null
product_id | bigint |
cart_id | bigint |
Indexes:
"cartitem_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"fk4393e7353ab523e" FOREIGN KEY (product_id) REFERENCES product(id)
"fk4393e7355396066" FOREIGN KEY (cart_id) REFERENCES shoppingcart(id)
update:
I made this work by
cartItem.setCart(cart);//why do I have to do this?
Now,after I save the cart, the cartitem table has
select * from cartitem;
id | quantity | product_id | cart_id
-----+----------+------------+---------
197 | 4 | 164 | 196
(1 row)
So,I think the bidirectional association doesn't work..
Does someone know why?
Right after
cart.addItem(cartItem);
Try adding
cartItem.cart = cart;
cartItem.save();
because of efficiency,if your ShoppingCart have hundreds of CartItem,every time you invoke save,you must traverse all of them to find which are update,which are add!

Categories