I'm getting what I think are needless CROSS JOINs when I'm doing a select IN SUBQUERY, which is hurting performance. I'm using Postgres if that makes a difference.
I'm aiming to generate the following query
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
where (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
But I get
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
cross join author a2 where b.author_id = a2.id -- <<< I don't want this cross join!
and (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
Code
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> cq = cb.createQuery(Author.class);
Root<Author> authorRoot = cq.from(Author.class);
Subquery<Long> countSubquery = cq.subquery(Long.class);
Root<Book> bookRoot = countSubquery.from(Book.class);
Expression<Long> count = cb.count(bookRoot.get(Book_.author));
countSubquery.select(bookRoot.get(Book_.AUTHOR))
.distinct(true)
.where(cb.between(bookRoot.get(Book_.publishedOn),
LocalDate.of(2021, MARCH, 1),
LocalDate.of(2021, MARCH, 31)))
.groupBy(bookRoot.get(Book_.author))
.having(cb.greaterThanOrEqualTo(count, 2L));
cq.where(
cb.equal(authorRoot.get(Author_.lastName), "Smith"),
cb.in(authorRoot.get(Author_.ID)).value(countSubquery));
cq.select(authorRoot.get(Author_.FIRST_NAME));
TypedQuery<String> query = entityManager.createQuery(cq);
return query.getResultList();
In reality I'm generating the queries from a user driven query builder, this code recreates the exact problem I'm having.
When using the query builder the user could end up with multiple select in subqueries so I need this to perform as well as possible.
I don't see why I should need any join/cross join for my query to work.
Entities
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Book> books;
}
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "author_id")
private Author author;
private LocalDate publishedOn;
}
This expression: bookRoot.get(Book_.author) means you're joining Author to Book implicitly.
To get rid of the extra join, you would have to either use a native query, or map Book.author_id once more as a simple column:
#Column(name = "author_id", insertable = false, updatable = false)
private Long authorId;
And use Book_.authorId instead.
Related
How to join a table with a selection using CriteriaBuilder?
Suppose I have this query:
SELECT
tbl1.*,
tbl2.total
FROM
table_1 tbl1
LEFT JOIN
(SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id) tbl2
ON
tbl1.id = tbl2.id;
Where the definition of table_1 is:
CREATE TABLE table_1(
id NUMBER(19, 0),
-- other columns
)
... and table_2 ...
CREATE TABLE table_2(
id NUMBER(19, 0),
col_id NUMBER(19, 0),
value NUMBER(14, 2),
-- other columns
FOREING KEY (col_id) REFERENCES table_1(id);
)
This is not possible with plain JPA or Hibernate. With Hibernate you can model this query if the subquery is static, but for dynamic subqueries you will need something like Blaze-Persistence which works on top of JPA/Hibernate and provides support for these things.
For the static query solution you can do this:
#Entity
#Table(name = "table1")
public class Table1 {
#Column(name = "id")
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id", insertable = false, updatable = false)
private Table2 table2;
}
#Entity
#Subselect("SELECT col_id AS id, SUM(value) AS total FROM table_2 WHERE active = 1 GROUP BY col_id")
public class Table2Query {
#Column(name = "id")
private Integer id;
#Column(name = "total")
private BigDecimal total;
}
Here is a nice article by Vlad Mihalcea about Blaze-Persistence if you want a dynamic solution i.e. where the query structure isn't fixed: https://vladmihalcea.com/blaze-persistence-jpa-criteria-queries/
Use the join method in root and use it to get the values from the other table.
Note: you need to add the relation in the entity depending on the relationship of these tables (onetone, manytoone or onetomany). Something like this:
Entity code Table1:
#OneToOne
private Table2 table2;
Search code example:
(Root<Table1> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
Join<Table1, Table2> joinTable2 = root.join("table2");
cb.equal(joinTable2.get("active"), 1);
.. other filters ..
};
The below code snippet does not work:
#Id
private Long id;
#ManyToMany(fetch = FetchType.LAZY)
#JoinFormula("""
(SELECT DISTINCT ON (product11.id, text26.language, textsourced28.source) text26.id
FROM product AS product11
JOIN producttexttitle AS producttexttitle27 ON product11.id = producttexttitle27.product
JOIN text AS text26 ON producttexttitle27.text = text26.id
JOIN textsourced AS textsourced28 ON text26.id = textsourced28.text
WHERE product11.id=id
ORDER BY product11.id, text26.language, textsourced28.source, textsourced28.time DESC)
""")
private List<Text> titles;
However this does,
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinFormula("""
(SELECT DISTINCT ON (product11.id, text26.language, textsourced28.source) text26.id
FROM product AS product11
JOIN producttexttitle AS producttexttitle27 ON product11.id = producttexttitle27.product
JOIN text AS text26 ON producttexttitle27.text = text26.id
JOIN textsourced AS textsourced28 ON text26.id = textsourced28.text
WHERE product11.id=id
ORDER BY product11.id, text26.language, textsourced28.source, textsourced28.time DESC
LIMIT 1)
""")
private Text titles;
The only problem is that the latter one only returns one instance, where I am looking for several.
Can figure it out.
#CollectionTable or #ElementCollection usage perhaps?
I need to supply a custom sql.
I am using spring with hibernate to store data in MySql database. I am trying to retrieve rows based on filters requested by the user.
I have the following tables/entities : Product and Gemstone
Relations:
Product many2many Gemstone
I am trying to write a query to get products that have Gemstone A and Gemstone B and Gemstone C.. and so on.
Use Case:
If user is asking for a product with gemstones 51 and 46. Query should only return product id 4.
Query:
filterGemstones() method return the gemstone user wants to filter products to. Using the below query I get zero records but if I remove HAVING Count(DISTINCT p.product_id) = 2 I get product id 4, 5
HQL :
createQuery("select p.productId from Product p JOIN p.gemstones g where g in :gemstones group by p having count (distinct p) =" + filterGemstones().size() ).setParameter("gemstones",filterGemstones());
SQL generate by hibernate :
SELECT p.product_id
FROM product p
INNER JOIN gemstone_product gp
ON p.product_id = gp.product_id
INNER JOIN gemstone g
ON gp.gemstone_id = g.gemstone_id
WHERE g.gemstone_id IN ( 51, 46 )
GROUP BY p.product_id
HAVING Count(DISTINCT p.product_id) = 2
Product class:
#Entity
#Table(name = "product")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "product_id")
private long productId;
#ManyToMany()
#JoinTable(
name = "gemstone_product",
joinColumns = {#JoinColumn(name = "product_id")},
inverseJoinColumns = {#JoinColumn(name = "gemstone_id")}
)
private Set<Gemstone> gemstones = new HashSet<>(0);
// setters and getters
}
Gemstone class:
#Entity
#Table(name = "gemstone")
public class Gemstone {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "gemstone_id")
private long gemstoneId;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "gemstone_product",
joinColumns = {#JoinColumn(name = "gemstone_id")},
inverseJoinColumns = {#JoinColumn(name = "product_id")}
)
private Set<Product> products = new HashSet<>(0);
// setters and getters
}
Actually the SQL query that we need here is pretty simple:
SELECT t1.product_id
FROM gemstone_product AS t1
WHERE (t1.gemstone_id IN ?1 ) # (51, 46)
GROUP BY t1.product_id
HAVING (COUNT(t1.gemstone_id) = ?2) # 2 - # of items
It's a bit frustrating that it's not easy to create it with JPA, but it can be done with FluentJPA (produces the query above):
public List<Integer> getProductsContainingAllStones(List<Long> gemstoneIds) {
int count = gemstoneIds.size();
FluentQuery query = FluentJPA.SQL((Gemstone gemstone,
JoinTable<Gemstone, Product> gemstoneProduct) -> {
discardSQL(gemstoneProduct.join(gemstone, Gemstone::getProducts));
long productId = gemstoneProduct.getInverseJoined().getProductId();
long gemstoneId = gemstoneProduct.getJoined().getGemstoneId();
SELECT(productId);
FROM(gemstoneProduct);
WHERE(gemstoneIds.contains(gemstoneId));
GROUP(BY(productId));
HAVING(COUNT(gemstoneId) == count);
});
return query.createQuery(em).getResultList();
}
More details on how it works can be found here.
Good evening,
Let my pojo Athor be:
public class Author {
private long id;
private String name;
#JoinColumn(name = "id", nullable = false)
private Editor editor;
}
I'd like to know if is there any difference, like performance, executions, etc between this two JPQL queries using Hibernate:
select a.name, b.name from Author a inner join a.editor e where a.id = 1;
and
select a.name, a.editor.name from Athor a where a.id = 1;
Thanks!
Here Orders has customer, I want to get customer table along with count of orders for each record.
how to left join order table here? if customer has one to many relationship with order.
'*
Class customer{
#Id
#Column(name = "id")
int id;
#Column(name = "id")
String name;
#oneToMany
List<Order> orders;
}
*'
' *
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<CustomerView> q = cb.createQuery(CustomerView.class);
Root<Customer> root = q.from(Customer.class);
q.select((Selection<? extends CustomerView>)
Root<Order> orderRoot = q.from(Geofence.class) cb.tuple(root,cb.count(orderRoot.get("customer")).alias("orderCount")));
q.where(cb.equal(root.get("id"), orderRoot.get("customer")));
q.groupBy(root.get("id"));
TypedQuery<CustomerView> query = em.createQuery(q);
List<CustomerView> results = query.getResultList();
*'