All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.databasesandlife.util.hibernate.InsertOrFetcher Maven / Gradle / Ivy

There is a newer version: 21.0.1
Show newest version
package com.databasesandlife.util.hibernate;

import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.hibernate.exception.ConstraintViolationException;

import java.lang.reflect.InvocationTargetException;
import java.util.Collection;

// 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 */ @SuppressWarnings("deprecation") 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy