JPA CriteriaQuery Generated SQL with hardcoded parameter on nested entity id - java

I've been studying SpringData JPA and came across with this weird behavior when using CriteriaQuery to find a given entity by its child entity's Id, I've noticed a hardcoded child_id parameter in the generated SQL:
Hibernate: select parent0_.parent_id as parent_i1_4_, parent0_.parent_name as parent_n2_4_, parent0_.parent_type_parent_type_id as parent_t3_4_ from parent parent0_ inner join child children1_ on parent0_.parent_id=children1_.parent_parent_id where children1_.child_id=1
Hibernate: select parent0_.parent_id as parent_i1_4_, parent0_.parent_name as parent_n2_4_, parent0_.parent_type_parent_type_id as parent_t3_4_ from parent parent0_ inner join child children1_ on parent0_.parent_id=children1_.parent_parent_id where children1_.child_id=?
The java code:
Parent
#Data
#Entity
public class Parent {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer parentId;
private String parentName;
#OneToMany(mappedBy = "parent")
private List<Child> children;
}
Child
#Builder
#Data
#Entity
public class Child {
#Id
private Integer childId;
private String childName;
#ManyToOne
private Parent parent;
}
ParentDAO
#Repository
public class ParentDAO {
private EntityManager em;
public ParentDAO(EntityManager em) {
this.em = em;
}
public List<Parent> findByChildId(Integer childId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Parent> cq = cb.createQuery(Parent.class);
Root<Parent> root = cq.from(Parent.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.join("children").get("childId"), childId));
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
public List<Parent> findByChild(Child child) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Parent> cq = cb.createQuery(Parent.class);
Root<Parent> root = cq.from(Parent.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.join("children"), child));
cq.where(predicates.toArray(new Predicate[0]));
return em.createQuery(cq).getResultList();
}
}
SpringDataApplication
#SpringBootApplication
public class SpringDataApplication implements CommandLineRunner {
private ParentDAO parentDAO;
public SpringDataApplication(ParentDAO parentDAO) {
this.parentDAO = parentDAO;
}
public static void main(String[] args) {
SpringApplication.run(SpringDataApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
parentDAO.findByChildId(1);
parentDAO.findByChild(Child.builder().childId(1).build());
}
}
It's not a big deal since the goal could be achieved with the findByChild method, I'm just curious about this situation. Best regards!

Because Strings can contain SQL and Integers cannot, there is no need to bind from a security aspect point of view (SQL injection).
From hibernate documentation of literal_handling_mode:
This enum defines how literals are handled by JPA Criteria. By default (AUTO), Criteria queries uses bind parameters for any literal that is not a numeric value. However, to increase the likelihood of JDBC statement caching, you might want to use bind parameters for numeric values too. The BIND mode will use bind variables for any literal value. The INLINE mode will inline literal values as-is. To prevent SQL injection, never use INLINE with String variables. Always use constants with the INLINE mode.
In issue HHH-9576 a new parameter was added to fix this issue, applicable since version 5.2.12
<property name="hibernate.criteria.literal_handling_mode" value="bind"/>
or in spring boot application.properties you can use
spring.jpa.properties.hibernate.criteria.literal_handling_mode=bind
After adding configuration your both query will look the same with the bind parameter.
select parent0_.parent_id as parent_i1_2_, parent0_.parent_name as parent_n2_2_ from parent parent0_ inner join child children1_ on parent0_.parent_id=children1_.parent_parent_id where children1_.child_id=?
binding parameter [1] as [INTEGER] - [1]
select parent0_.parent_id as parent_i1_2_, parent0_.parent_name as parent_n2_2_ from parent parent0_ inner join child children1_ on parent0_.parent_id=children1_.parent_parent_id where children1_.child_id=?
binding parameter [1] as [INTEGER] - [1]

Related

JPA Criteria: QuerySyntaxException for left join on treat entity

Model:
#Entity
public class User {
#Id
private Integer id;
#JoinColumn(name = "user_id")
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Project> projects;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "Type")
public abstract class Project {
#Id
private Integer id;
private String name;
}
#Entity
#DiscriminatorValue("Administrative")
public class AdminProject extends Project {
private String departmentName;
}
#Entity
#DiscriminatorValue("Design")
public class DesignProject extends Project {
private String companyName;
}
I am trying to use JPA's criteria api to query for User entities based on an attribute of an implementation of Project. For example, query all users that have a project with "SOME_NAME" department (that field does not exist on DesignProject).
I see there is a way of doing so via downcasting of the Project entity for the query. I am trying something similar to:
CriteriaBuilder cb...
Root<User> userRoot...
root = ((From) root).join("projects", JoinType.LEFT);
root = cb.treat(root, AdminProject.class);
root = root.get("departmentName");
Exception:
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.departmentName' [select generatedAlias0 from io.github.perplexhub.rsql.model.User as generatedAlias0 left join generatedAlias0.projects as generatedAlias1 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0]; nested exception is java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.departmentName' [select generatedAlias0 from io.github.perplexhub.rsql.model.User as generatedAlias0 left join generatedAlias0.projects as generatedAlias1 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0]
What am I missing? Is it something related to the join, or how the downcasting occurs afterwards?
Edit
After the answer by #K.Nicholas, I have managed to make the query work on an isolated scenario, but not on my app.
But, I noticed that the entityManager.createQuery(query) call throws the exception above when called for the first time, and it works if I call it again without changing the query object. Here is the query generated on the second call (this query finds the objects I want from the database):
select generatedAlias0 from User as generatedAlias0 left join generatedAlias0.projects as generatedAlias2 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0
Why is the entity manager creating two different queries when called two consecutive times?
I would do the Entitys a little different, as you will see. The main concern is that you are using User as your root with a join to a list of Projects. This is a concern because you should have the foreign key on the Project class and use the projects field as a query only field. That is what I have done. It works better that way. It is also a concern because you have to do a join fetch instead of a join so that the projects get fetched along with the users.
So, first, the entities are like so:
#Entity
public class User {
#Id
private Integer id;
#OneToMany(mappedBy="user")
private List<Project> projects;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "Type")
public abstract class Project {
#Id
private Integer id;
private String name;
#ManyToOne
private User user;
}
#Entity
#DiscriminatorValue("Administrative")
public class AdminProject extends Project {
private String departmentName;
}
#Entity
#DiscriminatorValue("Design")
public class DesignProject extends Project {
private String companyName;
}
After a bit a digging I found a JPQL query that does the trick. This was a starting point:
List<User> users = entityManager.createQuery("select distinct(u) from User u join fetch u.projects p where TYPE(p) = 'Administrative' and p.departmentName = 'dept1'", User.class).getResultList();
After a bit more digging I found that the treat worked fine if you do it correctly and that with JPA 2.1 you should use an EntityGraph do get the join to do a fetch.
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
Root<User> root = query.from(User.class);
Join<User, Project> join = root.join("projects");
query.select(root).where(builder.equal(builder.treat(join, AdminProject.class).get("departmentName"), "dept1"));
EntityGraph<User> fetchGraph = entityManager.createEntityGraph(User.class);
fetchGraph.addSubgraph("projects");
users = entityManager.createQuery(query.distinct(true)).setHint("javax.persistence.loadgraph", fetchGraph).getResultList();
As a side note the queries generated as slightly different but I didn't look that closely at them. You should.

Hibernate Criteria: projection with joined entity

I'm trying to create a query with Criteria, but I don't succeed to map data from a joined entity.
With this Criteria query the id of the Order entity is override with the id of the ShippingCondition entity :
final Criteria criteria = session.createCriteria(Order.class, "o")
.createAlias("o.shippingCondition", "sc", JoinType.INNER_JOIN)
.setProjection(Projections.projectionList()
.add(Projections.property("o.id"), "id")
.add(Projections.property("o.orderNum"), "orderNum")
.add(Projections.property("o.notes"), "notes")
.add(Projections.property("sc.id"), "id"))
.add(Restrictions.eq("o.id", id))
.setResultTransformer(Transformers.aliasToBean(Order.class));
return (Order) criteria.uniqueResult();
My entities :
#Table(name = "order", schema = "myschema")
public class Order {
private Integer id;
private String orderNum;
private String notes;
private ShippingCondition shippingCondition;
...
}
#Table(name = "shipping_condition", schema = "myschema")
public class ShippingCondition {
private Integer id;
private String shippingCondition;
private Integer sorting;
...
}
I have tryed to replace .add(Projections.property("sc.id"), "id") by .add(Projections.property("sc.id"), "shippingCondition.id") but then I get a ClassCastException (java.lang.ClassCastException: entity.Order cannot be cast to java.util.Map)
Do you have any idea how I can do that ?
Thanks
Hibernate doesn't support nested projections. You will need to create DTO for that. You can extend DTO from Order class and add methods to set fields of ShippingCondition.
class OrderDto extends Order {
public OrderDto() {
setShippingCondition(new ShippingCondition());
}
public void setShippingConditionId(Integer id) {
getShippingCondition().setId(id);
}
}
You can use a special nested transformer if you don't want to use DTO
How to transform a flat result set using Hibernate
Additional notes
JPA doesn't support any transformers at all. And it is hard to implement such transformer by consistent way. For example, my transformer doesn't support child collections like #OneToMany, only single associations. Also, you can't use nested projections with HQL, because HQL doesn't support parent.child aliases.

QueryDSL build a query with subclass

I am building a query in QueryDSL. I have entity and sub class entity having same column. I wanna use same query to both entity using only single JPAQuery.
here is my entity.
#Entity
public class Region {
#Id
private Integer id;
}
#Entity
public class RegionTemp extends Region {}
queryer
#Component
public class RegionQueryer {
#PersistenceContext
private EntityManager mysqlEntityManager;
QRegion qRegion = QRegion.region; // ???
public Integer loadLastId() {
return new JPAQueryFactory(mysqlEntityManager)
.select(qRegion.id)
.from(qRegion)
.orderBy(qRegion.id.desc()).fetchFirst();
}
}
My code. this sample. if you want to using single query. use simple Repository. it take easy find, delete, save. you search JPA Tutorial.
#Override
public List<CompanyInformaion> findCompanyInformationList(String language, Association association) {
QCompanyInformaion qCompanyInformaion = QCompanyInformaion.companyInformaion;
QCompany qCompany = QCompany.company;
EntityManager em = entityManagerFactory.createEntityManager();
JPAQuery jpaQuery = new JPAQuery(em);
List<CompanyInformaion> infos = jpaQuery.from(qCompanyInformaion)
.where(qCompanyInformaion.language.eq(language)
.and(qCompanyInformaion.company.in(new JPASubQuery().from(qCompany)
.where(qCompany.association.eq(association)).list(qCompany))))
.orderBy(qCompanyInformaion.companyName.asc()).list(qCompanyInformaion);
return infos;
}

Missing rows when ordering by nested field using Criteria Api

I've encounterd weird behaviour of JPA (provider: EclipseLink) using order by functionality. I have TransactionData class, which has reference to CustomerData class:
#Entity
public class TransactionData {
//...
#ManyToOne
#JoinColumn(name = "CUSTOMER_ID")
private CustomerData customer;
//...
}
#Entity
public class CustomerData {
//...
#Column(name = "LAST_NAME")
private String lastName;
//...
}
In my project, there are some specific cases, where there are transactions, which are not assigned to any customer (called non-customer transactions).
I try to get list of all registered transactions (both cusotmer transactions and non-customer transactions) and sort them by customer's last name. To acheive this I've written following Criteria Api
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<TransactionData> criteriaQuery = criteriaBuilder.createQuery(TransactionData.class);
Root<TransactionData> from = criteriaQuery.from(TransactionData.class);
criteriaQuery.orderBy(criteriaBuilder.asc(from.get("customer").get("lastName"));
TypedQuery<TransactionData> query = entityManager.createQuery(criteriaQuery);
return query.getResultList();
I think I should get list of all transactions, of course, with those, where customer field is set to NULL value. But JPA behaviour is different, because it cut out all transactions, where reference to customer is empty.
from.get("customer").get("lastName") will do implicitly an INNER JOIN. If some transactions have no customer assigned then what you need is a LEFT JOIN:
Join<TransactionData , CustomerData> customer = from.join("customer", JoinType.LEFT);
criteriaQuery.orderBy(criteriaBuilder.asc(customer.get("lastName"));

JPA/Hibernate SELECT all parent fields with Child count in Single Query

I am using JPA 2.0, with Hibernate 1.0.1.Final.
I want all Parent table fields with no of children in single query.
In Other word, I want to translate from SQL into CriteriaAPI of JPA/Hibernate.
select kgroup.*, count(userGroup.uid)
from kernelGroup kgroup
left join kernelUserGroup userGroup on (kgroup.groupId = userGroup.groupId)
group by kgroup.groupId
I have following JPA Entities.
#Entity
#Table(name="kernel_group")
public class KernelGroup implements Serializable {
#Id
private int groupId;
private boolean autoGroup;
private String groupName;
#OneToMany
private Set<KernelUserGroup> kernelUserGroups;
private long jpaVersion;
}
#Entity
#Table(name="kernel_usergroup")
public class KernelUserGroup implements Serializable {
#EmbeddedId
private KernelUserGroupPK id;
private long jpaVersion;
#ManyToOne
private KernelGroup kernelGroup;
#ManyToOne
private KernelUser kernelUser;
}
#Embeddable
public class KernelUserGroupPK implements Serializable {
private String uid;
private int groupId;
}
My Current Criteria Query is like this :
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<KernelGroupDto> cQuery = cb.createQuery(KernelGroupDto.class);
Root<KernelGroup> root = cQuery.from(KernelGroup.class);
Join<KernelGroup, KernelUserGroup> userGroupsJoin = root.join(KernelGroup_.kernelUserGroups, JoinType.LEFT);
cQuery.select(cb.construct(KernelGroupDto.class, root, cb.count(userGroupsJoin.get(KernelUserGroup_.id).get(KernelUserGroupPK_.uid))));
cQuery.groupBy(root.get(KernelGroup_.groupId));
em.createQuery(cQuery).getResultList();
Now the Problem is, It fires multiple Queries to the database.
1) One query to retrieve groupId and no of count of users
2) N Queries to retrieve group info for each group.
I want only one Query to retrieve GroupInfo and no of count of the users as shown in Above SQL Query.
Please give me good suggestion.
Implement Using the bellow Code.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<KernelGroupDto> cQuery = cb.createQuery(KernelGroupDto.class);
Root<KernelGroup> root = cQuery.from(KernelGroup.class);
Join<KernelGroup, KernelUserGroup> userGroupsJoin =
root.join(KernelGroup_.kernelUserGroups, JoinType.LEFT);
cQuery.select(cb.construct(KernelGroupDto.class, root.
<Long>get("id"),cb.count(userGroupsJoin)));
cQuery .groupBy( root.<Long>get("id") );
cQuery.groupBy(root.get(KernelGroup_.groupId));
em.createQuery(cQuery).getResultList();
This code will work. for child count.
you must have the constructor of the class KernelGroupDto(Long id, Long childCount)

Categories

Resources