Managing hierarchical data using two dimentional tables is a pain. There are some patterns to reduce this pain. One such solution is described at here. This article is about implementing the same using Spring, JPA, Annotations and Aspects. Please go through the is link to better understand this solution described. The purpose is to come up with a component that will remove the boiler-plate code in business layer to handle hierarchical data.
Summary
-
Create base class for Entities used to represent Hierarchical data
-
Create annotation classes
-
Code the Aspect that will execute addional steps for managing Hierarchical data. (Heart of the solution)
-
Now the Aspect can be used everywhere Hierarchical data is used.
Detail
-
Create base class for Entities used to represent Hierarchical data.
The purpose of the super class is to encapsulate all the common attrubutes and operations required for managing hierarchical data in a table. Please note that the class is annotated as@MappedSuperclass
.
The methods are meant to generate queies required to perform CRUD operations on the Table. Their use will be more clear later in the article when we will revisitHierarchicalEntity
.
Now any Entity that extends this class will have all the attribues required to manage hierarchical data.
import com.es.clms.aspect.HierarchicalEntity;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
@EntityListeners({HierarchicalEntity.class})
public abstract class AbstractHierarchyEntity
implements
Serializable {
protected Long parentId;
protected Long lft;
protected Long rgt;
public String getMaxRightQuery() {
return "Select max(e.rgt) from " + this.getClass().getName() + " e";
}
public String getQueryForParentRight() {
return "Select e.rgt from " + this.getClass().getName()
+ " e where e.id = ?1";
}
public String getDeleteStmt() {
return "Delete from " + this.getClass().getName()
+ " e Where e.lft between ?1 and ?2";
}
public String getUpdateStmtForFirst() {
return "Update " + this.getClass().getName()
+ " e set e.lft = e.lft + ?2 Where e.lft >= ?1";
}
public String getUpdateStmtForRight() {
return "Update " + this.getClass().getName()
+ " e set e.rgt = e.rgt + ?2 Where e.rgt >= ?1";
}
.
.
.//Getter and setters for all the attributes.
}
-
Create annotation classes
Following is a annotation class that will be used to annotate the methods that performs CRUD operations on hierarchical data. It is followed by a enum that will decided the type of curd operation to be performed.
These classes will make more sense after the next section.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HierarchicalOperation {
HierarchicalOperationType operationType();
}
/**
* Enum - Type of CRUD operation.
*/
public enum HierarchicalOperationType {
SAVE,
DELETE;
}
-
Code the Aspect that will execute addional steps for managing Hierarchical data.
HierarchicalEntity
is an aspect that performs the additional logic required to manage the hierarchical data as descriped in the article here.
This is the first time I am using Aspect. Therefore I am sure that there are better ways to do this. Those of you, who are good at it, please improve this part of code.
This class is annotated as@Aspect
. The pointcut will intercept any method anotated withHierarchicalOperation
and has a input of typeAbstractHierarchyEntity
. A sample its usage is in next section.
operation
method is anotated to be executed before the pointcut. Based on the HierarchicalOperationType passed, this method will either execute the additional tasks required to save or delete the hierarchical record. This is where the methods defined inAbstractHierarchyEntity
for generating JPA Queries are used.
GenericDAOHelper
is a utility class for using JPA.
import com.es.clms.annotation.HierarchicalOperation;
import com.es.clms.annotation.HierarchicalOperationType;
import com.es.clms.common.GenericDAOHelper;
import com.es.clms.model.AbstractHierarchyEntity;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Aspect
@Service("hierarchicalEntity")
public class HierarchicalEntity {
@Autowired
private GenericDAOHelper genericDAOHelper;
@Pointcut(value = "execution(@com.es.clms.annotation.HierarchicalOperation * *(..)) "
+ "&& args(AbstractHierarchyEntity)")
private void hierarchicalOps() {
}
/**
*
* @param jp
* @param hierarchicalOperation
*/
@Before("hierarchicalOps() && @annotation(hierarchicalOperation) ")
public void operation(final JoinPoint jp,
final HierarchicalOperation hierarchicalOperation) {
if (jp.getArgs().length != 1) {
throw new IllegalArgumentException(
"Expecting only one parameter of type AbstractHierarchyEntity in "
+ jp.getSignature());
}
if (HierarchicalOperationType.SAVE.equals(
hierarchicalOperation.operationType())) {
save(jp);
} else if (HierarchicalOperationType.DELETE.equals(
hierarchicalOperation.operationType())) {
delete(jp);
}
}
/**
*
* @param jp
*/
private void save(JoinPoint jp) {
AbstractHierarchyEntity entity =
(AbstractHierarchyEntity) jp.getArgs()[0];
if (entity == null)
return;
if (entity.getParentId() == null) {
Long maxRight = (Long) genericDAOHelper.executeSingleResultQuery(
entity.getMaxRightQuery());
if (maxRight == null) {
maxRight = 0L;
}
entity.setLft(maxRight + 1);
entity.setRgt(maxRight + 2);
} else {
Long parentRight = (Long) genericDAOHelper.executeSingleResultQuery(
entity.getQueryForParentRight(), entity.getParentId());
entity.setLft(parentRight);
entity.setRgt(parentRight + 1);
genericDAOHelper.executeUpdate(
entity.getUpdateStmtForFirst(), parentRight, 2L);
genericDAOHelper.executeUpdate(
entity.getUpdateStmtForRight(), parentRight, 2L);
}
}
/**
*
* @param jp
*/
private void delete(JoinPoint jp) {
AbstractHierarchyEntity entity =
(AbstractHierarchyEntity) jp.getArgs()[0];
genericDAOHelper.executeUpdate(
entity.getDeleteStmt(), entity.getLft(), entity.getRgt());
Long width = (entity.getRgt() - entity.getLft()) + 1;
genericDAOHelper.executeUpdate(
entity.getUpdateStmtForFirst(), entity.getRgt(), width * (-1));
genericDAOHelper.executeUpdate(
entity.getUpdateStmtForRight(), entity.getRgt(), width * (-1));
}
}
-
Sample Usage
From this point on you donot have to worry about the additional tasks required for managing the data. Just use theHierarchicalOperation
anotation with appropriateHierarchicalOperationType
.
Below is a sample use of the code developed so far.
@HierarchicalOperation(operationType = HierarchicalOperationType.SAVE)
public long save(VariableGroup group) {
entityManager.persist(group);
return group.getId();
}
@HierarchicalOperation(operationType = HierarchicalOperationType.DELETE)
public void delete(VariableGroup group) {
entityManager.remove(entityManager.merge(group));
}