DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Oliver has posted 3 posts at DZone. View Full User Profile

Nested Set Implementation With Hibernate And Spring

03.29.2009
| 10307 views |
  • submit to reddit
        From: Oliver Paulus, http://blog.code-project.org

Simple implementation of nested set model with Hibernate and Spring. Simplified version of Hibernate "CaveatEmptor" sample (http://anonsvn.jboss.org/repos/hibernate/trunk/CaveatEmptor/HiA-SE)

NestedSetInterceptor for Hibernate:
package testproject;

import org.hibernate.*;
import org.hibernate.type.Type;
import org.apache.commons.logging.*;

import testproject.Folder;

import java.io.Serializable;
import java.util.*;

// mostly coming from new version of Hibernate "CaveatEmptor" sample
// http://anonsvn.jboss.org/repos/hibernate/trunk/CaveatEmptor/HiA-SE
@SuppressWarnings({"unchecked", "deprecation"})
public class NestedSetInterceptor extends EmptyInterceptor {
	private static final long serialVersionUID = 4073508315368385925L;

	private static Log log = LogFactory.getLog(NestedSetInterceptor.class);

        private Collection newNodes = new ArrayList();
        private Collection deletedNodes = new ArrayList();

        private SessionFactory sessionFactory;

        public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	public int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        if (entity instanceof Folder && ((Folder) entity).getThread() != 0) {
            Folder node = (Folder) entity;
            Folder oldParent = null;
            Folder newParent = null;

            // Find index of "parent" property
            int parentPropertyIndex = -1;
            for (int it = 0; it < propertyNames.length; it++) {
                String propertyName = propertyNames[it];
                if (propertyName.equals("parent")) parentPropertyIndex = it;
            }
            // Get old and current state for the "parent" property
            if (previousState != null) oldParent = (Folder) previousState[parentPropertyIndex];
            if (currentState != null) newParent = (Folder) currentState[parentPropertyIndex];

            // Move the node if parent changed
            if ( oldParent != null && !oldParent.equals(newParent) ) {
                // Delete the node from its current position (possibly also its children)
                log.debug("Node will be deleted: " + node);
                deletedNodes.add(node);
                if (newParent != null) {
                    // Place it in new position
                    log.debug("Node will be inserted: " + node);
                    newNodes.add(node);
                }
            }
        }
        return null;
     }

    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (entity instanceof Folder && ((Folder) entity).getThread() != 0) {
            log.debug("Node will be deleted: " + entity);
            deletedNodes.add(entity);
        }
        super.onDelete(entity, id, state, propertyNames, types);
    }

    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) throws CallbackException {
        if (entity instanceof Folder && ((Folder) entity).getThread() == 0) {
            log.debug("Node will be inserted: " + entity);
            newNodes.add(entity);
        }
        return false;
    }

    public void postFlush(Iterator entities) throws CallbackException {
        // Get a collection of all nodes in memory for synchronization
        Collection nodesInContext = new HashSet();
        while (entities.hasNext()) {
            Object o = entities.next();
            if (o instanceof Folder) {
                nodesInContext.add(o);
            }
        }
        
        // Session.connection() is deprecated - see
        // http://forum.hibernate.org/viewtopic.php?t=974518 
        // for more info.
        Session tmpSession = sessionFactory.openSession( sessionFactory.getCurrentSession().connection() );
        try {
            // Handle delete nodes in tree
            Collection alreadyDeleted = new ArrayList();
            for (Iterator it = deletedNodes.iterator(); it.hasNext();) {
                Folder node = (Folder) it.next();
                String entityName = tmpSession.getSessionFactory()
                        .getClassMetadata( node.getClass() ).getEntityName();

                if (alreadyDeleted.contains(node)) continue;

                // Node with children, deleting subtree
                log.debug("Deleting node: " + node);

                long moveOffset = 2;

                if (node.getRight() != (node.getLeft() + 1) ) {
                    // Node with children, deleting subtree
                    log.debug("Deleting subtree of node: " + node);

                    // Calculate update offset for other nodes
                    moveOffset = (int)Math.floor( (node.getRight() -
                                                       node.getLeft()) / 2 );
                    moveOffset = 2 * (1 + moveOffset);

                    // Add subtree nodes to already deleted collection, avoid duplicate updates
                    for (Iterator subit = deletedNodes.iterator(); subit.hasNext();) {
                        Folder n = (Folder) subit.next();
                        if (n.getThread() == node.getThread()
                            && n.getLeft() > node.getLeft()
                            && n.getLeft() < node.getRight() ) {
                            log.debug("Subtree node, mark as already deleted: " + n);
                            alreadyDeleted.add(n);
                        }
                    }
                }

                Query updateLeft = tmpSession
                        .createQuery("update " + entityName + " n set n.left = n.left - :offset " +
                                     "where n.thread = :thread and n.left > :right");
                updateLeft.setParameter("offset", moveOffset);
                updateLeft.setParameter("thread", node.getThread());
                updateLeft.setParameter("right", node.getRight());
                updateLeft.executeUpdate();
                for (Iterator itContext = nodesInContext.iterator(); itContext.hasNext();) {
                    Folder n = (Folder) itContext.next();
                    if (n.getThread() == node.getThread()
                        && n.getLeft() > node.getRight()) {
                        n.setLeft(n.getLeft() - moveOffset);
                        log.debug("Updated node in memory: " + n);
                    }
                }

                Query updateRight = tmpSession
                        .createQuery("update " + entityName + " n set n.right = n.right - :offset " +
                                     "where n.thread = :thread and n.right > :right");
                updateRight.setParameter("offset", moveOffset);
                updateRight.setParameter("thread", node.getThread());
                updateRight.setParameter("right", node.getRight());
                updateRight.executeUpdate();
                for (Iterator itContext = nodesInContext.iterator(); itContext.hasNext();) {
                    Folder n = (Folder) itContext.next();
                    if (n.getThread() == node.getThread()
                        && n.getRight() > node.getRight()) {
                        n.setRight(n.getRight() - moveOffset);
                        log.debug("Updated node in memory: " + n);
                    }
                }
            }

            // Handle new nodes in tree
            for (Iterator it = newNodes.iterator(); it.hasNext();) {
                Folder node = (Folder) it.next();
                String entityName = tmpSession.getSessionFactory()
                        .getClassMetadata( node.getClass() ).getEntityName();

                if (node.getParent() != null) {
                    // New child node
                    log.debug("New child node: " + node);
                    long parentThread = node.getParent().getThread();
                    long parentRight = node.getParent().getRight();

                    Query updateLeft = tmpSession
                            .createQuery("update " + entityName + " n set n.left = n.left + 2 " +
                                         "where n.thread = :thread and n.left > :right");
                    updateLeft.setParameter("thread", parentThread);
                    updateLeft.setParameter("right", parentRight);
                    updateLeft.executeUpdate();
                    for (Iterator itContext = nodesInContext.iterator(); itContext.hasNext();) {
                        Folder n = (Folder) itContext.next();
                        if (n.getThread() == parentThread && n.getLeft() > parentRight) {
                            n.setLeft(n.getLeft() + 2);
                            log.debug("Updated node in memory: " + n);
                        }
                    }

                    Query updateRight = tmpSession
                            .createQuery("update " + entityName + " n set n.right = n.right + 2 " +
                                         "where n.thread = :thread and n.right >= :right");
                    updateRight.setParameter("thread", parentThread);
                    updateRight.setParameter("right", parentRight);
                    updateRight.executeUpdate();
                    for (Iterator itContext = nodesInContext.iterator(); itContext.hasNext();) {
                        Folder n = (Folder) itContext.next();
                        if (n.getThread() == parentThread && n.getRight() >= parentRight) {
                            n.setRight(n.getRight() + 2);
                            log.debug("Updated node in memory: " + n);
                        }
                    }

                    Query updateNode = tmpSession
                            .createQuery("update " + entityName + " n set n.thread = :thread, " +
                                         "n.left = :left, " +
                                         "n.right = :right " +
                                         "where n.id = :nid");
                    updateNode.setParameter("thread", parentThread);
                    updateNode.setParameter("left", parentRight);
                    updateNode.setParameter("right", parentRight + 1);
                    updateNode.setParameter("nid", node.getId());
                    updateNode.executeUpdate();

                    node.setThread(parentThread);
                    node.setLeft(parentRight);
                    node.setRight(parentRight + 1);
                    log.debug("Inserted node: " + node);
                } else {
                    // New root node, hence new thread (thread identifier is root node identifier)
                    log.debug("New root node: " + node);
                    node.setThread(node.getId());

                    // Set thread in database
                    Query updateInDB =
                            tmpSession.createQuery("update " + entityName + " n set n.thread = :thread " +
                                                   "where n.id = :nid");
                    updateInDB.setParameter("thread", node.getId());
                    updateInDB.setParameter("nid", node.getId());
                    updateInDB.executeUpdate();
                    log.debug("Inserted node: " + node);
                }
            }

        } finally {
            tmpSession.close();
            newNodes.clear();
            deletedNodes.clear();
        }
    }
}

Domain model class "Folder":
package testproject;

import java.util.ArrayList;
import java.util.List;

public class Folder extends DomainBase {
	private static final long serialVersionUID = 3070223622468427374L;
	private String name;
	private Folder parent;
	private List<Folder> children;
	// nested set
	private long thread;
	private long left = 1;
	private long right = 2;

	public Folder() {
		this.children = new ArrayList<Folder>();
	}

	public Folder(String name) {
		this();
		this.name = name;
	}
	
	public List<Folder> getChildren() {
		return this.children;
	}

	public void setChildren(List<Folder> children) {
		this.children = children;
	}

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Folder getParent() {
		return this.parent;
	}

	public void setParent(Folder parent) {
		this.parent = parent;
	}

	public long getThread() {
		return this.thread;
	}

	public void setThread(long thread) {
		this.thread = thread;
	}

	public long getLeft() {
		return this.left;
	}

	public void setLeft(long left) {
		this.left = left;
	}

	public long getRight() {
		return this.right;
	}

	public void setRight(long right) {
		this.right = right;
	}
	
	public void addChild(Folder child) {
		if (child == null)
		    throw new IllegalArgumentException("Can't add a null node as child.");

		// Remove from old parent - one-to-many multiplicity
		if (child.getParent() != null)
		    child.getParent().getChildren().remove(child);

		// Set parent in child
		child.setParent(this);

		// Set child in parent
		this.getChildren().add(child);
    	}
	
    	public void removeChild(Folder child) {
		if (child == null) return;
		// Remove from parent and set parent to null
		if (child.getParent() != null)
		    child.getParent().getChildren().remove(child);
		child.setParent(null);
    	}
}

Hibernate configuration (Folder.hbm.xml):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="testproject">
	<class name="Folder" table="FOLDER">
	    	<id name="id" type="long" column="ID">
			<generator class="native" />
		</id>
		
	       	<property name="name" column="NAME" type="string" not-null="true" length="250" unique-key="UQ_FOLDER_NAME_PARENT" />
		<many-to-one name="parent" column="PARENT_ID" unique-key="UQ_FOLDER_NAME_PARENT" cascade="save-update, persist, merge"/>
	       	
	       	<bag name="children" inverse="true" cascade="all-delete-orphan">
			<key column="parent_id" />
			<one-to-many class="Folder" />
		</bag>
	       	
	       	<property name="thread" column="NS_THREAD"/>
		<property name="left" column="NS_LEFT"/>
		<property name="right" column="NS_RIGHT"/>
	       	
	       	<property type="timestamp" name="created" column="CREATED" generated="insert"/>
	       	<property type="timestamp" name="modified" column="MODIFIED" generated="always"/>
	</class>
</hibernate-mapping>

Spring configuration:
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       					   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
	default-autowire="byName" >

	<!-- sessionFactory configuration, data source, ... -->

	<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory" />
		<property name="entityInterceptor">
			<bean name="nestedSetInterceptor" class="testproject.NestedSetInterceptor">
				<property name="sessionFactory" ref="sessionFactory"/>
			</bean>
		</property>
	</bean>
</beans>