Hierarchical Data Fetch in Spring and Hibernate - spring

I have 2 tables Account and Group both contain data in a hierarchy.
Example -
(Just for reference I am using PostgresSQL)
Group
|------|----------|-------------------|
| id | name | parent_group_id |
|------|----------|-------------------|
| 1 | Group1 | null |
| 2 | Group2 | 1 |
| 3 | Group3 | 2 |
| 4 | Group4 | 1 |
|------|----------|-------------------|
Account
|----|----------|----------|
| id | name | group_id |
|----|----------|----------|
| 1 | Account1 | 1 |
| 2 | Account2 | 1 |
| 3 | Account3 | 2 |
| 4 | Account4 | 3 |
| 4 | Account5 | 4 |
-----|----------|-----------
This account and group hierarchy can be many levels deep. I want to fetch all groups and accounts in an efficient way using Spring and Hibernate.
I want the output to be like -
{"name":"Group1","groups":[{"name":"Group4","groups":[],"accounts":[{"name":"Account5"}]},{"name":"Group2","groups":[{"name":"Group3","groups":[],"accounts":[{"name":"Account4"}]}],"accounts":[{"name":"Account3"}]}],"accounts":[{"name":"Account2"},{"name":"Account1"}]}
I have checked some articles but they are not recursive (means group inside a group and so on).

This is the perfect use case for Blaze-Persistence.
Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. To model CTEs or recursive CTEs, which is what you need here, you first need to introduce a CTE entity that models the result type of the CTE.
#CTE
#Entity
public class GroupCTE {
#Id Integer id;
}
A query for this could look like the following
List<Group> groups = criteriaBuilderFactory.create(entityManager, Group.class)
.withRecursive(GroupCTE.class)
.from(Group.class, "g1")
.bind("id").select("g1.id")
.where("g1.parent").isNull()
.unionAll()
.from(Group.class, "g2")
.innerJoinOn(GroupCTE.class, "cte")
.on("cte.id").eqExpression("g2.parent.id")
.end()
.bind("id").select("g2.id")
.end()
.from(Group.class, "g")
.fetch("accounts", "groups")
.where("g.id").in()
.from(GroupCTE.class, "c")
.select("c.id")
.end()
.getResultList();
This renders to SQL looking like the following
WITH RECURSIVE GroupCTE(id) AS (
SELECT g1.id
FROM Group g1
WHERE g1.parent_group_id IS NULL
UNION ALL
SELECT g2.id
FROM Group g2
INNER JOIN GroupCTE cte ON g2.parent_group_id = cte.id
)
SELECT *
FROM Group g
LEFT JOIN Account a ON a.group_id = g.id
LEFT JOIN Group gsub ON gsub.parent_group_id = g.id
WHERE g.id IN (
SELECT c.id
FROM GroupCTE c
)
You can find out more about recursive CTEs in the documentation: https://persistence.blazebit.com/documentation/core/manual/en_US/index.html#recursive-ctes

Related

TABLE ACCESS FULL in Oracle execution plan

I have been tasked to find out the SELECT statement for an explain plan
------------------------------------------
| Id | Operation | Name |
------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | HASH JOIN RIGHT ANTI | |
| 2 | VIEW | VW_NSO_1 |
| 3 | HASH JOIN RIGHT SEMI| |
| 4 | TABLE ACCESS FULL | PART |
| 5 | TABLE ACCESS FULL | ORDERS |
| 6 | TABLE ACCESS FULL | CUSTOMER |
------------------------------------------
I am able to find the select statement from Id 0-5 but what does the line 6 mean?
This is what I have managed to figure out so far I can't get where the last sentence comes into play.
select *
from customer c join orders o
on c.custkey = o.custkey
where o_totalprice
not in
(select p_retailprice
from part p join orders o
on orders.o_custkey >= 0 and 0.1*o_totalprice >= 0)
I can't get where the last sentence comes into play?
Your query is:
select *
from customer c join orders o
on c.custkey = o.custkey
where o_totalprice
not in
(select p_retailprice
from part p join orders o
on orders.o_custkey >= 0 and 0.1*o_totalprice >= 0)
And your explain plan is
------------------------------------------
| Id | Operation | Name |
------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | HASH JOIN RIGHT ANTI | |
| 2 | VIEW | VW_NSO_1 |
| 3 | HASH JOIN RIGHT SEMI| |
| 4 | TABLE ACCESS FULL | PART |
| 5 | TABLE ACCESS FULL | ORDERS |
| 6 | TABLE ACCESS FULL | CUSTOMER |
------------------------------------------
In your case, this is what happens:
You are getting all the records from both customer and orders that match the condition based on the custkey field.
Your predicate information is delimiting the output to those where o_totalprice ( by the way it should clarified for reading easiness where this field is coming from, although I guess is from orders table ) is not part of the dataset retrieved from the subquery.
the subquery is getting all values of p_retailprice that match the join between part and orders using orders.o_custkey >= 0 and 0.1*o_totalprice >= 0
Getting this in consideration the CBO is:
Accessing ( Line 6 ) by TABLE FULL SCAN the table CUSTOMER, which is logical as you are getting all fields from the table and probably you have no index over custkey.
Making a HASH SEMI JOIN ( line 3 ) between PARTS and ORDERS. In general, a semi join is used for an in or exists clause, and the join stops as soon as the exists condition or the in condition is satisfied.
The HASH JOIN ANTI of line 1 is when the optimizer push the join predicate into a view, normally when an anti join ( not in ) is in place. This is then join to the CUSTOMER TABLE in line 6.
You are filtering only in the right table of the join ( ORDERS ) that is why the access are reflecting that.
This is just an overview of your execution plan and the reasons why the CBO is using those access paths.

Convert raw query into laravel eloquent

I have this written and working as a raw SQL query, but I am trying to convert it to a more Laravel eloquent / query builder design instead of just a raw query.
My table structure like this:
Table One (Name model)
______________
| id | name |
|------------|
| 1 | bob |
| 2 | jane |
--------------
Table Two (Date Model)
_________________________________
| id | table_1_id | date |
|-------------------------------|
| 1 | 1 | 2000-01-01 |
| 2 | 1 | 2000-01-31 |
| 4 | 1 | 2000-02-28 |
| 5 | 1 | 2000-03-03 |
| 6 | 2 | 2000-01-03 |
| 7 | 2 | 2000-01-05 |
---------------------------------
I am returning only the the highest (most recent) dates from table 2 (Dates model) that match the user bob from table 1 (Name model).
For instance, in the example above, I return this from my query
2000-01-31
2000-02-28
2000-03-03
Here is what I am doing now (which works), but i'm just not sure how to use YEAR, MONTH and MAX with laravel.
DB::select(
DB::raw("
SELECT MAX(date) as max_date
FROM table_2
INNER JOIN table_1 ON table_1.id = table_2.table_1_id
WHERE table_1.name = 'bob'
GROUP BY YEAR(date), MONTH(date)
ORDER BY max_date DESC
")
);
Try this code if any problem then,
DB::table('table_1')->join('table_2', 'table_1.id','=','table_2.table_1_id')
->select(DB::raw('MAX(date) as max_date'),DB::raw('YEAR(date) year, MONTH(date) month'),'table_1.name')
->where('name','bob')
->groupBy('year','month')
->orderBy('max_date')
->get();
If any problem with above code then feel free to ask.

Remove duplicate values from a listagg in oracle

I have used listagg to concat and list books along with the supplementary books name.
SELECT DISTINCT SUBSTR(LISTAGG(',-'||B1.BOOK_NO||','||B1.BOOK_NAME||','||A.AUTHOR_NAME||',-'||B2.BOOK_NO||','||B2.BOOK_NAME) WITHIN GROUP (ORDER BY B2.BOOK_NO),2)
FROM BOOK_LIST B1
INNER JOIN AUTHORS A ON A.AUTHOR_NO=B1.AUTHOR_NO
INNER JOIN SUPPLEMENTARY B2 ON B2.BOOK_NO = B1.BOOK_SUP_NO
WHERE B1.SEQ = 123;
But since the number of supplementary books are more i get the main book name repeatedly.
Is there a way to remove the duplicate main book name and number.
My ouput is like this
-99,Anders Carlson ,-109,John Stuart,-99,Anders Carlson ,-47,James Anderson
Here the value 99 is repeated i want only one 99.
Desired Output:
-99,Anders Carlson ,-109,John Stuart,-47,James Anderson
DB data:
Book_list:
NO | MAIN_BOOK_NO | MAIN_BOOK_NAME | BOOK_SUP_NO | AUTHOR_NO
1 | 12 | xyz | 5 | 2
2 | 22 | abc | 7 | 4
Authors:
NO | AUTHOR_NO | AUTHOR_NAME
1 | 2 | Alex
2 | 3 | Leonard
3 | 4 | Benjamin
Supplementary:
NO | BOOK_NO | BOOK_NAME
1 | 5 | ABC
2 | 5 | XYZ
3 | 7 | LMN
4 | 7 | DEF
5 | 7 | NEW
The output should be like
NAME
12,xyz,Alex,-5,ABC,-5,XYZ
22,abc,Benjamin,-7,LMN,-7,DEF,-7,NEW
Similarly for the entire data in the table
If I understand you correctly, you need to append the list of supplementary books to the main book, so you're actually after something like:
SELECT B1.MAIN_BOOK_NO||','||B1.MAIN_BOOK_NAME||',-'||
LISTAGG(B2.BOOK_NO||','||B2.BOOK_NAME, ',-') WITHIN GROUP (ORDER BY B2.BOOK_NO)
FROM BOOK_LIST B1
INNER JOIN AUTHORS A ON A.AUTHOR_NO=B1.AUTHOR_NO
INNER JOIN SUPPLEMENTARY B2 ON B2.BOOK_NO = B1.BOOK_SUP_NO
WHERE B1.SEQ = 123
GROUP BY B1.MAIN_BOOK_NO, B1.MAIN_BOOK_NAME;
See if this works
select T1.MAIN_BOOK_NO, T11.MAIN_BOOK_NAME, LISTAGG(',-'||',-'||T1.BOOK_NO||','||T1.BOOK_NAME) WITHIN GROUP (order by T1.BOOK_NO)
from
(
SELECT B1.MAIN_BOOK_NO, B1.MAIN_BOOK_NAME, B2.BOOK_NO, B2.BOOK_NAME
FROM BOOK_LIST B1
INNER JOIN AUTHORS A ON A.AUTHOR_NO=B1.AUTHOR_NO
INNER JOIN SUPPLEMENTARY B2 ON B2.BOOK_NO = B1.BOOK_SUP_NO
WHERE B1.SEQ = 123
group by B1.MAIN_BOOK_NO, B1.MAIN_BOOK_NAME, B2.BOOK_NO, B2.BOOK_NAME
order by B2.BOOK_NO
) T1
group by T1.MAIN_BOOK_NO, T1.MAIN_BOOK_NAME;

List customer ID, name and all of his/her accounts

customers:
+------------+--------------+
| cid | Name |
+------------+--------------+
| 1 | Bob |
| 2 | John |
| 3 | Jane |
+------------+--------------+
accounts:
+------------+--------------+
| aid | type |
+------------+--------------+
| 1 | Checking |
| 2 | Saving |
| 3 | CD |
+------------+--------------+
transactions:
+------------+--------------+--------------+
| tid | cid | aid |
+------------+--------------+--------------+
| 1 | 1 | 1 |
| 2 | 2 | 1 |
| 3 | 1 | 2 |
| 4 | 2 | 3 |
| 5 | 3 | 1 |
+------------+--------------+--------------+
I am trying to write a plsql procedure that, given the customer id as a parameter, will display his/her id, name and all accounts. Displaying the id and name is simple enough. What I'm not sure about is how to get all the accounts that are linked to the customer id and how to retrieve more than a single account.
An ideea can be:
select c.cid, c.name, a.type
from customers c
left join transactions t on (t.cid = c.cid)
left join accounts a on (a.aid = t.aid)
where c.cid = :customer_id
group by c.cid, c.name, a.type;
the group by is needed because can be more transactions.
Further, if you want to see one line:
select cid, name, LISTAGG(type, ',') WITHIN GROUP (ORDER BY type) as account_types
from(
select distinct c.cid, c.name, a.type
from customers c
left join transactions t on (t.cid = c.cid)
left join accounts a on (a.aid = t.aid)
where c.cid = :customer_id
)
group by cid, name;
Putting this into a stored procedure/function is too simple, so I let it to you.

Sum of the grouped distinct values

This is a bit hard to explain in words ... I'm trying to calculate a sum of grouped distinct values in a matrix. Let's say I have the following data returned by a SQL query:
------------------------------------------------
| Group | ParentID | ChildID | ParentProdCount |
| A | 1 | 1 | 2 |
| A | 1 | 2 | 2 |
| A | 1 | 3 | 2 |
| A | 1 | 4 | 2 |
| A | 2 | 5 | 3 |
| A | 2 | 6 | 3 |
| A | 2 | 7 | 3 |
| A | 2 | 8 | 3 |
| B | 3 | 9 | 1 |
| B | 3 | 10 | 1 |
| B | 3 | 11 | 1 |
------------------------------------------------
There's some other data in the query, but it's irrelevant. ParentProdCount is specific to the ParentID.
Now, I have a matrix in the MS Report Designer in which I'm trying to calculate a sum for ParentProdCount (grouped by "Group"). If I just add the expression
=Sum(Fields!ParentProdCount.Value)
I get a result 20 for Group A and 3 for Group B, which is incorrect. The correct values should be 5 for group A and 1 for group B. This wouldn't happen if there wasn't ChildID involved, but I have to use some other child-specific data in the same matrix.
I tried to nest FIRST() and SUM() aggregate functions but apparently it's not possible to have nested aggregation functions, even when they have scopes defined.
I'm pretty sure there is some way to calculate the grouped distinct sum without needing to create another SQL query. Anyone got an idea how to do that?
Ok I got this sorted out by adding a ROW_NUMBER() function my SQL query:
SELECT Group, ParentID, ROW_NUMBER() OVER (PARTITION BY ParentID ORDER BY ChildID ASC) AS Position, ChildID, ParentProdCount FROM Table
and then I replaced the SSRS SUM function with
=SUM(IIF(Position = 1, ParentProdCount.Value, 0))
Put a grouping over the ParentID and use a summation over that group,
eg:
if group over ParentID = "ParentIDGroup"
then
column sum of ParentPrdCount = SUM(Fields!ParentProdCount.Value,"ParentIDGroup")

Resources