There have been debates in my company about why to use an ORM. The old fashioned way of using JDBC just works, so why change?
In this article, I am talking about a benefit that is usually not highlighted in such arguments.
The ORM literature, the books (like one of my favorites: Java Persistence with Hibernate) abound with the advantages of ORM. I think the best part is the massive reduction in development effort of writing JDBC code. And, if there is less code to write, there are lesser bugs to fix. So developers and development managers should both be happy.
Let’s take the same domain classes (Customer having a Set of Accounts) that we used in the fetch strategies article. This time, I am going to have a case which involves insert,update and a delete. The inserts are simple, the fun starts when you want to update and delete records.
The use case
Let’s say that our application fetches a customer with four accounts. These four accounts are displayed to the user. The customer does the following operations:
1. Adds one more account to this Customer. This should trigger an insert.
2. Removes an existing Account. This should trigger a delete.
3. Modifies the account balance of one of the accounts ( this account is not in #1 or #2 above). This should trigger an update.
4. After these changes, the user “submits” this Customer and it has to be persisted in the database.
Note: For the purpose of this demo, both Account and Customer are modified with mutator methods setBalance() and removeAccount() respectively. Also, toString() implementations are added to show the internal state of these objects.
The Problem Statement
The Customer has a Set
of Accounts. The copy that is submitted by the user has a different Set of Accounts – some are dropped, new ones are added and some are modified.
How do you know which record to delete, update and insert?
The JDBC approach
There are two approaches possible.
The first is the brute force approach. Empty out (delete) all the Account records for the customer and insert the records from the new set. However, this will result in new IDs for the same Accounts that were not even modified.
The second approach is to run a comparison loop. You match every element from the Set from the database with the set submitted by the user. For those Accounts which are not in the user set but are in the database set, you fire a delete statement. For those that are in the user set but not in the database set, you fire an insert. And for those which are in both, you have to check for their equality. And you have to be carefully check all the relevant fields (have a good equals() implementation).
There is a lot of work, a good amount of comparison logic to write.
The Hibernate approach
Let Hibernate figure out what to do. You load a copy from the database and ”merge” the user submitted copy onto it. Then you save this merged copy.
Here is the code that performs the merge in the DAO implementation
@Override @Transactional public Customer mergeAndSave(int custId, Customer userCopy){ Customer databaseCopy = getCustomer(custId); System.out.println("Database copy:"+databaseCopy); System.out.println("User Modified copy:"+userCopy); Session session = sessionFactory.getCurrentSession(); Customer mergedCopy = (Customer)session.merge(userCopy); session.saveOrUpdate(mergedCopy); return mergedCopy; }
Here is the code which is used to mutate a Customer object for this demo:
private static Customer getMutatedCustomer(int custId){ Customer c1 = custDao.getCustomer(custId); c1.addAccount(new Account(199)); List accts = new ArrayList(c1.getAccounts()); Account acctToRemove = accts.get(0); Account acctToModify = accts.get(1); acctToModify.setBalance(149); c1.removeAccount(acctToRemove); return c1; }
Here is the output on the console (color highlighting is mine)
Hibernate: select customer0_.CUSTOMER_ID as CUSTOMER1_1_1_, customer0_.CUSTOMER_NAME as CUSTOMER2_1_1_, accounts1_.cust_id as cust3_1_3_, accounts1_.ACCOUNT_NO as ACCOUNT1_0_3_, accounts1_.ACCOUNT_NO as ACCOUNT1_0_0_, accounts1_.BALANCE as BALANCE0_0_ from customer customer0_ left outer join account accounts1_ on customer0_.CUSTOMER_ID=accounts1_.cust_id where customer0_.CUSTOMER_ID=?
Database copy:Customer [custId=1, name=Pankaj,
accounts=[Account [accountNo=1, balance=100], Account [accountNo=2, balance=150], Account [accountNo=5, balance=250], Account [accountNo=6, balance=175]]]
User Modified copy:Customer [custId=1, name=Pankaj,
accounts=[Account [accountNo=2, balance=149], Account [accountNo=5, balance=250], Account [accountNo=6, balance=175], Account [accountNo=0, balance=199]]]
Hibernate: select max(ACCOUNT_NO) from account
Hibernate: insert into account (BALANCE, ACCOUNT_NO) values (?, ?)
Hibernate: update account set BALANCE=? where ACCOUNT_NO=?
Hibernate: update account set cust_id=null where cust_id=? and ACCOUNT_NO=?
Hibernate: update account set cust_id=? where ACCOUNT_NO=?
Notice that the account balance has changed in the User modified copy for accountNo=2. Also a new Account is seen in the User modified copy with accountNo=0. It is zero because it is not yet persisted to the database and an id assigned to it. Also, accountNo=1 is missing from the User modified copy.
Here is how the Account table looked before and after the merge.
Explanation
The first insert is creating a new record in the Account table for the new account that has been added. Thus a new Account entity is created. However, notice that the cust_id field is not set.
The second statement is an update for the account that has been modified
The third statement is setting the account field to null thus effectively remove this account from the customer. Note that the record is not deleted because the Account is an entity type.
The last update links the newly created Account to the Customer via the cust_id field update.
So, why ORM(Hibernate)?
If you had several objects and a graph associated with them, there is a lot of code to loop through the graph, detect the changes, maintain a whole bunch of insert, delete, update statement strings and write the logic to invoke them properly. On top of that you have to put your transaction boundaries so that the database does not land in inconsistent state while your gazillion lines of code throw an exception in between.
Hibernate frees your time to concentrate on the business logic rather than spend effort on getting Persistence right.