Pages

Showing posts with label Hibernate. Show all posts
Showing posts with label Hibernate. Show all posts

Friday, March 29, 2013

Don't use 'Query.list()' when you really need 'Query.uniqueResult()' in Hibernate.

I was motivated to discuss a little about this topic, since I am seeing a usage of hibernate which is capable of making server issues in your application. What I am going to discuss here, is very simple, but some developers are tend to do it in wrong way. Sometime, they are doing this intentionally for safe side.

When we are using hibernate Query to fetch a single instance, Query.uniqueResult() is being used except special scenarios. To use Query.uniqueResult(), we must make sure that query will return a single object. But some programmers are still used to do it with Query.list() method which can pull your application out into very inconsistency situation and issues which are very hard to discover the reason for failure. 

Just have a look into the following example.

public Account getAccountByAccountIdAndType(Long accountId, AccountType accountType) throws FetchAccountException {
 
    Account account = null;
    Query query = getSession().getNamedQuery("getAccountByAccountId");
    query.setLong("accountId", accountId); 
    query.setString("accountType", AccountType.SAVING.toString());
    List<Account> list = query.list();
    if (null != list && list.size() > 0) {
         account = list.get(0);
    }
    return account;
}

The above function is responsible to get Account instance for a given account number and type. Let's assume there can not be two accounts with the same type having same account number. 

If you carefully see the above code, the developer has applied many safe factors to minimize the exceptions. Some are even ambiguous. The above method will be functioning well until there is one Account with same type and account number and you expects that too. And also, it will execute well when there are more than one Account with same type and account number, which results wrong output and we don't actually need. 

What will happen, if there are more than one accounts with same type and same account number?

That will be a total failure of your application and you will have to compensate if banking system will be on this kind of state. Let's see how the above method will behave in this kind of situation. The above method will hide this critical exception and keeps application flow functioning. The function will get a list which is having the accounts and will return the first Account instance. Ultimately, this will result wrong outputs in your application. 

The programmer has added a safe factor to keep application flow functioning even with more than one accounts with same type and number. This is unnecessary and a kind of cheat programming. The programmer can survive for some time, the company who is working for, will have to compensate for loses. This is very poor programming. Some programmers do this intentionally as a safe factor, some are doing this as a quick fix for a defect. But this is very very dangerous.

Further the programmer has checked the "list" for NULL which is ambiguous, because hibernate does not return NULL list, instead it always returns empty list.

And other shortage of above method is, it may return NULL as the output which is not a good programming practice. Instead, you can throw an exception.

The getAccountByAccountIdAndType() method must return an exception in state. If the programmer used Query.uniqueResult() for this purpose, it returns the following exception keeping every one enlighten. We can take immediate action to get rid of this killer situation.

org.hibernate.NonUniqueResultException: query did not return a unique result: 2

We can improve the above function as follows.

public Account getAccountByAccountIdAndType(Long accountId, AccountType accountType) {
 
    Query query = getSession().getNamedQuery("getAccountByAccountId");
    query.setLong("accountId", accountId); 
    query.setString("accountType", AccountType.SAVING.toString());
    Account account = (Account)Query.uniqueResult();
    
    if (account == null) {
         throw new RuntimeException("Unable to find Account for account number :" + accountId);
    }
    return account;
}

Now the above method will return the exception when there are more than one account with same type and number.

If you want to send more specific error message rather than a hibernate exception message when you are having more than one account with same account number and type, you can put a try-catch block as follows.

public Account getAccountByAccountIdAndType(Long accountId, AccountType accountType) {
     
    Account account = null;
    try {
        Query query = getSession().getNamedQuery("getAccountByAccountId");
        query.setLong("accountId", accountId); 
        query.setString("accountType", AccountType.SAVING.toString());
        Account account = (Account)Query.uniqueResult();
    } catch(NonUniqueResultException) {
        throw new RuntimeException("Two account found with same account number and type : Acc No-" + accountId);
    }
    if (account == null) {
         throw new RuntimeException("Unable to find Account for account number :" + accountId);
    }
    return account;
}

As a conclusion, always use Query.uniqueResult() when you want to fetch a single object with hibernate Query and you should make sure your query will return a single object too.

Sunday, September 11, 2011

How to Use Hibernate for Composite Ids with association mappings

Recently, We faced a tricky situation with hibernate association mapping with a composite id field. We needed to have bidirectional association with one-to-may and many-to-one.Our tow tables was "REPORT" and "REPORT_SUMMARY" which has one-to-many relationship from REPORT to REPORT_SUMMARY and many-to-one relationship from REPORT_SUMMARY to REPORT table. The primary key of REPORT_SUMMARY table is defined as a composite primary key which consists of auto increment id field and the primary key of REPORT table.
CREATE TABLE REPORT (
     ID INT(10) NOT NULL AUTO_INCREMENT,
     NAME VARCHAR(45) NOT NULL,
     PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE REPORT_SUMMARY (
   ID INT(10) NOT NULL AUTO_INCREMENT,
   NAME VARCHAR(45) NOT NULL,
   RPT_ID INT(10) NOT NULL,
   PRIMARY KEY (`ID`,`RPT_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

The hibernate entity classes are as fallows.

Report.java
package com.semika.autoac.entities;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
public class Report implements Serializable{

    private static final long serialVersionUID = 9146156921169669644L;

    private Integer id;
    private String name;
    private Set<ReportSummary> reportSummaryList  = new HashSet<ReportSummary>();
    
    public Integer getId() {
         return id;
    }
    public void setId(Integer id) {
         this.id = id;
    }
    public String getName() {
         return name;
    }
    public void setName(String name) {
         this.name = name;
    }
    public Set<ReportSummary> getReportSummaryList() {
         return reportSummaryList;
    }
    public void setReportSummaryList(Set<ReportSummary> reportSummaryList) {
         this.reportSummaryList = reportSummaryList;
    }
}

ReportSummary.java
package com.semika.autoac.entities;

import java.io.Serializable;
public class ReportSummary implements Serializable {

private static final long serialVersionUID = 8052962961003467437L;

private ReportSummaryId id;
private String name;

public ReportSummaryId getId() {
    return id;
}
public void setId(ReportSummaryId id) {
    this.id = id;
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}
@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    ReportSummary other = (ReportSummary) obj;
    if (id == null) {
       if (other.id != null)
          return false;
       } else if (!id.equals(other.id))
          return false;
    if (name == null) {
       if (other.name != null)
          return false;
       } else if (!name.equals(other.name))
          return false;

   return true;
  }
}

ReportSummaryId.java
package com.semika.autoac.entities;

import java.io.Serializable;

public class ReportSummaryId implements Serializable{

private static final long serialVersionUID = 6911616314813390449L;

private Integer id;
private Report report;

public Integer getId() {
   return id;
}
public void setId(Integer id) {
   this.id = id;
}
public Report getReport() {
   return report;
}
public void setReport(Report report) {
   this.report = report;
}
@Override
public int hashCode() {
   final int prime = 31;
   int result = 1;
   result = prime * result + ((id == null) ? 0 : id.hashCode());
   result = prime * result + ((report == null) ? 0 : report.hashCode());
   return result;
}
@Override
public boolean equals(Object obj) {
   if (this == obj)
      return true;
   if (obj == null)
      return false;
   if (getClass() != obj.getClass())
      return false;
   ReportSummaryId other = (ReportSummaryId) obj;
   if (id == null) {
      if (other.id != null)
         return false;
      } else if (!id.equals(other.id))
         return false;
   if (report == null) {
      if (other.report != null)
         return false;
      } else if (!report.equals(other.report))
         return false;

   return true;
  }
}
Report object has a collection of ReportSummary objects and ReportSummaryId has a reference to Report object. The most important part of this implementation is hibernate mapping files.
Report.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="com.semika.autoac.entities.Report" table="REPORT" >
         <id name="id" type="int" column="id" >
                 <generator class="native"/>
         </id>
         <property name="name">
               <column name="NAME" />
         </property>
         <set name="reportSummaryList" table="REPORT_SUMMARY" cascade="all" inverse="true">
             <key column="RPT_ID" not-null="true"></key>
             <one-to-many class="com.semika.autoac.entities.ReportSummary"/>
         </set>
     </class>
</hibernate-mapping>
ReportSummary.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class name="com.semika.autoac.entities.ReportSummary" table="REPORT_SUMMARY" >
        <composite-id name="id" class="com.semika.autoac.entities.ReportSummaryId">
             <key-property name="id" column="ID"></key-property>
             <key-many-to-one name="report" 
                              class="com.semika.autoac.entities.Report"
                              column="RPT_ID"</key-many-to-one>
        </composite-id>
        <property name="name">
             <column name="NAME" />
        </property>
    </class>
</hibernate-mapping>

Share

Widgets