com.databasesandlife.util.hibernate.InsertOrFetcher Maven / Gradle / Ivy
Show all versions of java-common Show documentation
package com.databasesandlife.util.hibernate;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import org.hibernate.Criteria;
import org.hibernate.LockMode;
import org.hibernate.Transaction;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.hibernate.exception.ConstraintViolationException;
// Javadoc deliberately doesn't have * at the start to make it easier to copy/paste to an HTML editor
/**
Reads an instance from a database, creating it just in time if it doesn't exist yet.
Background
It is sometimes desirable to create rows "just in time" in a database.
For example, to maintain a count of the number of SMS sent per day, a database table with columns (day, count) might be
created. Each time an SMS is sent, a row should be created for that day if none exists yet, and the count in the previously
existing row should be incremented otherwise.
Hibernate provides no facility for managing such "just in time" creation. (The saveOrUpdate method looks like it might
help, but assumes the application knows whether the row already exists or not. In reality, in a concurrent environment, as soon
as the application has determined whether the row exists or not, the information is already stale, as some other session might
have changed the data.)
Usage
Session s = .... ; // Hibernate Session
DailyLog newObj = new DailyLog();
newObj.setDay(getTodaysDate());
newObj.setCount(0);
Collection<String> uniqueIdentifier = List.of("day");
DailyLog todaysLog = InsertOrFetcher.loadAndLock(
DailyLog.class, s, newObj, uniqueIdentifier);
todaysLog.incrementCount();
If the row didn't exist in the database, then newObj is inserted and returned. If the row existed then it is
fetched and returned.
In addition, the returned object is:
- never null,
- always already exists in the database by the time this call ends
- associated with the passed Hibernate Session (i.e. is "persistent" in Hibernate Session terminology)
The database table backing DailyLog must have not only a primary key constraint defined, but in addition another
constraint which will make sure that only one row per day can be created. The decision about if the object already
exists or if it needs to be inserted is taken by attempting an insert and seeing if a constraint violation error occurs (see
"strategy" later.)
The method loadAndLock locks the object when returning it (with "select for update"), intended for read/write access;
the method load loads the object without locking it, intended for read-only access.
Strategy
As mentioned earlier, to "just in time" insert an object it is no good doing a "select" to see if the object exists and
inserting it if it doesn't. Between the time the select is done and the time the insert is done, another session might have done
an insert.
The only way to proceed is to perform the "insert", and if that succeeds then one can be certain that the row now exists, and
if that fails with a constraint violation then one can be certain that the row already existed.
However, although this is the only strategy that can be adopted, it is not easy to implement in Hibernate. Hibernate states
(in the Session Javadoc) that if a statement fails, then the Session must be discarded.
Therefore the strategy which is adopted is to create a new Session with its own Transaction, perform the insert. Afterwards
one can be certain that the row exists in the database, so the Session is destroyed, and the row is loaded in the original
Session and returned.
This may have performance penalties, however it is the only way to ensure correct behavior.
* See Programming with unique constraints.
*
* @author This source is copyright Adrian Smith and licensed under the LGPL 3.
* @see Project on GitHub
*/
public class InsertOrFetcher {
protected static T load(Class cl, Session mainSession, T objectForInsertion, Collection domainKey, LockMode lk) {
try {
// Insert object & catch exception if fail
try (var newSession = mainSession.getSessionFactory().openSession()) {
var tx = newSession.beginTransaction();
try {
newSession.save(objectForInsertion);
tx.commit();
} finally {
if (tx.isActive()) tx.rollback();
}
} catch (ConstraintViolationException ignored) {}
// Create fetch parameters
var select = mainSession.createCriteria(cl);
select.setLockMode(lk);
for (var attr : domainKey) {
try {
var methodName = "get" + attr.substring(0, 1).toUpperCase() + attr.substring(1);
var method = cl.getMethod(methodName);
var value = method.invoke(objectForInsertion);
select.add(Restrictions.eq(attr, value));
}
catch (NoSuchMethodException e) { throw new RuntimeException(
"Class '"+cl+"' has no public getter for property '"+attr+"'"); }
}
// Fetch & return object (it must exist if insert failed)
var result = cl.cast(select.uniqueResult());
if (result == null) throw new RuntimeException("INSERT was successful or caused constraint exception, " +
"but SELECT didn't find object -- possibly unique constraint wrongly defined?");
return result;
}
catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); }
}
/**
* See class documentation.
* @param cl The type of Hibernate-managed to be inserted/fetched
* @param mainSession Hibernate session with active transaction
* @param objectForInsertion Not managed by Hibernate yet
* @param domainKey Which attributes of objectForInsertion should be used for the WHERE to re-find the object
* @return See class documentation
*/
public static T load(Class cl, Session mainSession, T objectForInsertion, Collection domainKey) {
return load(cl, mainSession, objectForInsertion, domainKey, LockMode.READ);
}
/**
* See class documentation.
* @param cl The type of Hibernate-managed to be inserted/fetched
* @param mainSession Hibernate session with active transaction
* @param objectForInsertion Not managed by Hibernate yet
* @param domainKey Which attributes of objectForInsertion should be used for the WHERE to re-find the object
* @return See class documentation
*/
public static T loadAndLock(Class cl, Session mainSession, T objectForInsertion, Collection domainKey) {
return load(cl, mainSession, objectForInsertion, domainKey, LockMode.UPGRADE);
}
}