Hi,
I'm using Hibernate in the database layer of a non-web application. During execution the application creates new/transient objects that are being written to the database at the end of a execution cycle.
The first cycle works as expected and all transient objects are inserted into the database. Starting a second cycle creates a new set of transient objects that to some part overlap with ones from the first cycle. Since the objects are transient, Hibernate inserts new rows into the database and does not care about existing ones with the same natural ID (or insert fails if I define natural ID columns unique).
Code:
public class TestAll {
private SessionFactory factory = HibernateUtil.getSessionFactory();
@Test
public final void testDbSaveTwice() {
try {
ParentObject p1 = createParentObject();
ChildObject c1 = createChildObject();
p1.addChildObject(c1);
write(p1);
ParentObject p2 = createParentObject();
ChildObject c2 = createChildObject();
p2.addChildObject(c2);
p2.setParentData(p2.getParentData() + " - modified");
c2.setChildData(c2.getChildData() + " - modified");
write(p2);
}
catch(Exception e) {
fail(e.getMessage());
}
}
private ParentObject createParentObject() {
ParentObject rval = new ParentObject("my unique parent object name");
rval.setParentData("some parent data");
return rval;
}
private ChildObject createChildObject() {
ChildObject rval = new ChildObject("my unique child object name");
rval.setChildData("some child data");
return rval;
}
public void write(Object obj) {
Session session = factory.openSession();
Transaction transaction = null;
try {
transaction = session.beginTransaction();
session.saveOrUpdate(obj);
transaction.commit();
}
catch(RuntimeException e) {
if(transaction != null) transaction.rollback();
e.printStackTrace();
throw(e);
}
finally {
session.close();
}
}
}
Since I'm new to Hiberante I followed the advice to have Hibernate assign surrogate primary keys and define natural IDs with equals() and hashcode() implemented accordingly:
Code:
@Entity
public class ParentObject {
@Id @GeneratedValue
private Long id;
@Column(length = 100, nullable = false)
@org.hibernate.annotations.NaturalId
private String uniqName = new String();
@Basic
private String parentData;
@OneToMany(mappedBy="parentObject", cascade=CascadeType.ALL)
private List<ChildObject> childObjects = new ArrayList<ChildObject>();
...
public void addChildObject(ChildObject childObject) {
childObjects.add(childObject);
childObject.setParentObject(this);
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null) return false;
if(getClass() != obj.getClass()) return false;
ParentObject other = (ParentObject) obj;
return uniqName.equals(other.uniqName);
}
@Override
public int hashCode() {
return uniqName.hashCode();
}
}
Code:
@Entity
public class ChildObject {
@Id @GeneratedValue
private Long id;
@Column(length = 100, nullable = false)
@org.hibernate.annotations.NaturalId
private String uniqName;
@Basic
private String childData;
@ManyToOne
@JoinColumn(nullable=false)
private ParentObject parentObject;
...
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null) return false;
if(getClass() != obj.getClass()) return false;
ChildObject other = (ChildObject) obj;
return uniqName.equals(other.uniqName);
}
@Override
public int hashCode() {
return uniqName.hashCode();
}
}
I was able to force (or trick) Hibernate into updating exising rows by looking up surrogate ID if existing, and set that primary ID of the transient object (Hibernate then treats that object as detached). While such change is fine for single objects, it causes some additional effort in duplicating what cascading updates already provide for associated objects.
When I learned about event listeners (in my case SaveOrUpdateEventListener) I added that primary key lookup in a custom event listener before calling the onSaveOrUpdate() of the DefaultSaveOrUpdateEventListner. That proved to work for entities without associated objects, but resulted in an endless recursion with associated objects. Hibernate triggers a flush before executing my ID lookup query in the event listener for the associated object which again triggers SaveOrUpdate...
Code:
public class TestSafeOrUpdateListener extends DefaultSaveOrUpdateEventListener {
private final Logger logger = LoggerFactory.getLogger(TestSafeOrUpdateListener.class);
@Override
public void onSaveOrUpdate(SaveOrUpdateEvent event) {
if(event.getObject() instanceof ParentObject) {
logger.debug("saveOrUpdate fired type ParentObject");
ParentObject parentObject = (ParentObject)event.getObject();
Long id = lookupParentObjectId(event);
if(id != null) {
parentObject.setId(id);
}
}
else if(event.getObject() instanceof ChildObject) {
logger.debug("saveOrUpdate fired for type ChildObject!");
ChildObject childObject = (ChildObject)event.getObject();
Long id = lookupChildObjectId(event);
if(id != null) {
childObject.setId(id);
}
}
else {
logger.debug("saveOrUpdate fired for unhandled type!");
}
super.onSaveOrUpdate(event);
}
private Long lookupParentObjectId(SaveOrUpdateEvent event) {
Session session = event.getSession();
ParentObject parentObject = (ParentObject)event.getObject();
Query getIdQuery = session.createQuery("select parentObject.id from ParentObject parentObject where parentObject.uniqName = :uniqName");
getIdQuery.setParameter("uniqName", parentObject.getUniqName());
getIdQuery.setCacheable(true);
Long id = (Long)getIdQuery.uniqueResult();
return id;
}
private Long lookupChildObjectId(SaveOrUpdateEvent event) {
Session session = event.getSession();
ChildObject childObject = (ChildObject)event.getObject();
Query getIdQuery = session.createQuery("select childObject.id from ChildObject childObject where childObject.uniqName = :uniqName");
getIdQuery.setParameter("uniqName", childObject.getUniqName());
getIdQuery.setCacheable(true);
Long id = (Long)getIdQuery.uniqueResult();
return id;
}
}
Is there a way to have Hibernate check if entities exist in the database for a mesh of transient objects based on natural IDs or some other identifying properties?
I'm using Hibernate 3.3.1.GA, Hibernate annotations 3.4.0.GA and Apache Derby 10.4.2.0.
Best regards,
Manfred