вторник, 24 мая 2011 г.

Немного о hibernate

В процессе работы с Hibernate столкнулся с весьма неординарной проблемой. Вкратце на проекте нужно было организовать редактирование данных в древовидной модели. Т.е. в таблице есть поле которое на которое ссылаются прочие записи в этой таблице (пример XML документ или файловая система). Пример маппинга до решения проблемы:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC 
"-//Hibernate/Hibernate Mapping DTD 3.0//EN" 
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
 <hibernate-mapping>
  <class name="com.full.package.name.Node" 
   table="calulation_tree_nodes">
   <id name="id" column="calulation_tree_node_id" type="integer">
    <generator class="native"/>
   </id>
...
  <many-to-one name="parent" column="parent"     class="com.full.package.name.Node"/>   <many-to-one name="dataNode" column="data_id"     class="com.full.package.name.Node"/>
...
  <set name="children" inverse="true"      cascade="all,delete-orphan" lazy="true" fetch="join"     order-by="node_name desc" batch-size="20">     <key column="parent"/>     <one-to-many class="com.full.package.name.Node"/>    </set>   </class>  </hibernate-mapping>
Моя же основная задача свелась к смене родителя у коллекции детей при добавлении промежуточного узла. Сделать стандартными средствами Hibernate это оказалось невозможным - При создании Set'a из новой коллекции и добавления туда child'ов выскакивал Exception при сохранении (org.hibernate.ObjectDeletedException: deleted object would be re-saved by cascade (remove deleted object from associations)) При работе с существующей коллекцией (клонирование элементов, очистка Set'a, вставка промежуточного nod'а и заполнение его клонами) также не дало результата - NonUniqueObjectInSession. Дело в том что Hibernate считает операцию удаления приоритетнее, т.е. сначала выполняет каскадирование помещает объект в сессию для удаления его, а потом находит в сессии точно такой же присоединенный объект к другому узлу дерева - в результате Exception. Вылечить это как оказалось можно только использованием своей специфической Usertype-collection. Маппнг Хмл притерпел следующие изменения:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC 
"-//Hibernate/Hibernate Mapping DTD 3.0//EN" 
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
 <hibernate-mapping>
  <class name="com.full.package.name.Node" 
   table="calulation_tree_nodes">
   <id name="id" column="calulation_tree_node_id" type="integer">
    <generator class="native"/>
   </id>
...
  <many-to-one name="parent" column="parent"     class="com.full.package.name.Node"/>   <many-to-one name="dataNode" column="data_id"     class="com.full.package.name.Node"/>
...
  <set name="children"     collection-type="com.full.package.name.ChangeParentsCollection"     inverse="false"      cascade="all,delete-orphan" lazy="true" fetch="join"     order-by="node_name desc" batch-size="20">     <key column="parent"/>     <one-to-many class="com.full.package.name.Node"/>    </set>   </class>  </hibernate-mapping>

Ну и для того чтобы Код Java работал и Hibernate увидел нашу пользовательскую коллекцию мы должны унаследовать интерфейс UserCollectionType В Итоге пользовательский клас получился таким
public class ChangeParentsCollection extends 
PersistentSet implements UserCollectionType {
    
    /**
     * 
     */
    private static final long serialVersionUID = -5852154001164009614L;

    public ChangeParentsCollection() {
        super();
    }
    public ChangeParentsCollection(SessionImplementor session) {
        super(session);
    }        
    public ChangeParentsCollection(SessionImplementor session, 
      Set set) {
        super(session, set);
    }    

    @Override
    public Collection getOrphans(Serializable snapshot, 
      String entityName) throws HibernateException {
        final Collection orphans = super.getOrphans(snapshot, entityName);
        if(orphans != null){
            final Iterator iterator = orphans.iterator();
            while (iterator.hasNext()) {
                final Object item = iterator.next();
                if (item != null && item instanceof Node) {
                    final Node node = (Node) item;
                    if(node.getParent() == null){
                        iterator.remove();
                    }                   
                }
            }
        }         
        return orphans;
    }

    // standard wrapper for collection type.
    @Override
    public PersistentCollection wrap(SessionImplementor session, 
      Object collection) {
        return new ChangeParentsCollection(session, (Set)collection);
    }

    @Override
    public boolean contains(Object collection, Object entity) {
        Set set = (Set)collection;        
        return set.contains(entity);
    }

    // could be common for all collection implementations.
    @Override
    public Iterator getElementsIterator(Object collection) {
        return ((Set)collection).iterator();
    }

    // common for list-like collections.
    @Override
    public Object indexOf(Object collection, Object entity) {
        return null;
    }

    // factory method for certain collection type.
    @Override
    public Object instantiate(int anticipatedSize) {
        return new HashSet();  }

    @Override
    public PersistentCollection instantiate(SessionImplementor session, 
      CollectionPersister persister) throws HibernateException {
        return new ChangeParentsCollection(session);
    }

    // could be common implementation for all collection implementations
    @Override
    public Object replaceElements(Object original, Object target,
      CollectionPersister persister, Object owner,
      Map copyCache, SessionImplementor session) 
      throws HibernateException {        
        Set result = (Set) target;     
        result.clear();
        if(original != null && original instanceof Collection){
            Iterator it = ((Collection)original).iterator();
            while(it.hasNext()){
                final Object item = it.next();
                if(item != null){
                    result.add(item);
                }
            }            
        }
        return result;        
    }
}
Inverse был изменен для того, чтобы отношением управлял родительский объект, а не наоборот. Снижения производительности это не вызвало, так как inverse больше используется при вставке элементов. Ссылочка на документацию об inverse В коде при очистке коллекции дочерних элементов ссылка на Родителя устанавливается в null, чтобы в последствии этот элемент был найден и удален из сессии вручную. Тесты показали, что все замечательно работает.

Ах, да, совсем забыл это решение проблемки описаной в hibernate а именно
Persistence through cascading should have higher precedence than orphan-delete
moving child between parents in tree-like structure

Там про это можно почитать подробнее...

Комментариев нет:

Отправить комментарий