package com.breeze.hib; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import org.hibernate.FlushMode; import org.hibernate.JDBCException; import org.hibernate.PropertyValueException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.metadata.ClassMetadata; import com.breeze.metadata.DataType; import com.breeze.metadata.Metadata; import com.breeze.save.*; public class HibernateSaveProcessor extends SaveProcessor { private SessionFactory _sessionFactory; private Session _session; private RelationshipFixer _fixer; private List<String> _possibleErrors = new ArrayList<String>(); /** * @param metadata * @param sessionFactory */ public HibernateSaveProcessor(Metadata metadata, SessionFactory sessionFactory) { super(metadata); _sessionFactory = sessionFactory; } /** * Persist the changes to the entities in the SaveWorkState that is passed in. * This implements the abstract method in SaveProcessor. * After the completion of this method the saveWorkState.toSaveResult method may be called the will * return a SaveResult instance. * This method both persists the entities and creates the collection of KeyMappings, * which map the temporary keys to their real generated keys. * Note that this method sets session.FlushMode = FlushMode.MANUAL, so manual flushes are required. * @param saveWorkState */ @Override protected void saveChangesCore(SaveWorkState saveWorkState) { _session = _sessionFactory.openSession(); _session.setFlushMode(FlushMode.MANUAL); Transaction tx = _session.getTransaction(); boolean hasExistingTransaction = tx.isActive(); if (!hasExistingTransaction) tx.begin(); try { // Relate entities in the saveMap to other entities, so Hibernate can save the FK values. _fixer = new RelationshipFixer(saveWorkState, _session); _fixer.fixupRelationships(); // At this point all entities are hooked up but are not yet in the session. setSaveState(SaveState.AfterFixup); // Allow subclass to process entities before we save them saveWorkState.beforeSaveEntities(); List<EntityInfo> saveOrder = _fixer.sortDependencies(); processSaves(saveOrder); // At this point all entities are hooked up and in the session, and // all tempIds have been replaced with real ids. // Final chance to process entities before we save them - all entities // have been added to the session. setSaveState(SaveState.BeforeCommit); saveWorkState.beforeCommit(_session); _session.flush(); refreshFromSession(saveWorkState); if (!hasExistingTransaction) tx.commit(); // so that serialization of saveResult doesn't have issues. _fixer.removeRelationships(); } catch (EntityErrorsException eee) { if (tx.isActive()) tx.rollback(); throw eee; } catch (PropertyValueException pve) { // Hibernate can throw this if (tx.isActive()) tx.rollback(); EntityError entityError = new EntityError("PropertyValueException", pve.getEntityName(), null, pve.getPropertyName(), pve.getMessage()); throw new EntityErrorsException(entityError); } catch (Exception ex) { if (tx.isActive()) tx.rollback(); String msg = "Save exception: "; if (ex instanceof JDBCException) { msg += "SQLException: " + ((JDBCException) ex).getSQLException().getMessage(); } else { msg += ex.getMessage(); // most hibernate exceptions appear here } if (_possibleErrors.size() > 0) { msg += ". Possible errors: " + _possibleErrors.toString() + " "; } throw new RuntimeException(msg, ex); } finally { // if (!hasExistingTransaction) tx.Dispose(); _session.close(); } } @Override public void processRelationships(EntityInfo entityInfo, boolean removeMode) { // _fixer will not be initialized until just before beforeSaveEntities is called // so it will not be available for beforeSaveEntity calls. SaveState saveState = getSaveState(); if (saveState == SaveState.BeforeFixup) return; _fixer.processRelationships(entityInfo, removeMode); if (saveState == SaveState.BeforeCommit) { processEntity(entityInfo); } } // TODO: determine why this is different from getIdentifier commented out below. // May have to do with this method only getting called for single part keys. @Override public Object getIdentifier(Object entity) { Object id = getClassMetadata(entity.getClass()).getIdentifier(entity, null); return id != null ? id : null; } /** * Persist the changes to the entities in the saveOrder. * @param saveOrder */ protected void processSaves(List<EntityInfo> saveOrder) { for (EntityInfo entityInfo : saveOrder) { processEntity(entityInfo); } } /** * Add, update, or delete the entity according to its EntityState. * @param entityInfo * @param classMeta */ protected void processEntity(EntityInfo entityInfo) { Object entity = entityInfo.entity; ClassMetadata classMeta = getClassMetadata(entity.getClass()); EntityState state = entityInfo.entityState; // Restore the old value of the concurrency column so Hibernate will be able to save the entity if (classMeta.isVersioned()) { restoreOldVersionValue(entityInfo, classMeta); } if (state == EntityState.Modified) { _session.update(entity); } else if (state == EntityState.Added) { _session.save(entity); } else if (state == EntityState.Deleted) { _session.delete(entity); } else { // Ignore EntityState.Unchanged. Too many problems using session.Lock or session.Merge //session.Lock(entity, LockMode.None); } } /** * Restore the old value of the concurrency column so Hibernate will save the entity. * Otherwise it will complain because Breeze has already changed the value. * @param entityInfo * @param classMeta */ protected void restoreOldVersionValue(EntityInfo entityInfo, ClassMetadata classMeta) { if (entityInfo.originalValuesMap == null || entityInfo.originalValuesMap.size() == 0) return; int vcol = classMeta.getVersionProperty(); String vname = classMeta.getPropertyNames()[vcol]; if (entityInfo.originalValuesMap.containsKey(vname)) { Object oldVersion = entityInfo.originalValuesMap.get(vname); Object entity = entityInfo.entity; if (oldVersion == null) { _possibleErrors.add("Hibernate does not support 'null' version properties. " + "Entity: " + entity + ", Property: " + vname); } Class versionClazz = classMeta.getPropertyTypes()[vcol].getReturnedClass(); DataType dataType = DataType.fromClass(versionClazz); Object oldValue = DataType.coerceData(oldVersion, dataType); classMeta.setPropertyValue(entity, vname, oldValue); } } /** * Refresh the entities from the database. This picks up changes due to triggers, etc. * and makes Hibernate update the foreign keys. * @param saveMap */ protected void refreshFromSession(SaveWorkState saveWorkState) { for (Entry<Class, List<EntityInfo>> entry : saveWorkState.entrySet()) { for (EntityInfo entityInfo : entry.getValue()) { if (entityInfo.entityState == EntityState.Added || entityInfo.entityState == EntityState.Modified) { _session.refresh(entityInfo.entity); } } } } // perf private Class _classCached; private ClassMetadata _classMetadataCached; private ClassMetadata getClassMetadata(Class clazz) { // perf enhancement - this method gets called a lot in loops. if (clazz != _classCached) { _classCached = clazz; _classMetadataCached = _sessionFactory.getClassMetadata(clazz); } return _classMetadataCached; } // /** // * Get the identifier value for the entity. If the entity does not have an // * identifier property, or natural identifiers defined, then the entity itself is returned. // * @param entity // * @param meta // * @return // */ // protected Object getIdentifier(Object entity) { // Class type = entity.getClass(); // meta = getClassMetadata(type); // // Type idType = meta.getIdentifierType(); // if (idType != null) { // Serializable id = meta.getIdentifier(entity, null); // if (idType.isComponentType()) { // ComponentType compType = (ComponentType) idType; // return compType.getPropertyValues(id, EntityMode.POJO); // } else { // return id; // } // } else if (meta.hasNaturalIdentifier()) { // int[] idprops = meta.getNaturalIdentifierProperties(); // Object[] values = meta.getPropertyValues(entity); // Object[] idvalues = new Object[idprops.length]; // for (int i = 0; i < idprops.length; i++) { // idvalues[i] = values[idprops[i]]; // } // return idvalues; // } // return entity; // } }