zhuweiba 2011-03-29
Spring Data Graph是Spring提供的图型数据库应用开发解决方案,其基本原理是用Spring Data Graph注解对POJO entities 进行标注,AspectJ根据这些标注对其进行graph数据存储的映射处理。Entities映射到图中的顶点(node),对其他entities的引用映射为图中的边(relationships)。对于有属性的边(relationship),可以定义relationship entities进行对应。
目前Spring Data Graph支持的数据库是Neo4j,一个支持事务的图型数据库。(图论,图是由顶点和边组成的,顶点之间由边进行连接。)图型数据库在进行关系复杂的数据查询时具有明显的性能优势。neo4j有着悠久的历史(7年+),成熟和稳定性是经过了时间检验的。neo4j数据建模直观,node、relationship、properties;既可以作为独立服务器,也可以嵌入在程序中(700k);扩展性强,单机能处理几十亿的nodes/relationships/properties,支持ha;强大的图遍历框架,API简单明了。
图型数据库是关系型数据库的替代品。
关系数据模型原则上能够建模任何数据结构且没有信息冗余和丢失。建模完成之后,就可以使用SQL插入、修改和查询数据。然而随着数据量的爆炸性增长,以及数据依赖关系的复杂化,RDBM的查询性能遭到了挑战:联结大量表、深度嵌套的SQL查询,尤其以递归结构(文件树)和网络结构(关系网)查询性能影响最为突出。
关系模型也很难适应当前软件开发的方法,虽然Hibernate这样的ORM简化了对象模型映射到关系数据模型的任务,但是并不能解决查询性能的问题。
在图型数据库中进行数据查询是遍历图的过程,不会出现RDBMS那样的联结操作,数据规模的扩大对查询速度的影响是线性的。图型数据库实现采用最广泛的是属性图形模型(Property Graph Model)。按照该模型,属性图里信息的建模使用3种构造单元:
Neo4j是一个用Java实现、具有ACID特性(事务性:A代表原子性、C表示一致性、I是隔离性、D则为持久性)的图型数据库。数据以一种针对图进行过优化的格式保存在磁盘上。作为内核的图存储引擎性能卓越,具有数据库产品应当具备的所有特性,如恢复、两阶段提交、符合XA等。自2003年起,Neo4j就已经被作为24/7的企业级产品使用。Neo4j既可作为无需任何管理开销的内嵌数据库使用,通过Java-API进行数据操作;也可以作为单独的服务器使用,通过REST接口进行数据操作,方便地集成到PHP、.NET和JavaScript等语言开发的应用系统中。
neo4j提供的接口 org.neo4j.graphdb.GraphDatabaseService用于获取图存储引擎。通过他可以创建(当然也可以查询)节点和关系,通过IndexManager管理索引,指定数据库生命周期事件回调方法,进行事务管理。
EmbeddedGraphDatabaseService是用于java程序中嵌入neo4j的实现类,还有一个通过REST远程访问Neo4j的实现类。
通过GraphDatabaseService存储节点和关系简单直接,Neo4j用Node 和 Relationship类来作为数据操作对象,Node 和 Relationship都可以有属性,Property的值可以是基本类型或String,也可以是字节数组。数据的写操作必须在事务中完成,读操作则无所谓。
GraphDatabaseService graphDb = new EmbeddedGraphDatabase( "helloworld" ); Transaction tx = graphDb.beginTx(); try { Node firstNode = graphDb.createNode(); Node secondNode = graphDb.createNode(); firstNode.setProperty( "message", "Hello, " ); secondNode.setProperty( "message", "world!" ); Relationship relationship = firstNode.createRelationshipTo( secondNode, DynamicRelationshipType.of("KNOWS") ); relationship.setProperty( "message", "brave Neo4j " ); tx.success(); } finally { tx.finish(); }
找出单个节点或关系不是图型数据库的优势,图型数据库的出现是为了解决大批量关联数据的查询,Neo4j提供了精准的DSL来定义TraversalDescriptions,然后提供startNode,Neo4j就会提供一个lazy的Iterable查询结果。注意到了吗?lazy!所以你也不用limit查询结果的返回数量,分页完全是显示层的事情了。
TraversalDescription traversalDescription = Traversal.description() .depthFirst() .relationships( KNOWS ) .relationships( LIKES, Direction.INCOMING ) .prune( Traversal.pruneAfterDepth( 5 ) ); for ( Path position : traversalDescription.traverse( myStartNode )) { System.out.println( "Path from start node to current position is " + position ); }
为了便于查询,neo4j集成lucene提供了索引机制。可以通过GraphDatabaseService访问 IndexManager,他负责利用Node 和 Relationship 中定义的索引进行数据查询,索引用Node 和 Relationship 的属性名称及值来定义。 Index提供了查询方法,返回结果是IndexHits iterator。
IndexManager indexManager = graphDb.index(); Index<Node> nodeIndex = indexManager.forNodes("a-node-index"); nodeIndex.add(node, "property","value"); for (Node foundNode = nodeIndex.get("property","value")) { assert node.getProperty("property").equals("value"); }
上面的代码就是图型数据库的基本操作,看起来都比较“底层”。要解决业务问题,需要把Node 和domain entity对应起来,需要把Relationship和对象之间的引用对应上,需要了解如何查询各种数据。
neo4j推荐用domain entity 包在 Node 外面。先创建一个接口,然后在实现类里弄一个underlyingNode属性。
一个普通的接口:
public interface Customer { public void setFirstName( String firstName ); public String getFirstName(); // ... }
一个不普通的entity:
import org.neo4j.graphdb.Node; public class CustomerImpl implements Customer { private final Node underlyingNode; private static final String KEY_FIRST_NAME = "firstName"; public CustomerImpl( Node underlyingNode ) { this.underlyingNode = underlyingNode; } public void setFirstName( String firstName ) { underlyingNode.setProperty( KEY_FIRST_NAME, firstName ); } public String getFirstName() { return ( String ) underlyingNode.getProperty( KEY_FIRST_NAME ); } // ... }
上面的代码看起来会有点累,但里面还没涉及到Relationship。还需要一个普通的接口来说明问题:
public interface Order { public void setOrderNumber( long orderNumber ); public long getOrderNumber(); public Customer getCustomer(); }
和另一个不普通的类:
import org.neo4j.graphdb.Node; public class OrderImpl implements Order { private final Node underlyingNode; // ... public Customer getCustomer() { Node customerNode = underlyingNode.getSingleRelationship( RelationshipTypes.CUSTOMER_TO_ORDER, Direction.INCOMING ).getStartNode(); return new CustomerImpl( customerNode ); } }
是的。只有get,没有set,因为一般会在Customer里加上一个Order。先走Customer接口:
public void addOrder( Order order );
再进CustomerImpl类:
public void addOrder( Order order ) { underlyingNode.createRelationshipTo( orderNode, RelationshipTypes.CUSTOMER_TO_ORDER ); }
好了,这样就行了,不需要在Order里面setCustomer了。
还有个问题,Node不会自己长出来,也不能让代码里到处都是下面这种代码
Node node = graphDb.createNode(); Order order = new OrderImpl(node);
最简单的办法就是Factory
public class OrderFactoryImpl implements OrderFactory { private final GraphDatabaseService graphDb; private final Node orderFactoryNode; public OrderFactoryImpl( GraphDatabaseService graphDb ) { this.graphDb = graphDb; Relationship rel = graphDb.getReferenceNode().getSingleRelationship( MyRelationshipTypes.ORDERS, Direction.OUTGOING ); if ( rel == null ) { orderFactoryNode = graphDb.createNode(); graphDb.getReferenceNode().createRelationshipTo( orderFactoryNode, MyRelationshipTypes.ORDERS ); } else { orderFactoryNode = rel.getEndNode(); } } public Order createOrder() { Node node = graphDb.createNode(); orderFactoryNode.createRelationshipTo( node, MyRelationshipTypes.ORDER ); return new OrderImpl( node ); } }
上面的Factory做了些额外的工作,并没有将Order 直接 关联到reference node上,而是引入了一个orderFactoryNode,这个节点和reference node之间的Relationship type 是 ORDERS,所有的order节点都是用ORDER类型的Relationship挂到他上面。neo4j将这个节点称之为subreference node,他会让图看起来更有纪律,一个类型的节点都排在一起。
neo4j可以通过四种方式进行数据搜索:
1)使用索引精确查询
这种最典型的应用就是根据ID查询,创建节点的时候,赋予一个唯一的ID,并建立对应的索引,以后就可以根据该索引查询这个唯一的节点。
private IndexService index; // ... public Order createOrder() { Node node = graphDb.createNode(); orderFactoryNode.createRelationshipTo( node, MyRelationshipTypes.ORDER ); long orderId = getNextId(); // add index index.index( node, "order", orderId ); Order order = new OrderImpl( node ); order.setId( orderId ); return order; } public Order getOrderById( int orderId ) { // use index to get order Node orderNode = index.getSingleNode( "order", orderId ); if ( orderNode != null ) { return new OrderImpl( orderNode ); } // handle no such order id here }
2)利用Relationship查询
开发的时候经常要根据对象的类别或特性进行查询,这种类别的数量通常比较稳定,这种情况下可以考虑把这些类别作为节点放到图中,建立对象节点到类别节点的关系,通过关系查询指定类别的目标对象节点。
比如要查询所有来自北京的顾客:
先创建一个城市节点,然后把所有城市加到该subreference节点上。
设置客户的居住地时,创建如下的关系:
(Customer Node)--LIVES_IN-->(Beijing Node)
这样要查找住在北京的客户数据就很容易:
public Iterable<Customer> getCustomers() { Iterable<Relationship> rels = cityNode .getRelationships( MyRelationshipTypes.LIVES_IN ); ArrayList<Customer> custs = new ArrayList<Customer>(); for ( Relationship rel : rels ) { Node customerNode = rel.getStartNode(); Customer cust = new CustomerImpl( customerNode ); custs.add(cust); } return custs; }
这个方法会返回所有以北京市为尾节点的LIVES_IN关系,我们只要取出关系的头节点,逐一创建Customer对象,加入链表,就得到了所有居住在北京的客户对象。
3)遍历图查询
neo4j的traverser api灰常强大,比如上面那个查询,就可以创建一个简单的Traverser来实现:
Traverser trav = beijingNode.traverse( Order.DEPTH_FIRST, StopEvaluator.DEPTH_ONE, ReturnableEvaluator.ALL_BUT_START_NODE, LIVES_IN, Direction.INCOMING ); // iterate over traverser...
想找到所有来自北京的客户所下的订单,也很简单,只要在遍历条件里加上一个关系类型,稍微修改一下evaluator就可以了。
Traverser trav = beijingNode.traverse(Order.DEPTH_FIRST, StopEvaluator.END_OF_GRAPH, new ReturnableEvaluator() { public boolean isReturnableNode( TraversalPosition pos ) { return !pos.isStartNode() && pos.lastRelationshipTraversed().isType( CUSTOMER_TO_ORDER );//CUSTOMER_TO_ORDER关系中的非头节点 } }, LIVES_IN, Direction.INCOMING, CUSTOMER_TO_ORDER, Direction.OUTGOING ); // iterate over traverser...
4)利用索引模糊查询
可以借助lucene的全文检索进行模糊查询,这个,深了。
neo4j提供的Java API只能算是原生态的,用起来稍嫌复杂,而且每个entity里面都弄个node也让人不爽。Spring Data Graph借助AspectJ,利用注解将underlyingNode的映射处理集中到一处,让代码看起来更清爽。此外,还对索引、查询、事务及会话处理等进行了优化,使得在图型数据库上编程简单直观,符合java传统的编程方式。
Spring Data Graph 借助 AspectJ为普通的 entities 创建对应的Node,用来保存entities的属性和与其他entities的引用关系。AspectJ会拦截对entity对象属性的访问,将其转向entity对应的node对象属性或Relationship。
为了便于进行Graph data数据的处理,AspectJ还为entities增加了一些属性和方法,比如访问存储状态的getPersistentState(),用于创建Relationship的
relateTo和获取
relationship的getRelationshipTo,还有用于查询的
find(Class<? extends NodeEntity>, TraversalDescription),并且代理了
equals和 hashCode。
Spring Data Graph提供了一系列的注解为AspectJ标记需要处理的图型数据,类级别的包括用于标记节点的@NodeEntity和标记关系的@RelationshipEntity,还有标记节点中域的@GraphProperty, @RelatedTo, @RelatedToVia, @Indexed 和 @GraphId;标记关系中域的@StartNode和@EndNode 。
@NodeEntity用于标记在图型数据库中存储为节点的POJO entity,其中的简单域默认映射为节点的属性,对其他NodeEntities对象的引用映射为关系。该标记可以设置参数,当fullIndex设置为true的时候,会对该entity的所有属性设置索引。
@NodeEntity public class Movie { String title; }
@RelationshipEntity用于标记在图型数据库中存储为边的POJO entity,在关系型数据库中为了避免多对多关系出现而创建的类几乎都可以定义为关系entity。关系entity中可以包含属性,还有必不可少的,用@StartNode和@EndNode标记的关系两头的首尾节点。Spring Graph Data 不允许new一个关系对象出来,只能通过节点类中标记为@RelatedToVia属性,或relateTo和
getRelationshipTo方法访问。
@RelationshipEntity public class Role { @StartNode private Actor actor; @EndNode private Movie movie; } @NodeEntity public class Movie { @RelatedTo(type="DIRECTED", direction = INCOMING) Person director; @RelatedTo(elementClass = Person.class, type = "ACTS_IN", direction = INCOMING) Set<Person> actors; @RelatedToVia(elementClass = Role.class, type = "ACTS_IN", direction = INCOMING) Iterable<Role> roles; public Collection<Role> getRoles() { return IteratorUtil.asCollection(roles); } // no setRoles,no setActors,no setDirector } @NodeEntity public class Person { @RelatedTo(elementClass = Movie.class, type = "DIRECTED") private Set<Movie> directedMovies; public Set<Movie> getDirectedMovies() { return directedMovies; } public void directed(Movie movie) { this.directedMovies.add(movie); } @RelatedToVia(elementClass = Role.class, type = "ACTS_IN") Iterable<Role> roles; public Iterable<Role> getRoles() { return roles; } public Role playedIn(Movie movie, String roleName) { Role role = relateTo(movie, Role.class, "ACTS_IN"); role.setName(roleName); return role; } }
@RelatedTo用于在node entity中标记和被引用的node entity之间的关系,一对一的关系一般不用进行标记,但一对多的关系因为属性是个集合,所以必须进行标记(type erase,无法通过反射获取类型),并且要指定elementClass参数,参数direction和type则根据需要说明。一对一的关系是在set属性值创建的,而把属性值set为null则会被删除。一对多的关系在集合属性中的单值被加入移除时会做相应的关系操作。
@RelatedToVia用于标记映射为关系的属性,被@RelatedToVia标记的iterable属性会提供到对应的关系实体的只读访问。
Neo4j可以基于索引进行精确匹配搜索及全文搜索,目前采用的搜索引擎为lucene。
Spring Data Graph提供了@Indexed注解来声明需要被IndexManager建立索引的属性,属性的值被修改时就会触发IndexManager调整索引,以后进行搜索时,就可以根据属性的值来查找符合条件的node 或 Relationship实体对象。通常用于遍历的start node都会被建立索引,FinderFactory创建的Finder可以根据索引查找特定的NodeEntity和RelationshipEntity。为了进行区分,可以对索引命名,@Indexed有个name参数,如果不指定,那就是Neo4j中默认的“node”或 “relationship”。
Spring Data Graph 的Finder提供的搜索方法包括:
根据ID索引查找 findById(id)
,
遍历指定类型的node (findAll),
计算指定类型node的数量 (count),
搜索所有索引属性的值符合条件的node (findAllByPropertyValue),
搜索特定的索引属性的值符合条件的node (findByPropertyValue),
搜索索引的数值属性的值在指定范围之内的node (inclusive) (findAllByRange),
循环遍历结果 (findAllByTraversal).
@NodeEntity class Person { @Indexed(indexName = "people") String name; // automatically indexed numerically @Indexed int age; } @NodeEntity @Indexed(indexName="groups") class Group { @Indexed String name; @RelatedTo(elementClass = Person.class, type = "people" ) Set<Person> people; } NodeFinder<Person> finder = finderFactory.createNodeEntityFinder(Person.class); Person dave=finder.findById(123); int people = finder.count(); Person mark = finder.findByPropertyValue("name", "mark"); Iterable<Person> devs = finder.findAllByProperyValue("occupation","developer"); Iterable<Person> davesFriends = finder.findAllByTraversal(dave, Traversal.description().pruneAfterDepth(1) .relationships(KNOWS).filter(returnAllButStartNode()));
Neo4j是支持事务的,也提供了SpringTransactionManager,Spring Data Graph可以简单的将他集成到Spring 的 JtaTransactionManager中。
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager"> <bean class="org.neo4j.kernel.impl.transaction.SpringTransactionManager"> <constructor-arg ref="graphDatabaseService"/> </bean> </property> <property name="userTransaction"> <bean class="org.neo4j.kernel.impl.transaction.UserTransactionImpl"> <constructor-arg ref="graphDatabaseService"/> </bean> </property> </bean> <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/>
默认情况下,新创建的entity并不会放到存储环境中,需要调用persist()方法之后才行,在事务中对node entity的任何修改,包括属性及其相关的关系,都会反应到数据库中,而如果在事务外修改node entity,则该node就脱离了存储环境,要加入到存储环境中,还需要调用persist()。通过API提供的所有方法得到的node entity都处于存储环境中。persist不仅仅是把单个node entity放到存储环境中,而是包括他的关系及关联的node entity。