Hibernate querying on a parent class creates ineffective union query - java

I am using spring boot and hibernate and I have following data structure.
#Entity
public class Template {
#Id
private Integer id;
#OneToMany(mappedBy = "template", orphanRemoval = true, fetch = FetchType.EAGER,cascade = CascadeType.ALL)
#BatchSize(size = 30)
private Collection<TemplateContent> contents;
}
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class TemplateContent {
#Id
private String contentType;
#Id
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "template_id", nullable = false)
private Template template;
}
#Entity
public class SomeContent extends TemplateContent {
private String someValue;
}
#Entity
public class SomeOtherContent extends TemplateContent {
private String someOtherValue;
}
#Repository
public interface TemplateRepository extends JpaRepository<Template, Integer> {
Page<Template> findByIdIn(Collection<Integer> ids, Pageable pageable);
}
When I call findByIdIn method it generates the following query:
SELECT ...
FROM (SELECT content_type,
template_id,
some_value,
NULL AS some_other_value
FROM someContent
UNION
SELECT content_type,
template_id,
some_other_value,
NULL AS some_value
FROM someOtherContent) contents0_
WHERE contents0_.template_id IN ( ?, ?, ? )
which is ineffective, because MySQL can't use indexes over derived tables. Is there a way to generate a more effective query.
//this would be the desired query
SELECT ...
FROM (SELECT content_type,
template_id,
some_value,
NULL AS some_other_value
FROM someContent
WHERE template_id IN ( ?, ?, ? )
UNION
SELECT content_type,
template_id,
some_other_value,
NULL AS some_value
FROM someOtherContent
WHERE template_id IN ( ?, ?, ? )) contents_0_ ....
I tried also to use different inheritance strategies, but it seems like all of them have a similar drawback.

If you want to have best performance in polymorphic query the best approach is to use strategy = InheritanceType.SINGLE_TABLE, probably you will pay performance with empty columns, but it depends which aspect is more important for you, also because strategy = InheritanceType.JOINED has same problems of TABLE_PER_CLASS

Related

Spring Data JPA how to specify Join type or Fetch Mode when using get("property") chain vs Join

I have two (Hibernate-based) Spring Data JPA domain classes, the "One" side Customer.class:
#Entity
#Table(name = "sys_customer")
#Data
public class Customer implements Serializable {
#Id
#Column(name = "cust_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "cust_name")
private String customerName;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
private Set<Order> orders;
}
and the "Many" side Order.class:
#Entity
#Table(name = "sys_order")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Order implements Serializable {
#Id
#Column(name = "order_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "order_name")
private String orderName;
#ManyToOne
#JoinColumn(name = "order_cust_id", referencedColumnName = "cust_id")
private Customer customer;
public Order( String orderName) {
this.orderName = orderName;
}
public Order(String orderName, Customer customer) {
this.orderName = orderName;
this.customer = customer;
}
}
I have OrderRepository interface which extends JpaRepository interface and JpaSpecificationExecutor interface:
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
I have a OrderSpecification.class with the static method searchByCustomerName:
public class OrderSpecification {
public static Specification<Order> searchByCustomerName(String customerName) {
return new Specification<Order>() {
#Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Join<Order, Customer> join = root.join("customer");
return criteriaBuilder.like(join.get("customerName"), "%" + customerName + "%");
//return criteriaBuilder.like(root.get("customer").get("customerName"), "%" + customerName + "%");
}
};
}
}
To find the differences between get("property") chain and Join, I wrote a simple test method and comment out the above OrderSpecificatin.class code
#Test
#Transactional
public void testFindOrderByCustomerName(){
String name = "adam";
List<Order> orders = orderRepository.findAll(OrderSpecification.searchByCustomerName(name));
for(Order order: orders){
Customer customer = order.getCustomer();
log.info(new StringBuilder().append(customer.getId()).append(" ").append(customer.getCustomerName()).toString());
}
}
I found that:
get("property") chain use a cross-join(which is very bad performancing) while Join use inner-join(since ManyToOne() by default is Fetch= FetchType.EAGER)
/* get("property") chain: Hibernate: select order0_.order_id as
order_id1_1_, order0_.order_cust_id as order_cu3_1_,
order0_.order_name as order_na2_1_ from sys_order order0_ cross join
sys_customer customer1_ where order0_.order_cust_id=customer1_.cust_id
and (customer1_.cust_name like ?) Hibernate: select customer0_.cust_id
as cust_id1_0_0_, customer0_.cust_name as cust_nam2_0_0_ from
sys_customer customer0_ where customer0_.cust_id=? */
/** * "Join": * Hibernate: select order0_.order_id as order_id1_1_,
order0_.order_cust_id as order_cu3_1_, order0_.order_name as
order_na2_1_ from sys_order order0_ inner join sys_customer customer1_
on order0_.order_cust_id=customer1_.cust_id where customer1_.cust_name
like ? * Hibernate: select customer0_.cust_id as cust_id1_0_0_,
customer0_.cust_name as cust_nam2_0_0_ from sys_customer customer0_
where customer0_.cust_id=? */
My questions are:
Can I specify the Join type(inner, all three outers) or Fetch Type(LAZY, EAGER) when using get("property") chain approach to avoid cross-join?
What scenario/best practice should I use get("chain") or always stay in Join?
Does the approach OrderSpecification.class with static method obey a good OOP design pattern?
You can't specify the join type for paths. It will use INNER join semantics by default and that is mandated by the JPA specification. If you want a different join type, you will have to create joins explicitly. The fact that using get renders as cross joins is a limitation of the old query model of Hibernate, but Hibernate 6.0 will fix this. The semantics are the same though and the query planner of your database should be able to treat both queries the same way. Maybe you just need to update your database version?
There is no "best practice" i.e. this really depends on your needs. Explicit joins are just that, explicit. So multiple calls to join will create multiple joins in SQL.
As for the OOP question, I think this is fine, yes.

Can't make #Formula attributes work with CriteriaBuilder

I have an entity like the following were I use #Formula to populate clientId from other tables.
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Formula("(SELECT cl.id from failure f " +
"INNER JOIN pvplant p ON f.pv_plant_id = p.id " +
"INNER JOIN company co ON p.company_id = co.id "+
"INNER JOIN client cl ON co.client_id = cl.id "+
"WHERE f.id = id) ")
public Integer clientId;
}
while CrudRepository<Failure,Integer> JPA method getByClientId works fine I am trying to make something more dynamic for filtering using a Map of keys and values with Specification and CriteriaBuilder.
public MySpecifications {
public static Specification<Failure> equalToEachColumn(HashMap<String,Object> map) {
return new Specification<Failure>() {
#Override
public Predicate toPredicate(Root<Failure> root, CriteriaQuery<?> cq, CriteriaBuilder builder) {
return builder.and(root.getModel().getAttributes().stream().map(a ->
{
if (map.containsKey(a.getName())) {
Object val = map.get(a.getName());
return builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString()));
}
return builder.disjunction();
}
).toArray(Predicate[]::new)
);
}
};
}
}
When I am passing id in the HashMap it works fine but when I have clientId it doesn't send anything back. It is interesting that getAttributes() actually returns clientId but it seems that builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString())) is false and not true
This is how I am using the Specification:
failureRepository.findAll(Specifications.where(MySpecifications.equalToEachColumn(map)));
Am I missing something?
Thanks in advance!
I wouldn't expect this to work however you could make it work by using a database view as an alternative to #Formula and mapping the entity across the table and view using #SecondaryTable.
//failures_client_vw is a 2 column db view: failure_id, client_id
#Table(name = "failures")
#SecondaryTable(name = "failures_client_vw",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "failure_id"))
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Column(name = "client_id", table = "failures_client_vw")
public Integer clientId;
}
You can then query clientId as you would any other property.
https://en.wikibooks.org/wiki/Java_Persistence/Tables#Multiple_tables
The actual problem was that I was using
builder.disjunction()
in the and() which creates 0=1 false predicates.
When I replaced it with
builder.conjunction() (which creates 1=1 true predicates)
in my code it worked fine. So #Formula properties behave as native ones to the table and it seems there is no need for SecondaryTable and a new View. Apparently in my earlier tests I used an entity that had just an id in its class and when I added clientId it misled me to believe that #Formula properties don't work, while it was the disjunction from the id that broke clientId

Spring Data JPA does not fire update query on dirty context

I have tried a lot to solve this problem. Read spring data jpa reference documentation with hibernate documentation as well but no luck in this case.
Found spring data jpa examples at github way too short and documentation was not very helpful for novice people like me.
I have following scenario:
My project is in spring boot with spring-data-jpa in repository layer.
Investment can be FixedDepositInvestment, RecurringDepositInvestment or any other subtype. For this, I have used inheritance in hibernate.
Note: getters and setters omitted.
Question might be lengthy
BaseEntity class
#MappedSuperclass
public abstract class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Integer id;
}
Investment class
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
public class Investment extends BaseEntity{
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User user;
#Temporal(TemporalType.DATE)
#DateTimeFormat(pattern = "yyyy-MM-dd")
#Column(name = "issue_date", nullable = true)
private Date dateOfIssue;
#CreationTimestamp
#DateTimeFormat(pattern = "yyyy-MM-dd")
#Column(name = "dateCreated")
private Date dateCreated;
#UpdateTimestamp
#DateTimeFormat(pattern = "yyyy-MM-dd")
#Column(name = "lastUpdated")
private Date lastUpdated;
}
CertificateInvestment class
#MappedSuperclass
public abstract class CertificateInvestment extends Investment {
public CertificateInvestment() {
}
public CertificateInvestment(User user, int amountInvested) {
super(user);
this.amountInvested = amountInvested;
}
#Column(name = "amount_invested", nullable = false)
private int amountInvested;
#Column(name = "account_no", nullable = true) // can be folio no or any other name
private String accountNo;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "investment_id", nullable = true)
List<Nominee> nominees;
}
BankCertificateInvestment class
#MappedSuperclass
public abstract class BankCertificateInvestment extends CertificateInvestment{
public BankCertificateInvestment() {
super();
}
public BankCertificateInvestment(User user, int amountInvested, Bank bank) {
super(user, amountInvested);
this.bank = bank;
}
#OneToOne
#JoinColumn(name = "bank_id", nullable = false)
private Bank bank;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "investment_id", nullable = true) //???
private Set<JointHolder> accountJointHolders; // make sure same jh is not added again
#Temporal(TemporalType.DATE)
#DateTimeFormat(pattern="yyyy-MM-dd")
#Column(name = "maturity_date", nullable = true)
private Date dateOfMaturity;
#Column(name = "interest_rate", nullable = true)
private float interestRate;
#Enumerated(EnumType.STRING)
#Column(nullable = true)
private InterestPayable interestPayable;
#Column(nullable = true)
private int durationInMonths;
}
FixedDepositInvestment class
#Entity
#PrimaryKeyJoinColumn(name = "id")
#Table(name = "FixedDepositInvestment")
public class FixedDepositInvestment extends BankCertificateInvestment {
public FixedDepositInvestment() {
super();
}
public FixedDepositInvestment(User user, int amountInvested, Bank bank) {
super(user,amountInvested, bank);
}
#Column(nullable = true)
private String creditToAccountNumber;
}
For this I have created following repository interfaces:
InvestmentRepository interface:
#Transactional(readOnly = true)
public interface InvestmentRepository extends CrudRepository<Investment, Integer> {
}
FixedDepositInvestmentRepository interface :
#Transactional
public interface FixedDepositInvestmentRepository extends CrudRepository<FixedDepositInvestment, Integer> {
}
UserRepository interface :
#Transactional(readOnly = true)
public interface UserRepository extends CrudRepository<User, Integer> {
}
BankRepository Interface :
public interface BankRepository extends CrudRepository<Bank,Integer>{
}
Finally, I have executed following code.
Note: I have directly written code in the controller #RequestMapping method for testing purposes only.
#Controller
public class AddInvestmentController {
#Autowired
FixedDepositInvestmentRepository fixedDepositInvestmentRepo;
#Autowired
UserRepository userRepository;
#Autowired
BankRepository bankRepository;
#Autowired
EntityManager em;
#RequestMapping(value = "/addme", method = RequestMethod.GET)
public String addInvestment() {
User user = new User("Oliver","Gierke","9999999999",new Date(),"123456");
Bank bank = new Bank("tjsb","123456");
userRepository.save(user);
bankRepository.save(bank);
FixedDepositInvestment fd = new FixedDepositInvestment(user, 1000, bank);
fixedDepositInvestmentRepo.save(fd);
fd.setAccountNo("55555");
fd.setAmountInvested(12345);
fd.setAmountInvested(352);
System.out.println("date created : -------------" + fd.getDateCreated() + " " + fd.getLastUpdated());
// System.out.println(fixedDepositInvestmentRepo.findAll());
return "add";
}
}
Queries that get fired are:
Hibernate:
insert
into
User
(address, dob, firstName, lastName, mobile, user_password)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
insert
into
Bank
(address, name, ifsc_code)
values
(?, ?, ?)
Hibernate:
insert
into
Investment
(dateCreated, issue_date, lastUpdated, user_id)
values
(?, ?, ?, ?)
Hibernate:
insert
into
FixedDepositInvestment
(account_no, amount_invested, amount_on_maturity, bank_id, maturity_date, durationInMonths, interestPayable, interest_rate, creditToAccountNumber, id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
The annoying part is lines
fd.setAccountNo("55555");
fd.setAmountInvested(12345);
fd.setAmountInvested(352);
do NOT fire UPDATE query at all.So, my lastUpdated date is same as dateCreated.
However, If I call
System.out.println(fixedDepositInvestmentRepo.findAll());
It fires an UPDATE then select
Same thing happens in the test cases also.
With same code, if I add
#Autowired
TestEntityManager em;
and
em.flush()
// at method ending, the update is called.
This behaviour is annoying, which raises many questions for me:
Why update is not being called? Spring data jpa does not check for dirty context, do we have to fire a repository.save(entity) again to update changes?
In spring jpa documentation,it has been mentioned, when you configure a transaction as readOnly which causes Hibernate to skip dirty checks (a noticeable improvement on large object trees). .
Now I am confused whether I should use #Transactional with readOnly=true or not.
Do I need to define #NoRepositoryBean for all #MappedsuperClass too? I have seen in some posts, people create these, I do not know if that is needed, in what scenario should I create norepositorybean for such classes?
Have I called queries(methods) in proper format?
Is this problem of hibernate or spring data jpa?
When do we need TestEntityManager? Is it required when we use spring data jpa?

Hibernate/JPA JPQL to wrong SQL when querying Map<String,String> field

This is my Entity configuration
#Entity
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE KEY(a) = 'email' AND VALUE(a) = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
public class Payment {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "payment_type")
private Integer paymentType;
/** other properties, getters and setters */
#ElementCollection
#CollectionTable(name = "additional_auth_data")
#MapKeyJoinColumn(name = "id", referencedColumnName = "id")
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Map<String, String> additionalAuthData;
}
The NamedQuery findByEmail("test#example.com") generates the following SQL
select -- all fields ...
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where
additional1_.field='email' and (select additional1_.data_value from additional_auth_data additional1_ where payment0_.id=additional1_.id)='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
which is wrong: it may work if you have only one row but it blows up otherwise. H2 complains Scalar subquery contains more than one row and PostgreSQL more than one row returned by a subquery used as an expression. In fact, query's where condition compares a scalar value ('test#example.com') with a subquery.
The correct SQL should be:
select -- all fields
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where additional1_.field='payerEmail' and additional1_.data_value='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
Is the HSQL correct? Is there a way to instruct Hibernate to generates a clever, better SQL? Is this a Hibernate bug?
Note: Hibernate shipped with Spring Boot Starter 1.3.7.RELEASE
Edit:
Using an #Embeddable class
#ElementCollection
#JoinTable(name = "additional_auth_data", joinColumns = #JoinColumn(name = "id"))
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Set<AdditionalData> additionalAuthData;
#Embeddable
public static class AdditionalData {
#Column(name = "field", nullable = false)
private String field;
#Column(name = "data_value")
private String dataValue;
protected AdditionalData() {
}
public AdditionalData(String field, String dataValue) {
this.field = field;
this.dataValue = dataValue;
}
/** Getters, setters; equals and hashCode on "field" */
}
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE a.field = 'email' AND a.dataValue = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
solves the problem, and the SQL is correct, but it looks just plain wrong, like shooting a fly with a bazooka...
It generates correct SQL without value().
Use just a=?1
But I would expect is should generate it simple also with it.

JPA Join table with SELECT

I have a SQL query like this:
SELECT h.name, h.created_date, tbl.*
FROM my_table tbl
LEFT JOIN
(SELECT name, max(created_date) created_date FROM my_table GROUP BY name) h
ON tbl.name = h.name;
It returns the row from my_table (which has multiple for name="") along with the maximum created_date for that name.
Is there a way to replicate this in a JPQL query?
Here is the gist of the Entity class, it's quite simple:
#Entity
#Table(name = "MY_TABLE")
#XmlRootElement
public class MyTable implements Serializable {
private BigDecimal tableId;
private String name;
private Date createdDate;
// ...
#Id
#Basic(optional = false)
#Column(name = "TABLE_ID")
#GeneratedValue(generator = "TBL_ID_SEQ")
public BigDecimal getTableId() {
return tableId;
}
#Basic(optional = false)
#Column(name = "NAME")
public String getName() {
return name;
}
#Basic(optional = false)
#Column(name = "CREATED_DATE", insertable = false)
#Temporal(TemporalType.TIMESTAMP)
public Date getCreatedDate() {
return createdDate;
}
// ... getters/setters
}
Just reading your question I guess you do not need another entity. Entities in JPA are the same like tables in SQL. Usually there is a 1:1 relationship between entities and tables. You just have to know how to invoke a query using JPQ. You need a entity manager, which invokes your statement.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("PersistenceUnit");
EntityManager em = emf.createEntityManager();
You have to define your persistence unit, i.e. in a pom file, or a config java file. So done you can go on coding something like this:
Query q = em.createQuery( "Your query in sql syntax as a string object" );
In respect to your entities and invoked query you will receive a List using
List<object> resultOfMyQuery = q.getResultList();
This is only one short example. But hopefully you got some buzzwords to look for ;)

Categories

Resources