Download Sources
from here until I find the time to publish this. You can do what
you like with it, except publishing it under your own name.
That means this is freeware, but I claim its authorship.
This document tries to make the use of JpaTreeDao library quick and easy. It is not an introduction to Nested Sets and Closure Table concepts. Please read about this on the internet. I learnt a lot from (and want to thank) Bill Karwin, who published articles and slides about trees in database tables.What do you need to know about these methods when using JpaTreeDao?
Uses an additional paths-table where tree structures
(references, level, order) are stored. No additional fields
are needed in your entity.
Through that, a tree node entity can belong to more than
one tree, lets call it tree-aspect. For each
tree-aspect you will need one additional paths-table.
Nevertheless several roots (that can not share nodes) can
exist in just one paths-table.
Adjacency List is the traditional way to hold trees in
database tables. This method uses a simple parent reference in
every entity. With a JPA back-reference, a children list would
be available directly in such an entity. Such trees are nice
for subsequent tree branch expansion, but complicated when you
need to read the whole tree at once (e.g. for a report). JpaTreeDao
doesn't provide an Adjacency List implementation.
Entity,
Node, Tree Node |
The Java object representation of a database record in memory. |
Children | The direct children of a parent, not the
whole tree below it. |
Temporal | Is used to indicate that no physical database deletion takes place when a node is removed. Records are deleted by historicizing them, i.e. setting a validTo timestamp property. There are DAO methods that provide access to historicized nodes and allow recovery. You can even override DAO methods to implement another deletion-mechanism than validFrom and validTo fields (see "Another Recoverable Remove Method"). |
Note: Although this library is based on JPA EntityManager, you can use it with Hibernate Session, too (older Hibernate versions). Look for the
Java class in test directory. It is an alternative implementation of the DbSession interface. It contains a workaround for the differences between HQL and JPQL. There are also unit-tests that prove that everything works with HQL.
- DbSessionHibernateImpl
You need to pass the entity class and the database layer (as instance) to the DAO at construction time. Additionally you can pass in the name of the according JPQL entity (differing from the simple class-names of the Java entity). This is for the case that your entities are just interfaces and have a backing implementation with another name. JpaTreeDao uses entityClass.getSimpleName() as default entityName. While with NestedSetsTreeDao you can not get rid of children order, you can tell the ClosureTableTreeDao if order matters or not.public NestedSetsTreeDao( Class<? extends NestedSetsTreeNode> entityClass, DbSession session) public NestedSetsTreeDao( Class<? extends NestedSetsTreeNode> entityClass, String entityName, DbSession session)
public ClosureTableTreeDao( Class<? extends ClosureTableTreeNode> treeNodeEntityClass, Class<? extends TreePath> treePathsEntityClass, boolean orderIndexMatters, DbSession session) public ClosureTableTreeDao( Class<? extends ClosureTableTreeNode> treeNodeEntityClass, String treeNodeEntity, Class<? extends TreePath> treePathEntityClass, String treePathEntity, boolean orderIndexMatters, DbSession session)
@Entity public class Person implements NestedSetsTreeNode { @Id @GeneratedValue private String id; // primary key private String name; // some person property @ManyToOne(targetEntity = Person.class) private NestedSetsTreeNode topLevel; // root reference private int lft; // NestedSets index - "left" would be a SQL keyword! private int rgt; // NestedSets index - "right" would be a SQL keyword! public Person() { } public Person(String name) { this.name = name; } @Override public String getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public NestedSetsTreeNode getTopLevel() { return topLevel; } @Override public void setTopLevel(NestedSetsTreeNode topLevel) { this.topLevel = topLevel; } @Override public int getLeft() { return lft; } @Override public void setLeft(int left) { this.lft = left; } @Override public int getRight() { return rgt; } @Override public void setRight(int right) { this.rgt = right; } @Override public Person clone() { return new Person(getName()); } }
public class DbSessionJpaImpl implements DbSession { private final EntityManager entityManager; public DbSessionJpaImpl(EntityManager entityManager) { this.entityManager = entityManager; } @Override public Object get(Class<?> entityClass, Serializable id) { return entityManager.find(entityClass, id); } @Override public Object save(Object node) { return entityManager.merge(node); } @Override public void flush() { entityManager.flush(); } @Override public void refresh(Object node) { entityManager.refresh(node); } @Override public void delete(Object node) { entityManager.remove(node); } @Override public List<?> queryList(String queryText, Object[] parameters) { return query(queryText, parameters).getResultList(); } @Override public int queryCount(String queryText, Object[] parameters) { List result = queryList(queryText, parameters); return ((Number) result.get(0)).intValue(); } @Override public void executeUpdate(String sqlCommand, Object[] parameters) { Query query = query(sqlCommand, parameters); query.executeUpdate(); } private Query query(String queryText, Object[] parameters) { Query query = entityManager.createQuery(queryText); if (parameters != null) { int i = 1; for (Object parameter : parameters) { if (parameter == null) throw new IllegalArgumentException("Binding parameter at position "+i+" can not be null: "+queryText); query.setParameter(i, parameter); i++; } } return query; } }
This works with a JPA persistence.xml in META-INF directory (see test/resources package). Following example uses Hiberate-EntityManager (see Maven POM for Version) and H2 as in-memory database. To avoid auto-scanning for classes annotated with @Entity you need to remove hibernate.archive.autodetection and use <class>your.package.YourEntity</class> elements instead (refer to persistence.xml schema).final String PERSISTENCE_UNIT_NAME = "your-persistence-unit-name"; final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME); final EntityManager entityManager = entityManagerFactory.createEntityManager();
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="your-persistence-unit-name"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <properties> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> <property name="javax.persistence.jdbc.user" value="sa"/> <property name="javax.persistence.jdbc.password" value=""/> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE"/> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <!-- Make Hibernate scan for annotated classes --> <property name="hibernate.archive.autodetection" value="class" /> </properties> </persistence-unit> </persistence>
How to do this with Hibernate-Session you can see in test-package in classes namedfinal EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); final DbSession dbSession = new DbSessionJpaImpl(entityManager); final NestedSetsTreeDao dao = new NestedSetsTreeDao(Person.class, dbSession); // add a constraint that checks that a person's name is unique across the whole tree dao.setUniqueTreeConstraint(new UniqueWholeTreeConstraintImpl( new String [][] { { "name" } }, false)); // create a tree final NestedSetsTreeNode walter = dao.createRoot(new Person("Walter")); assert dao.getRoots().size() == 1; // insert root children final NestedSetsTreeNode linda = dao.addChild(walter, new Person("Linda")); final NestedSetsTreeNode mary = dao.addChild(walter, new Person"Mary")); assert dao.size(walter) == 3; // add children to a child final NestedSetsTreeNode peter = dao.addChild(mary, new Person("Peter")); final NestedSetsTreeNode paul = dao.addChild(mary, new Person("Paul")); assert dao.size(walter) == 5; assert dao.size(mary) == 3; // retrieve children lists final List<NestedSetsTreeNode> childrenOfWalter = dao.getChildren(walter); assert childrenOfWalter.size() == 2 && childrenOfWalter.get(0).equals(linda) && childrenOfWalter.get(1).equals(mary); final List<NestedSetsTreeNode> childrenOfMary = dao.getChildren(mary); assert childrenOfMary.size() == 2 && childrenOfMary.get(0).equals(peter) && childrenOfMary.get(1).equals(paul); transaction.commit();
You need the node entity and another entity that describes the tree structure (so there will be two database tables). The first needs no additional tree properties, just clone() is required for implementing interface ClosureTableTreeNode. The second must implement interface TreePath.
Following is an example node entity (see also class PersonCtt
in test-package):
@Entity public class Person implements ClosureTableTreeNode { @Id @GeneratedValue private String id; @Column(nullable = false) private String name; public Person() { } public Person(String name) { this.name = name; } @Override public String getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public ClosureTableTreeNode clone() { return new Person(getName()); } }
You can access the node table through other mechanisms than the DAO, but only nodes that were added through the DAO will be in tree. Adding a node through the DAO will also make it persistent (if it is not yet).
This is an example TreePath implementation (see also
class PersonOrganizationalTreePath in test-package):
The primary key of PersonTreePath is composed by the primary keys of ancestor and descendant (JPA @IdCass annotation). Here is an implementation for primary keys of type String (see also class CompositePersonTreePathId in test-package):@Entity @IdClass(CompositePersonTreePathId.class) public class PersonTreePath implements TreePath { @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "ancestor", nullable = false) private ClosureTableTreeNode ancestor; @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "descendant", nullable = false) private ClosureTableTreeNode descendant; @Column(nullable = false) private int depth; private int orderIndex; @Override public ClosureTableTreeNode getAncestor() { return ancestor; } @Override public void setAncestor(ClosureTableTreeNode ancestor) { this.ancestor = ancestor; } @Override public ClosureTableTreeNode getDescendant() { return descendant; } @Override public void setDescendant(ClosureTableTreeNode descendant) { this.descendant = descendant; } @Override public int getDepth() { return depth; } @Override public void setDepth(int depth) { this.depth = depth; } @Override public int getOrderIndex() { return orderIndex; } @Override public void setOrderIndex(int position) { this.orderIndex = position; } }
public class CompositePersonTreePathId implements Serializable { private String ancestor; // must have same name as TreePathImpl.ancestor, and data-type like TreePathImpl.ancestor.id private String descendant; // must have same name as TreePathImpl.descendant, and data-type like TreePathImpl.descendant.id public String getAncestor() { return ancestor; } public void setAncestor(String ancestorId) { this.ancestor = ancestorId; } public String getDescendant() { return descendant; } public void setDescendant(String descendantId) { this.descendant = descendantId; } @Override public boolean equals(Object o) { if (o == this) return true; if (o instanceof CompositePersonTreePathId == false) return false; final CompositePersonTreePathId other = (CompositePersonTreePathId) o; return other.getDescendant().equals(getDescendant()) && other.getAncestor().equals(getAncestor()); } @Override public int hashCode() { return getDescendant().hashCode() * 31 + getAncestor().hashCode(); } }
This is the same as with Nested Sets, see there.
Ready to start programming with the DAO. Its name is ClosureTableTreeDao (temporal extension is TemporalClosureTableTreeDao).
Following sample code shows how two tree-aspects are built
upon the same node table. The first is a organizational
hierarchy of persons, the usual chief/staff thing, while the
second is a functional one, lets say an expert/beginner thing.
Mind that you need two different TreePath
implementations (tree-aspects) for that! You can find this
example code (and the two TreePath implementations
used in it) in test-package in class
final EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); final DbSession dbSession = new DbSessionJpaImpl(entityManager);
final ClosureTableTreeDao organizationalDao = new ClosureTableTreeDao(Person.class, PersonOrganisationalTreePath.class, true, dbSession); final ClosureTableTreeDao functionalDao = new ClosureTableTreeDao(Person.class, PersonFunctionalTreePath.class, true, dbSession); // create persons and build organizational tree final ClosureTableTreeNode walter = organizationalDao.createRoot(new Person("Walter")); final ClosureTableTreeNode linda = organizationalDao.addChild(walter, new Person("Linda")); final ClosureTableTreeNode mary = organizationalDao.addChild(walter, new Person("Mary")); final ClosureTableTreeNode peter = organizationalDao.addChild(mary, new Person("Peter")); final ClosureTableTreeNode paul = organizationalDao.addChild(mary, new Person("Paul")); final List<ClosureTableTreeNode> organizationalChildrenOfWalter = organizationalDao.getChildren(walter); assert organizationalChildrenOfWalter.size() == 2 && organizationalChildrenOfWalter.get(0).equals(linda) && organizationalChildrenOfWalter.get(1).equals(mary); final List<ClosureTableTreeNode> organizationalChildrenOfMary = organizationalDao.getChildren(mary); assert organizationalChildrenOfMary.size() == 2 && organizationalChildrenOfMary.get(0).equals(peter) && organizationalChildrenOfMary.get(1).equals(paul); // build functional tree using the same persons functionalDao.createRoot(walter); functionalDao.createRoot(linda); // "Linda" is another root functionalDao.addChild(linda, peter); functionalDao.addChild(linda, paul); functionalDao.addChild(walter, mary); final List<ClosureTableTreeNode> functionalChildrenOfWalter = functionalDao.getChildren(walter); assert functionalChildrenOfWalter.size() == 1 && functionalChildrenOfWalter.get(0).equals(mary); final List<ClosureTableTreeNode> functionalChildrenOfLinda = functionalDao.getChildren(linda); assert functionalChildrenOfLinda.size() == 2 && functionalChildrenOfLinda.get(0).equals(peter) && functionalChildrenOfLinda.get(1).equals(paul);
transaction.commit();
Your node entity must implement interface TemporalNestedSestTreeNode, and you must pass the names of the Temporal properties to the DAO constructor (typically "validFrom" and "validTo").
Following example is different, it shows a special case, not a typical application. The validTo property is redirected to an endValid property, and validFrom is dismissed. This is to demonstrate how to use the temporal extension when your entity can't provide a property with name "validTo" for some reason, and you won't support future validity (validFrom).
Here is the example node entity. Mind how you can implement
Temporal, leaving out validFrom with do-nothing
implementations, and using @Transient annotations for all Temporal
methods.
And here is an example application for that entity, you find it in test-package in class@Entity public class Person implements TemporalNestedSetsTreeNode { @Id @GeneratedValue private String id; @Column(nullable = false) private String name; @ManyToOne(targetEntity = Person.class) private NestedSetsTreeNode topLevel; private int lft; private int rgt; @Temporal(TemporalType.TIMESTAMP) private Date endValid; // the "validTo" substitute, "validFrom" is not present at all public Person() { } public Person(String name) { this.name = name; } @Override public String getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getEndValid() { return endValid; } public void setEndValid(Date endValid) { this.endValid = endValid; } @Override public NestedSetsTreeNode getTopLevel() { return topLevel; } @Override public void setTopLevel(NestedSetsTreeNode topLevel) { this.topLevel = topLevel; } @Override public int getLeft() { return lft; } @Override public void setLeft(int left) { this.lft = left; } @Override public int getRight() { return rgt; } @Override public void setRight(int right) { this.rgt = right; } @Override public Person clone() { return new Person(getName()); } @Transient // will not generate a validTo field @Override public Date getValidTo() { // redirect to endValid return endValid; } @Transient @Override public void setValidTo(Date validTo) { // redirect to endValid this.endValid = validTo; } @Transient @Override public Date getValidFrom() { // do nothing than implement Temporal return null; } @Transient @Override public void setValidFrom(Date validFrom) { // do nothing than implement Temporal } }
final EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); final DbSession dbSession = new DbSessionJpaImpl(entityManager); final TemporalNestedSetsTreeDao dao = new TemporalNestedSetsTreeDao( Person.class, null, // entity does not support validFrom property "endValid", // entity's substitute for validTo property, needed in JPQL queries dbSession); final NestedSetsTreeNode walter = dao.createRoot(new Person("Walter")); final NestedSetsTreeNode peter = dao.addChild(walter, new Person("Peter")); final NestedSetsTreeNode paul = dao.addChild(walter, new Person("Paul")); final NestedSetsTreeNode mary = dao.addChild(walter, new Person("Mary")); final List<NestedSetsTreeNode> childrenOfWalter = dao.getChildren(walter); assert childrenOfWalter.size() == 3 && childrenOfWalter.get(0).equals(peter) && childrenOfWalter.get(1).equals(paul) && childrenOfWalter.get(2).equals(mary); dao.remove(peter); final List<NestedSetsTreeNode> newChildrenOfWalter = dao.getChildren(walter); assert newChildrenOfWalter.size() == 2 && newChildrenOfWalter.get(0).equals(paul) && newChildrenOfWalter.get(1).equals(mary); // recover node final Map<String,Object> criteria = new Hashtable<String,Object>(); criteria.put("name", "Peter"); final List<NestedSetsTreeNode> result = dao.findRemoved(walter, criteria); final Person removedPeter = (Person) result.get(0); dao.unremove(removedPeter);
transaction.commit();
@Entity @IdClass(CompositePersonTreePathId.class) public class PersonTreePath implements TemporalTreePath { @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "ancestor", nullable = false) private ClosureTableTreeNode ancestor; @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "descendant", nullable = false) private ClosureTableTreeNode descendant;
@Column(nullable = false) private int depth; private int orderIndex; @Temporal(TemporalType.TIMESTAMP) private Date validFrom; @Temporal(TemporalType.TIMESTAMP) private Date validTo; @Override public ClosureTableTreeNode getAncestor() { return ancestor; } @Override public void setAncestor(ClosureTableTreeNode ancestor) { this.ancestor = ancestor; } @Override public ClosureTableTreeNode getDescendant() { return descendant; } @Override public void setDescendant(ClosureTableTreeNode descendant) { this.descendant = descendant; }
@Override public int getDepth() { return depth; } @Override public void setDepth(int depth) { this.depth = depth; } @Override public int getOrderIndex() { return orderIndex; } @Override public void setOrderIndex(int position) { this.orderIndex = position; } @Override public Date getValidTo() { return validTo; } @Override public void setValidTo(Date validTo) { this.validTo = validTo; }
@Override public Date getValidFrom() { return validFrom; } @Override public void setValidFrom(Date validFrom) { this.validFrom = validFrom; } }
final EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); final DbSession dbSession = new DbSessionJpaImpl(entityManager); final TemporalClosureTableTreeDao dao = new TemporalClosureTableTreeDao( Person.class, PersonTreePath.class, true, Temporal.VALID_FROM, Temporal.VALID_TO, dbSession); final ClosureTableTreeNode walter = dao.createRoot(new Person("Walter")); dao.remove(walter); assert dao.getRoots().size() == 0; assert dao.getAllRoots().size() == 1; // it is still there!
dao.unremove(walter);
assert dao.getRoots().size() == 1;
transaction.commit();
Remarks:public interface TreeDao <N extends TreeNode> { N createRoot(N root) throws UniqueConstraintViolationException; List<N> getRoots(); N addChild(N parent, N child) throws UniqueConstraintViolationException; N addChildAt(N parent, N child, int position) throws UniqueConstraintViolationException; N addChildBefore(N sibling, N child) throws UniqueConstraintViolationException; void update(N entity) throws UniqueConstraintViolationException; void remove(N node); void removeAll(); N find(Serializable id); List<N> find(N parent, Map<String,Object> criteria); int size(N tree); N getRoot(N node); N getParent(N node); List<N> getPath(N node); List<N> getChildren(N parent); int getChildCount(N parent); boolean isEqualToOrChildOf(N child, N parent); boolean isChildOf(N child, N parent); boolean isRoot(N entity); boolean isLeaf(N node); int getLevel(N node); List<N> getTreeCacheable(N parent); List<N> findSubTree(N parent, List<N> treeCacheable); List<N> findDirectChildren(List<N> treeCacheable); List<N> getTree(N parent); void move(N node, N newParent) throws UniqueConstraintViolationException; void moveTo(N node, N parent, int position) throws UniqueConstraintViolationException; void moveBefore(N node, N sibling) throws UniqueConstraintViolationException; void moveToBeRoot(N child) throws UniqueConstraintViolationException; N copy(N node, N parent, N copiedNodeTemplate) throws UniqueConstraintViolationException; N copyTo(N node, N parent, int position, N copiedNodeTemplate) throws UniqueConstraintViolationException; N copyBefore(N node, N sibling, N copiedNodeTemplate) throws UniqueConstraintViolationException; N copyToBeRoot(N child, N copiedNodeTemplate) throws UniqueConstraintViolationException; void setUniqueTreeConstraint(UniqueTreeConstraint<N> uniqueTreeConstraint); void setCheckUniqueConstraintOnUpdate(boolean checkUniqueConstraintOnUpdate); void checkUniqueConstraint(N cloneOfExistingNodeWithNewValues, N root, N originalNode) throws UniqueConstraintViolationException; public interface CopiedNodeRenamer <N extends TreeNode> { void renameCopiedNode(N node); } void setCopiedNodeRenamer(CopiedNodeRenamer<N> copiedNodeRenamer); }
You can apply the non-temporal method findDirectChildren() on a getFullTreeCacheable() result to find both removed and valid children, and with the result of findValidDirectChildren() you can generate a difference list that will contain just the removed children.public interface TemporalTreeDao <N extends TreeNode> extends TreeDao<N> { List<N> findRemoved(N parent, Map<String,Object> criteria);
void unremove(N node); List<N> getAllRoots(); List<N> getFullTreeCacheable(N parent); List<N> findValidDirectChildren(List<N> treeCacheable); void removePhysically(N node); void removeHistoricizedTreesPhysically(); void removeAllPhysically(); }
String[][]
array in
constructor, containing the names of properties to be unique.
Combined property-sets are in inner arrays. For example, to
check propertiesAs a result every combination of name and code property values will be unique per tree root, and so will acronym values.dao.setUniqueTreeConstraint(new UniqueWholeTreeConstraintImpl( new String [][] { { "name","code" }, { "acronym" } }, false));
dao.setCheckUniqueConstraintOnUpdate(true); final String oldName = peter.getName(); try { peter.setName("Pietro"); dao.update(peter); // throws exception when not unique and dao.checkUniqueConstraintOnUpdate == true } catch (UniqueConstraintViolationException e) { peter.setName(oldName); throw e; }
final String petersNewName = "Pietro"; final Person peterClone = (Person) peter.clone(); // clone has null primary key peterClone.setName(petersNewName); dao.checkUniqueConstraint( // would throw exception when not unique peterClone, // the rename candidate walter, // the root node peter); // the existing node
peter.setName(petersNewName); // no exception was thrown dao.update(peter); // will not check again when dao.checkUniqueConstraintOnUpdate == false (is default)
JpaTreeDao does not cache anything by itself, it is up to you to put the cacheable results into some cache implementation.final List<N> waltersTree = dao.getTreeCacheable(walter); final List<N> marysSubTree = dao.findSubTree(mary, waltersTree); final List<N> waltersChildren = dao.findDirectChildren(waltersTree);
final List<N> marysChildren = dao.findDirectChildren(marysSubTree);
@Entity @IdClass(CompositePersonTreePathId.class) public class PersonTreePath implements TemporalTreePath { @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "ancestor", nullable = false) private ClosureTableTreeNode ancestor; @Id @ManyToOne(targetEntity = Person.class) @JoinColumn(name = "descendant", nullable = false) private ClosureTableTreeNode descendant; @Column(nullable = false) private int depth; private int orderIndex; private boolean deleted; @Override public ClosureTableTreeNode getAncestor() { return ancestor; } @Override public void setAncestor(ClosureTableTreeNode ancestor) { this.ancestor = ancestor; } @Override public ClosureTableTreeNode getDescendant() { return descendant; } @Override public void setDescendant(ClosureTableTreeNode descendant) { this.descendant = descendant; } @Override public int getDepth() { return depth; } @Override public void setDepth(int depth) { this.depth = depth; } @Override public int getOrderIndex() { return orderIndex; } @Override public void setOrderIndex(int position) { this.orderIndex = position; }
// dummy implementation of interface Temporal @Transient // will not generate a validTo field @Override public Date getValidTo() { return null; } @Transient // will not generate a validTo field @Override public void setValidTo(Date validTo) { } @Transient // will not generate a validTo field @Override public Date getValidFrom() { return null; } @Transient // will not generate a validTo field @Override public void setValidFrom(Date validFrom) { } // the really used remove-implementation public boolean isDeleted() { return deleted; } public void setDeleted(boolean deleted) { this.deleted = deleted; } }
final EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); final DbSession dbSession = new DbSessionJpaImpl(entityManager); final TemporalClosureTableTreeDao dao = new TemporalClosureTableTreeDao(Person.class, PersonTreePath.class, true, null, null, dbSession) { @Override public boolean isValid(Temporal node, Date validityDate) { return ((PersonDeletedFlagTreePath) node).isDeleted() == false; } @Override protected void assignInvalidity(TreePath path) { ((PersonDeletedFlagTreePath) path).setDeleted(true); } @Override protected void assignValidity(TreePath path) { ((PersonDeletedFlagTreePath) path).setDeleted(false); } @Override public void appendValidityCondition(String tableAlias, StringBuilder queryText, List<Object> parameters) { appendCondition(true, tableAlias, queryText, parameters); } @Override protected void appendInvalidityCondition(String tableAlias, StringBuilder queryText, List<Object> parameters) { appendCondition(false, tableAlias, queryText, parameters); } private void appendCondition(boolean validity, String tableAlias, StringBuilder queryText, List<Object> parameters) { queryText.append(buildAliasedPropertyName(tableAlias, "deleted")+" = "+buildIndexedPlaceHolder(parameters)); parameters.add(validity ? Boolean.FALSE : Boolean.TRUE); } }; final ClosureTableTreeNode walter = dao.createRoot(new Person("Walter")); dao.remove(walter); assert dao.getRoots().size() == 0; assert dao.getAllRoots().size() == 1; transaction.commit();
final Person copiedMaryTemplate = (Person) mary.clone(); copiedMaryTemplate.setName("Copy of "+mary.getName()); // walter is root, mary is child, with peter and paul as children final Person copiedMary = dao.copy(mary, walter, copiedMaryTemplate); assert copiedMary.getName().equals("Copy of Mary"); List<Person> copiedMarysChildren = dao.getChildren(copiedMary); for (Person person : copiedMarysChildren) { assert person.getName().startsWith("Copy of ") == false; }
dao.setCopiedNodeRenamer(new TreeDao.CopiedNodeRenamer<Person>() { @Override public void renameCopiedNode(Person person) { person.setName("Copy of "+person.getName()); } }); // walter is root, mary is child, with peter and paul as children final Person copiedMary = dao.copy(mary, walter, null); assert copiedMary.getName().equals("Copy of Mary"); List<Person> waltersTree = dao.getTree(walter); assert waltersTree.contains(copiedMary);
for (Person person : waltersTree) { assert person.getName().startsWith("Copy of "); }
You find all available examples in
directory, their names are JpaExample or HibernateSessionExample. Most example entities are in the same directory as the example that uses them. Their names differ from the names used in this documentation, because all entity names must be unique when using auto-scanning for annotated classes, so there are PersonNst, PersonCtt, PersonTctt and so on.