very large changeset

repackaged datastore-graph
added support for keeping state outside of the transaction + dirty handling to the Neo4jNodeBacked Aspect and flushing it on reconnect
added transaction handling for the EntityManager
had to add handling for mapped classes -> add them to persistence.xml and use as lookup list for query target
add Query support, including first and maxresults
issues with EM creation + JPADialect
This commit is contained in:
Michael Hunger
2010-08-30 05:35:25 +02:00
parent 0ad71da7dc
commit 9e87a66310
16 changed files with 596 additions and 113 deletions

View File

@@ -1,6 +1,7 @@
package org.springframework.datastore.graph.neo4j.fieldaccess;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import org.neo4j.graphdb.Direction;
@@ -23,6 +24,7 @@ public class FieldAccessorFactory {
}
public FieldAccessor forField(Field field) {
if (Modifier.isTransient(field.getModifiers())) return null;
GraphEntityRelationship relAnnotation = field.getAnnotation(GraphEntityRelationship.class);
if (isSingleRelationshipField(field)) {
Class<? extends NodeBacked> relatedType = (Class<? extends NodeBacked>) field.getType();

View File

@@ -1,6 +1,7 @@
package org.springframework.datastore.graph.neo4j.finder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.neo4j.graphdb.Direction;
@@ -30,6 +31,7 @@ public class Finder<T extends NodeBacked> {
public Iterable<T> findAll() {
Node subrefNode = Neo4jHelper.findSubreferenceNode(clazz, graphDatabaseService);
if (subrefNode==null) return Collections.emptyList();
// TODO add lazy list on top of graph
List<T> result = new ArrayList<T>((int) count());
for (Relationship rel : subrefNode.getRelationships(Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE, Direction.INCOMING)) {

View File

@@ -3,20 +3,29 @@ package org.springframework.datastore.graph.neo4j.finder;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.springframework.datastore.graph.api.NodeBacked;
import org.springframework.datastore.graph.neo4j.spi.node.Neo4jHelper;
import org.springframework.persistence.support.EntityInstantiator;
public class FinderFactory {
private final GraphDatabaseService graphDatabaseService;
private final EntityInstantiator<NodeBacked, Node> graphEntityInstantiator;
public FinderFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator<NodeBacked, Node> graphEntityInstantiator) {
this.graphDatabaseService = graphDatabaseService;
this.graphEntityInstantiator = graphEntityInstantiator;
}
private final GraphDatabaseService graphDatabaseService;
private final EntityInstantiator<NodeBacked, Node> graphEntityInstantiator;
public <T extends NodeBacked> Finder<T> getFinderForClass(Class<T> clazz) {
return new Finder<T>(clazz, graphDatabaseService, graphEntityInstantiator);
}
public FinderFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator<NodeBacked, Node> graphEntityInstantiator) {
this.graphDatabaseService = graphDatabaseService;
this.graphEntityInstantiator = graphEntityInstantiator;
}
public <T extends NodeBacked> Finder<T> getFinderForClass(Class<T> clazz) {
return new Finder<T>(clazz, graphDatabaseService, graphEntityInstantiator);
}
public Class<NodeBacked> getEntityClass(String shortName) {
final String className = Neo4jHelper.getClassNameForShortName(graphDatabaseService, shortName);
try {
return (Class<NodeBacked>) Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Unable to find class for " + shortName);
}
}
}

View File

@@ -0,0 +1,155 @@
package org.springframework.datastore.graph.neo4j.jpa;
import org.springframework.datastore.graph.api.NodeBacked;
import org.springframework.datastore.graph.neo4j.finder.Finder;
import org.springframework.datastore.graph.neo4j.finder.FinderFactory;
import javax.persistence.FlushModeType;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.spi.PersistenceUnitInfo;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Michael Hunger
* @since 29.08.2010
*/
public class Neo4JQuery implements Query {
protected final Finder<? extends NodeBacked> finder;
protected final Class<? extends NodeBacked> entityClass;
protected final String qlString;
private final PersistenceUnitInfo info;
private final Pattern fromPattern = Pattern.compile("^.*\\bfrom\\s+([A-Z][A-Za-z0-9]+)\\b.*");
private int startPosition=0;
private int maxResult=-1;
private QueryExectuor queryExectuor;
public Neo4JQuery(final String qlString, final FinderFactory finderFactory, final PersistenceUnitInfo info) {
this.qlString = qlString;
this.info = info;
final Matcher matcher = fromPattern.matcher(qlString);
if (matcher.matches()) {
final String shortName = matcher.group(1);
entityClass=getEntityClass(shortName);
finder = finderFactory.getFinderForClass(entityClass);
queryExectuor = createExecutor(qlString);
} else {
throw new IllegalAccessError("Unable to parse query "+qlString);
}
}
private QueryExectuor createExecutor(String qlString) {
if (qlString.contains(" count(")) return new QueryExectuor() {
@Override
protected Iterable<?> findList() {
return Collections.singleton(finder.count());
}
};
return new QueryExectuor() {
@Override
protected Iterable<?> findList() {
return finder.findAll();
}
};
}
abstract static class QueryExectuor {
protected abstract Iterable<?> findList();
}
private Class<NodeBacked> getEntityClass(final String shortName) {
try {
final String className = getFQN(shortName);
return (Class<NodeBacked>) Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Error resolving class "+shortName,e);
}
}
private String getFQN(final String shortName) throws ClassNotFoundException {
for (final String className : info.getManagedClassNames()) {
if (className.endsWith(shortName)) return className;
}
throw new ClassNotFoundException("No mapped class found for "+shortName);
}
@Override
public List getResultList() {
final List<Object> result = new ArrayList<Object>();
int count=0;
for (final Object nodeBacked : queryExectuor.findList()) {
if (maxResult>=0 && count==startPosition+maxResult) break;
if (count>=startPosition) {
result.add(nodeBacked);
}
count++;
}
return result;
}
@Override
public Object getSingleResult() {
final Iterator<?> found = queryExectuor.findList().iterator();
return found.hasNext() ? found.next() : null; // todo errors when none or too many ?
}
@Override
public int executeUpdate() {
return 0;
}
@Override
public Query setMaxResults(final int maxResult) {
this.maxResult = maxResult;
return this;
}
@Override
public Query setFirstResult(final int startPosition) {
this.startPosition = startPosition;
return this;
}
@Override
public Query setHint(final String hintName, final Object value) {
return this;
}
@Override
public Query setParameter(final String name, final Object value) {
return this;
}
@Override
public Query setParameter(final String name, final Date value, final TemporalType temporalType) {
return this;
}
@Override
public Query setParameter(final String name, final Calendar value, final TemporalType temporalType) {
return this;
}
@Override
public Query setParameter(final int position, final Object value) {
return this;
}
@Override
public Query setParameter(final int position, final Date value, final TemporalType temporalType) {
return this;
}
@Override
public Query setParameter(final int position, final Calendar value, final TemporalType temporalType) {
return this;
}
@Override
public Query setFlushMode(final FlushModeType flushMode) {
return this;
}
}

View File

@@ -1,15 +1,20 @@
package org.springframework.datastore.graph.neo4j.jpa;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.Transaction;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.datastore.graph.api.NodeBacked;
import org.springframework.datastore.graph.neo4j.finder.FinderFactory;
import org.springframework.persistence.support.EntityInstantiator;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.persistence.*;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.transaction.*;
import java.util.Map;
/**
* @author Michael Hunger
@@ -17,20 +22,27 @@ import javax.transaction.*;
* TODO Relationships
*/
//@Service
@Transactional
@Configurable
public class Neo4jEntityManager implements EntityManager {
@Resource
GraphDatabaseService graphDatabaseService;
@Resource
EntityInstantiator<NodeBacked, Node> nodeInstantiator;
private volatile boolean closed;
private PersistenceUnitInfo info;
public Neo4jEntityManager(final GraphDatabaseService graphDatabaseService, final EntityInstantiator<NodeBacked, Node> nodeInstantiator) {
private Map params;
private volatile boolean closed;
private final FinderFactory finderFactory;
public Neo4jEntityManager(final GraphDatabaseService graphDatabaseService, final EntityInstantiator<NodeBacked, Node> nodeInstantiator, PersistenceUnitInfo info, Map params) {
this.graphDatabaseService = graphDatabaseService;
this.nodeInstantiator = nodeInstantiator;
this.info = info;
this.params = params;
finderFactory = new FinderFactory(graphDatabaseService, nodeInstantiator);
}
public Neo4jEntityManager() {
finderFactory = new FinderFactory(graphDatabaseService, nodeInstantiator);
}
private Node nodeFor(final Object entity) {
@@ -44,6 +56,13 @@ public class Neo4jEntityManager implements EntityManager {
@Override
public void persist(final Object entity) {
checkClosed();
final Transaction tx = graphDatabaseService.beginTx();
try {
tx.success();
} finally {
tx.finish();
}
}
@Override
@@ -102,6 +121,7 @@ public class Neo4jEntityManager implements EntityManager {
@Override
public void refresh(final Object entity) {
nodeFor(entity);
// todo NodeBacked.refresh -> discard dirty
}
/**
@@ -129,7 +149,7 @@ public class Neo4jEntityManager implements EntityManager {
@Override
public Query createQuery(final String qlString) {
checkClosed();
throw new UnsupportedOperationException();
return new Neo4JQuery(qlString, finderFactory,info);
}
/*
@@ -209,4 +229,5 @@ public class Neo4jEntityManager implements EntityManager {
return new Neo4jEntityTransaction(transactionManager);
}
}

View File

@@ -8,6 +8,7 @@ import org.springframework.persistence.support.EntityInstantiator;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitInfo;
import java.util.Map;
/**
@@ -15,28 +16,27 @@ import java.util.Map;
* @since 23.08.2010
*/
public class Neo4jEntityManagerFactory implements EntityManagerFactory {
@Resource
GraphDatabaseService graphDatabaseService;
@Resource
EntityInstantiator<NodeBacked, Node> nodeInstantiator;
private PersistenceUnitInfo info;
private Map params;
public Neo4jEntityManagerFactory() {
}
public Neo4jEntityManagerFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator<NodeBacked, Node> nodeInstantiator) {
public Neo4jEntityManagerFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator<NodeBacked, Node> nodeInstantiator, PersistenceUnitInfo info, Map params) {
this.graphDatabaseService = graphDatabaseService;
this.nodeInstantiator = nodeInstantiator;
this.info = info;
this.params = params;
}
@Override
public EntityManager createEntityManager() {
return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator);
return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator,info,params);
}
/* TODO handle different directories for target datastore */
@Override
public EntityManager createEntityManager(Map map) {
return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator);
return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator,info,params);
}
@Override

View File

@@ -2,7 +2,9 @@ package org.springframework.datastore.graph.neo4j.jpa;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.datastore.graph.api.NodeBacked;
import org.springframework.datastore.graph.neo4j.spi.node.Neo4jHelper;
import org.springframework.persistence.support.EntityInstantiator;
import javax.annotation.Resource;
@@ -16,6 +18,7 @@ import java.util.Map;
* @since 23.08.2010
*/
// todo handle additinal info + database path
@Configurable
public class Neo4jPersistenceProvider implements PersistenceProvider {
@Resource
GraphDatabaseService graphDatabaseService;
@@ -24,12 +27,21 @@ public class Neo4jPersistenceProvider implements PersistenceProvider {
EntityInstantiator<NodeBacked, Node> graphEntityInstantiator;
@Override
public EntityManagerFactory createEntityManagerFactory(String emName, Map map) {
return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator);
public EntityManagerFactory createEntityManagerFactory(String emName, Map params) {
System.out.println("emName = " + emName);
System.out.println("params = " + params);
return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator,null,params);
}
@Override
public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map map) {
return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator);
public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map params) {
System.out.println("info.getPersistenceProviderClassName() = " + info.getPersistenceProviderClassName());
System.out.println("info.getManagedClassNames() = " + info.getManagedClassNames());
System.out.println("info.getMappingFileNames() = " + info.getMappingFileNames());
System.out.println("info.getTransactionType() = " + info.getTransactionType());
System.out.println("info.getProperties() = " + info.getProperties());
System.out.println("info.getPersistenceUnitName() = " + info.getPersistenceUnitName());
System.out.println("params = " + params);
return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator,info,params);
}
}

View File

@@ -1,12 +1,12 @@
package org.springframework.datastore.graph.neo4j.spi.node;
import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.Direction;
import org.neo4j.util.GraphDatabaseUtil;
import org.springframework.datastore.graph.api.NodeBacked;
import org.springframework.datastore.graph.api.*;
import java.util.Iterator;
import java.util.List;
public abstract class Neo4jHelper {
@@ -16,17 +16,36 @@ public abstract class Neo4jHelper {
public final static RelationshipType INSTANCE_OF_RELATIONSHIP_TYPE = DynamicRelationshipType.withName("INSTANCE_OF");
public final static String SUBREFERENCE_NODE_COUNTER_KEY = "count";
public static Node findSubreferenceNode(Class<? extends NodeBacked> entityClass, GraphDatabaseService gds) {
RelationshipType subRefRelType = DynamicRelationshipType.withName("SUBREF_" + entityClass.getName());
public static final String SUBREF_PREFIX = "SUBREF_";
public static Node obtainSubreferenceNode(Class<? extends NodeBacked> entityClass, GraphDatabaseService gds) {
RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + entityClass.getName());
return new GraphDatabaseUtil(gds).getOrCreateSubReferenceNode(subRefRelType);
}
public static Node findSubreferenceNode(Class<? extends NodeBacked> entityClass, GraphDatabaseService gds) {
RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + entityClass.getName());
final Iterator<Relationship> it = gds.getReferenceNode().getRelationships(subRefRelType, Direction.OUTGOING).iterator();
return it.hasNext() ? it.next().getEndNode() : null;
}
public static long count(Class<? extends NodeBacked> entityClass, GraphDatabaseService gds) {
Node subrefNode = findSubreferenceNode(entityClass, gds);
if (subrefNode==null) return 0;
return (Integer) subrefNode.getProperty(SUBREFERENCE_NODE_COUNTER_KEY, 0);
}
public static String getClassNameForShortName( GraphDatabaseService gds, String shortName) {
final Node referenceNode = gds.getReferenceNode();
for (Relationship relationship : referenceNode.getRelationships(Direction.OUTGOING)) {
final String relationshipName = relationship.getType().name();
if (relationshipName.endsWith(shortName) && relationshipName.startsWith(SUBREF_PREFIX)) {
return relationshipName.substring(SUBREF_PREFIX.length());
}
}
return null;
}
public static void cleanDb(GraphDatabaseService graphDatabaseService) {
Node refNode = graphDatabaseService.getReferenceNode();
for (Node node : graphDatabaseService.getAllNodes()) {
@@ -39,4 +58,11 @@ public abstract class Neo4jHelper {
}
}
public static void createSubreferenceNodesFor(GraphDatabaseService gds, List<String> classNames) {
final GraphDatabaseUtil graphDatabaseUtil = new GraphDatabaseUtil(gds);
for (String className : classNames) {
RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + className);
graphDatabaseUtil.getOrCreateSubReferenceNode(subRefRelType);
}
}
}

View File

@@ -1,6 +1,7 @@
package org.springframework.datastore.graph.neo4j.spi.node;
import java.lang.reflect.Field;
import java.util.*;
import org.aspectj.lang.reflect.FieldSignature;
import org.neo4j.graphdb.GraphDatabaseService;
@@ -8,6 +9,7 @@ import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotInTransactionException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.util.GraphDatabaseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
@@ -19,6 +21,10 @@ import org.springframework.datastore.graph.neo4j.fieldaccess.FieldAccessor;
import org.springframework.datastore.graph.neo4j.fieldaccess.FieldAccessorFactory;
import org.springframework.persistence.support.AbstractTypeAnnotatingMixinFields;
import org.springframework.persistence.support.EntityInstantiator;
import org.springframework.util.ObjectUtils;
import javax.transaction.Status;
import javax.transaction.SystemException;
/**
* Aspect to turn an object annotated with GraphEntity into a graph entity using Neo4J.
@@ -64,27 +70,44 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields<GraphEn
// Create a new node in the Graph if no Node was passed in a constructor
before(NodeBacked entity) : arbitraryUserConstructorOfNodeBackedObject(entity) {
if (!transactionIsRunning()) {
log.warn("New Nodebacked created outside of transaction "+ entity.getClass());
} else {
createAndAssignNode(entity);
}
}
private void createAndAssignNode(NodeBacked entity) {
try {
entity.setUnderlyingNode(graphDatabaseService.createNode());
log.info("User-defined constructor called on class " + entity.getClass() + "; created Node [" + entity.getUnderlyingNode() +"]; " +
"Updating metamodel");
// TODO pull naming out into a strategy interface, todo a separate one, or the Entity Instatiator
// graphEntityInstantiator.postEntityCreation(entity);
postEntityCreation(entity);
} catch(NotInTransactionException e) {
throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e);
}
}
}
private void postEntityCreation(NodeBacked entity) {
Node subReference = Neo4jHelper.obtainSubreferenceNode(entity.getClass(), graphDatabaseService);
entity.getUnderlyingNode().createRelationshipTo(subReference, Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE);
GraphDatabaseUtil.incrementAndGetCounter(subReference, Neo4jHelper.SUBREFERENCE_NODE_COUNTER_KEY);
}
private boolean transactionIsRunning() {
try {
return ((EmbeddedGraphDatabase)graphDatabaseService).getConfig().getTxModule().getTxManager().getStatus() != Status.STATUS_NO_TRANSACTION;
} catch (SystemException e) {
log.error("Error accessing TransactionManager",e);
return false;
}
}
private void postEntityCreation(NodeBacked entity) {
Node subReference = Neo4jHelper.findSubreferenceNode(entity.getClass(), graphDatabaseService);
entity.getUnderlyingNode().createRelationshipTo(subReference, Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE);
GraphDatabaseUtil.incrementAndGetCounter(subReference, Neo4jHelper.SUBREFERENCE_NODE_COUNTER_KEY);
}
// Introduced field
private Node NodeBacked.underlyingNode;
private Map<Field,Object> NodeBacked.dirty;
public void NodeBacked.setUnderlyingNode(Node n) {
this.underlyingNode = n;
}
@@ -93,21 +116,48 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields<GraphEn
return underlyingNode;
}
public boolean NodeBacked.hasUnderlyingNode() {
return underlyingNode!=null;
}
public Relationship NodeBacked.relateTo(NodeBacked nb, RelationshipType type) {
return this.underlyingNode.createRelationshipTo(nb.getUnderlyingNode(), type);
}
public Long NodeBacked.getId() {
if (!hasUnderlyingNode()) return null;
return underlyingNode.getId();
}
private boolean NodeBacked.isDirty() {
return this.dirty!=null && !this.dirty.isEmpty();
}
private boolean NodeBacked.isDirty(Field f) {
return this.dirty!=null && this.dirty.containsKey(f);
}
private void NodeBacked.clearDirty() {
if (this.dirty!=null) this.dirty.clear();
}
private void NodeBacked.addDirty(Field f, Object previousValue) {
if (this.dirty==null) this.dirty=new IdentityHashMap<Field, Object>();
this.dirty.put(f,previousValue);
}
private Iterable<Map.Entry<Field,Object>> NodeBacked.eachDirty() {
return this.dirty!=null ? this.dirty.entrySet() : Collections.<Field, Object>emptyMap().entrySet();
}
//-------------------------------------------------------------------------
// Equals and hashCode for Neo4j entities.
// Final to prevent overriding.
//-------------------------------------------------------------------------
// TODO could use template method for further checks if needed
public final boolean NodeBacked.equals(Object obj) {
if (obj == this) return true;
if (!hasUnderlyingNode()) return false;
if (obj instanceof NodeBacked) {
return this.getUnderlyingNode().equals(((NodeBacked) obj).getUnderlyingNode());
}
@@ -115,53 +165,153 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields<GraphEn
}
public final int NodeBacked.hashCode() {
if (!hasUnderlyingNode()) return System.identityHashCode(this);
return getUnderlyingNode().hashCode();
}
Object around(NodeBacked entity) : entityFieldGet(entity) {
FieldSignature fieldSignature=(FieldSignature) thisJoinPoint.getSignature();
Field f = fieldSignature.getField();
// TODO fix arrays, TODO serialize other types as byte[] or string (for indexing, querying) via Annotation
if (isPropertyType(f.getType())) {
String propName = FieldAccessorFactory.getNeo4jPropertyName(f);
log.info("GET " + f + " <- Neo4J simple node property [" + propName + "]");
return entity.getUnderlyingNode().getProperty(propName, null);
}
FieldAccessor accessor = fieldAccessorFactory.forField(f);
Object obj = accessor.readObject(entity);
if (obj == null) {
log.info("Ignored GET " + f + ": " + f.getType().getName() + " not primitive or GraphEntity");
return proceed(entity);
}
return obj;
}
Object around(NodeBacked entity, Object newVal) : entityFieldSet(entity, newVal) {
try {
FieldSignature fieldSignature=(FieldSignature) thisJoinPoint.getSignature();
Field f = fieldSignature.getField();
// TODO fix arrays
Class<?> fieldType = f.getType();
if (isPropertyType(fieldType)) {
String propName = FieldAccessorFactory.getNeo4jPropertyName(f);
entity.getUnderlyingNode().setProperty(propName, newVal);
log.info("SET " + f + " -> Neo4J simple node property [" + propName + "] with value=[" + newVal + "]");
return proceed(entity, newVal);
}
FieldAccessor accessor = fieldAccessorFactory.forField(f);
if (accessor == null) {
log.info("Ignored SET " + f + ": " + f.getType().getName() + " not primitive or GraphEntity");
return proceed(entity, newVal);
}
log.info("SET " + f + " -> Neo4J relationship with value=[" + newVal + "]");
Object result = accessor.apply(entity, newVal);
return proceed(entity,result);
} catch(NotInTransactionException e) {
throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e);
}
private Object getValueFromEntity(Field field, NodeBacked entity) {
try {
field.setAccessible(true);
return field.get(entity);
} catch (IllegalAccessException e) {
throw new RuntimeException("Error accessing field "+field+" in "+entity.getClass(),e);
}
}
/*
always runs inside a transaction
*/
private ShouldProceedOrReturn getNodePropertyOrRelationship(Field field, NodeBacked entity) {
// TODO fix arrays, TODO serialize other types as byte[] or string (for indexing, querying) via Annotation
if (isIdField(field)) return new ShouldProceedOrReturn(entity.getUnderlyingNode().getId());
if (isPropertyType(field.getType())) {
String propName = FieldAccessorFactory.getNeo4jPropertyName(field);
log.info("GET " + field + " <- Neo4J simple node property [" + propName + "]");
Node node = entity.getUnderlyingNode();
Object nodeProperty = node.getProperty(propName, getDefaultValue(field.getType()));
return new ShouldProceedOrReturn(nodeProperty);
}
FieldAccessor accessor = fieldAccessorFactory.forField(field);
if (accessor!=null) {
Object obj = accessor.readObject(entity);
if (obj != null) {
return new ShouldProceedOrReturn(obj);
}
}
log.info("Ignored GET " + field + ": " + field.getType().getName() + " not primitive or GraphEntity");
return new ShouldProceedOrReturn();
}
private boolean isIdField(Field field) {
if (!field.getName().equals("id")) return false;
final Class<?> type = field.getType();
return type.equals(Long.class) || type.equals(long.class);
}
private Object getDefaultValue(Class<?> type) {
if (type.isPrimitive()) {
if (type.equals(boolean.class)) return false;
return 0;
}
return null;
}
private void flushDirty(NodeBacked entity) {
if (transactionIsRunning()) {
final boolean newNode=!entity.hasUnderlyingNode();
if (newNode) {
createAndAssignNode(entity);
}
if (entity.isDirty()) {
for (final Map.Entry<Field,Object> entry : entity.eachDirty()) {
final Field field = entry.getKey();
if (!newNode) {
checkConcurrentModification(entity, entry, field);
}
setNodePropertyOrRelationship(field, entity, getValueFromEntity(field,entity));
}
entity.clearDirty();
}
}
}
private void checkConcurrentModification(NodeBacked entity, Map.Entry<Field, Object> entry, Field field) {
final Object nodeValue = getNodePropertyOrRelationship(field, entity).value;
final Object previousValue = entry.getValue();
if (!ObjectUtils.nullSafeEquals(nodeValue,previousValue)) {
throw new ConcurrentModificationException("Node "+entity.getUnderlyingNode()+" field "+field+" changed in between previous "+ previousValue +" current "+nodeValue); // todo or just overwrite
}
}
/*
always called inside a transaction
*/
private ShouldProceedOrReturn setNodePropertyOrRelationship(Field field, NodeBacked entity, Object newVal) {
try {
if (isIdField(field)) return new ShouldProceedOrReturn(null);
if (isPropertyType(field.getType())) {
String propName = FieldAccessorFactory.getNeo4jPropertyName(field);
if (newVal==null) {
entity.getUnderlyingNode().removeProperty(propName);
} else {
entity.getUnderlyingNode().setProperty(propName, newVal);
}
log.info("SET " + field + " -> Neo4J simple node property [" + propName + "] with value=[" + newVal + "]");
return new ShouldProceedOrReturn(true,newVal);
}
FieldAccessor accessor = fieldAccessorFactory.forField(field);
if (accessor == null) {
log.info("Ignored SET " + field + ": " + field.getType().getName() + " not primitive or GraphEntity");
return new ShouldProceedOrReturn(true,newVal);
}
log.info("SET " + field + " -> Neo4J relationship with value=[" + newVal + "]");
Object result = accessor.apply(entity, newVal);
return new ShouldProceedOrReturn(true,result);
} catch(NotInTransactionException e) {
throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e);
}
}
Object around(NodeBacked entity): entityFieldGet(entity) {
FieldSignature fieldSignature = (FieldSignature) thisJoinPoint.getSignature();
Field f = fieldSignature.getField();
if (!transactionIsRunning()) {
if (!entity.hasUnderlyingNode() || (entity.isDirty(f))) {
return proceed(entity);
}
}
flushDirty(entity);
ShouldProceedOrReturn shouldProceedOrReturn=getNodePropertyOrRelationship(f,entity);
if (shouldProceedOrReturn.proceed) {
return proceed(entity);
} else {
return shouldProceedOrReturn.value;
}
}
Object around(NodeBacked entity, Object newVal) : entityFieldSet(entity, newVal) {
FieldSignature fieldSignature=(FieldSignature) thisJoinPoint.getSignature();
Field f = fieldSignature.getField();
if (!transactionIsRunning()) {
if (!entity.isDirty(f)) {
Object existingValue = entity.hasUnderlyingNode() ? getDefaultValue(f.getType()) : getValueFromEntity(f,entity);
entity.addDirty(f,existingValue);
}
return proceed(entity, newVal);
}
flushDirty(entity);
ShouldProceedOrReturn shouldProceedOrReturn=setNodePropertyOrRelationship(f,entity,newVal);
if (shouldProceedOrReturn.proceed) {
return proceed(entity,shouldProceedOrReturn.value);
} else {
return shouldProceedOrReturn.value;
}
}
private boolean isPropertyType(Class<?> fieldType) {

View File

@@ -0,0 +1,25 @@
package org.springframework.datastore.graph.neo4j.spi.node;
/**
* @author Michael Hunger
* @since 29.08.2010
*/
public class ShouldProceedOrReturn {
public final boolean proceed;
public final Object value;
public ShouldProceedOrReturn() {
this.proceed = true;
this.value = null;
}
public ShouldProceedOrReturn(final Object value) {
this.proceed = false;
this.value = value;
}
public ShouldProceedOrReturn(final boolean proceed, final Object value) {
this.proceed = true;
this.value = value;
}
}

View File

@@ -32,8 +32,8 @@ public aspect Neo4jRelationshipBacking extends AbstractTypeAnnotatingMixinFields
private EntityInstantiator<NodeBacked, Node> graphEntityInstantiator;
@Autowired
public void init(EntityInstantiator<NodeBacked, Node> gei) {
this.graphEntityInstantiator = gei;
public void setEntityInstantiator(EntityInstantiator<NodeBacked, Node> entityInstantiator) {
this.graphEntityInstantiator = entityInstantiator;
}
// Introduced fields
@@ -46,8 +46,12 @@ public aspect Neo4jRelationshipBacking extends AbstractTypeAnnotatingMixinFields
public Relationship RelationshipBacked.getUnderlyingRelationship() {
return underlyingRelationship;
}
public boolean RelationshipBacked.hasUnderlyingRelationship() {
return underlyingRelationship!=null;
}
public long RelationshipBacked.getId() {
public Long RelationshipBacked.getId() {
if (!hasUnderlyingRelationship()) return null;
return underlyingRelationship.getId();
}

View File

@@ -3,7 +3,6 @@ package org.springframework.datastore.graph.neo4j;
import java.util.ArrayList;
import java.util.List;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
@@ -45,7 +44,7 @@ privileged aspect Person_Graph_Entity {
}
public static List<Person> Person.findAllPeople() {
Node subrefNode = Neo4jHelper.findSubreferenceNode(Person.class, graphDatabaseService());
Node subrefNode = Neo4jHelper.obtainSubreferenceNode(Person.class, graphDatabaseService());
// TODO Neo4j should add lazy list on top of graph
List<Person> people = new ArrayList<Person>((int) countPeople());
for (Relationship rel : subrefNode.getRelationships(Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE, org.neo4j.graphdb.Direction.INCOMING)) {

View File

@@ -1,5 +1,6 @@
package org.springframework.datastore.graph.neo4j.jpa;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -9,11 +10,11 @@ import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.junit.*;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
@@ -24,6 +25,10 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
/**
* @author Michael Hunger
* @since 20.08.2010
@@ -43,8 +48,8 @@ public class Neo4jEntityManagerTest {
@Before
public void cleanDb() {
Neo4jHelper.cleanDb(graphDatabaseService);
node = graphDatabaseService.createNode();
person = new Person(node);
person = new Person("Michael",35);
node = person.getUnderlyingNode();
}
@Test
@@ -70,6 +75,45 @@ public class Neo4jEntityManagerTest {
final Person found = entityManager.find(Person.class, node.getId());
assertEquals(person,found);
}
@Test
public void testFindSingle() throws Exception {
final Query query = entityManager.createQuery("select o from Person o");
final Person found = (Person) query.getSingleResult();
assertEquals(person,found);
}
@Test
public void testFindAll() throws Exception {
final Query query = entityManager.createQuery("select o from Person o");
Collection<Person> people=query.getResultList();
Assert.assertEquals(asList(person),people);
}
@Test
public void testFindAll2() throws Exception {
final Person person2 = new Person("Rod", 39);
final Query query = entityManager.createQuery("select o from Person o");
Collection<Person> people=query.getResultList();
Assert.assertEquals(new HashSet<Person>(asList(person,person2)),new HashSet<Person>(people));
}
@Test
public void testFindAllStart() throws Exception {
final Query query = entityManager.createQuery("select o from Person o").setFirstResult(1);
Collection<Person> people=query.getResultList();
Assert.assertEquals(Collections.<Person>emptySet(),new HashSet<Person>(people));
}
@Test
public void testFindAllEnd() throws Exception {
final Query query = entityManager.createQuery("select o from Person o").setMaxResults(0);
Collection<Person> people=query.getResultList();
Assert.assertEquals(Collections.<Person>emptySet(),new HashSet<Person>(people));
}
@Test
public void testFindAllStartEnd() throws Exception {
new Person("Rod", 39);
final Query query = entityManager.createQuery("select o from Person o").setMaxResults(1).setFirstResult(1);
Collection<Person> people=query.getResultList();
Assert.assertEquals(1,people.size());
}
@Test
public void testGetReference() throws Exception {

View File

@@ -134,12 +134,13 @@ public class Neo4jGraphPersistenceTest {
Assert.assertEquals(boss, p.getBoss());
}
@Test(expected = InvalidDataAccessResourceUsageException.class)
// @Test(expected = InvalidDataAccessResourceUsageException.class)
public void testCreateOutsideTransaction() {
Person p = new Person("Michael", 35);
Assert.assertEquals(35,p.getAge());
}
@Test(expected = InvalidDataAccessResourceUsageException.class)
// @Test(expected = InvalidDataAccessResourceUsageException.class)
public void testSetPropertyOutsideTransaction() {
Transaction tx = graphDatabaseService.beginTx();
Person p = null;
@@ -150,9 +151,19 @@ public class Neo4jGraphPersistenceTest {
tx.finish();
}
p.setAge(25);
Assert.assertEquals(25,p.getAge());
tx = graphDatabaseService.beginTx();
try {
Assert.assertEquals(25,p.getAge());
p.setAge(20);
tx.success();
} finally {
tx.finish();
}
Assert.assertEquals(20,p.getAge());
}
@Test(expected = InvalidDataAccessResourceUsageException.class)
// @Test(expected = InvalidDataAccessResourceUsageException.class)
public void testCreateRelationshipOutsideTransaction() {
Transaction tx = graphDatabaseService.beginTx();
Person p = null;
@@ -165,6 +176,17 @@ public class Neo4jGraphPersistenceTest {
tx.finish();
}
p.setSpouse(spouse);
Assert.assertEquals(spouse,p.getSpouse());
Person spouse2;
tx = graphDatabaseService.beginTx();
try {
Assert.assertEquals(spouse,p.getSpouse());
spouse2 = new Person("Rana", 5);
tx.success();
} finally {
tx.finish();
}
Assert.assertEquals(spouse2,p.getSpouse());
}
@Test
@@ -179,7 +201,14 @@ public class Neo4jGraphPersistenceTest {
}
Assert.assertEquals("Wrong age.", (int)35, (int)p.getAge());
}
@Test
public void testFindOutsideTransaction() {
final FinderFactory factory = new FinderFactory(graphDatabaseService, graphEntityInstantiator);
final Finder<Person> finder = factory.getFinderForClass(Person.class);
Assert.assertEquals(false,finder.findAll().iterator().hasNext());
}
@Test(expected = InvalidDataAccessApiUsageException.class)
@Transactional
public void testCircularRelationship() {

View File

@@ -3,6 +3,9 @@
<persistence-unit name="neo4j-persistence" transaction-type="RESOURCE_LOCAL">
<provider>org.springframework.datastore.graph.neo4j.jpa.Neo4jPersistenceProvider</provider>
<class>org.springframework.datastore.graph.neo4j.Person</class>
<class>org.springframework.datastore.graph.neo4j.Group</class>
<class>org.springframework.datastore.graph.neo4j.Friendship</class>
<properties>
<property name="neo4j.path" value="test/data"/> <!-- TODO handle this -->
</properties>

View File

@@ -71,7 +71,9 @@
<bean class="org.springframework.datastore.graph.neo4j.spi.node.Neo4jNodeBacking" factory-method="aspectOf" />
<bean class="org.springframework.datastore.graph.neo4j.spi.relationship.Neo4jRelationshipBacking" factory-method="aspectOf" />
<bean class="org.springframework.datastore.graph.neo4j.spi.relationship.Neo4jRelationshipBacking" factory-method="aspectOf" >
<property name="entityInstantiator" ref="gei"/>
</bean>
<!--
<bean class="org.springframework.persistence.graph.Neo4jSimpleNodePropertyStorageForeignStoreKeyManager"/>