Hi,
I'd appreciate guidance on how to write a safe findOrCreate method. I'd like to write a method that, given a natural
key:
-returns POJO representing a row (if one exists), or
-creates and inserts a new row and returns the object.
The constraints are:
-app is running on multiple appservers (thus making method 'syncrhonized' won't help)
-app logic allows for multiple transactions to attempt this method w/same argument at the same time
-DB is running under REPEATABLE_READ TX iso level
Here's what I have so far:
Code:
/**
*
* @author nikita
*
* @hibernate.class table="clients" dynamic-update="true" dynamic-insert="true"
*/
public class Client extends Persistent{
private String name;
/** Creates a new instance of Client */
public Client() {
}
/**
* guaranteed to return a Client object with given name:
* -if a row w/ name = aName exist return it
* -if such row does not exist, insert one and return it
*/
public static Client findOrCreate(String aName) {
Client c = null;
//get a matching row from DB
String hql = "select * from Client as client where client.name = ?";
Query q = HibernateUtil.getCurrentSession().createQuery(hql);
q.setParameter(0, aName);
c = (Client) q.uniqueResult();
//if row does not exist, create & insert one
if(c == null) {
c = new Client();
c.setName(aName);
HibernateUtil.getCurrentSession().persist(c); //may result in constraint violation exception!!!
//PROBLEM: what if another DB tx does an INSERT after this TX did a SELECT,
//but before this TX could commit an INSERT?
}
return c;
}
/**
* @hibernate.property unique="true" not-null="true"
*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
abstract public class Persistent implements Serializable{
private String id = UUID.randomUUID().toString();
transient Logger logger = Logger.getLogger(Persistent.class.getName());
private Integer hibernateVersion;
private Date creationDate = new Date();;
/**
* @hibernate.id generator-class="assigned"
*/
public String getId() {
return id;
}
void setId(String id) {
this.id = id;
}
Logger getLogger() {
return this.logger;
}
public String toString() {
String s = "";
s += this.getClass().getName();
s += ": ";
s += "id="+this.getId();
s +=" ";
return s;
}
/**
*do NOT manipulate directly
*
* @hibernate.version unsaved-value="null"
*/
public Integer getHibernateVersion() {
return hibernateVersion;
}
private void setHibernateVersion(Integer hibernateVersion) {
this.hibernateVersion = hibernateVersion;
}
/**
* @hibernate.property
*/
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
/**
* equal if IDs are equal
*/
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(! (obj instanceof Persistent)){
return false;
}
final Persistent other = (Persistent)obj;
return this.getId().equals(other.getId());
}
/**
* accompanying equals()
*/
public int hashCode() {
return this.getId().hashCode();
}
Problem:
Given two DB transactions T1, and T2 and the same natural key, the following race condition is possible:
Code:
T1: BEGIN TX;
T2: BEGIN TX;
T1: SELECT w/ name = "joe"; //returns nothing
T2: SELECT w/ name = "joe"; //returns nothing
T1: INSERT w/ name = "joe";
T1: COMMIT; //row inserted
T2: INSERT w/ name = "joe";
T2: COMMIT; //constraint violation
What I really need is "Insert if doesn't exist, but if exists then return that" but I am unsure on how to express that in Java (or even SQL for that matter) layer.
thanks
-nikita
Hibernate version: 3.1.3
Mapping documents:Code:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class dynamic-update="true" table="clients" name="net.eviltwinstudios.common.beans.Client" dynamic-insert="true">
<id name="id">
<generator class="assigned"/>
</id>
<version unsaved-value="null" name="hibernateVersion"/>
<property name="name" not-null="true" unique="true"/>
<property name="creationDate"/>
</class>
</hibernate-mapping>
Name and version of the database you are using:
MySQL 5.0
REPEATABLE_READ