Neo4j 簡介
數據存儲通常是應用開發中不可或缺的組成部分。應用運行中產生的和所須要的數據被以特定的格式持久化下來。應用開發中很常見的一項任務是在應用自己的領域對象模型與數據存儲格式之間進行相互轉換。若是數據存儲格式與領域對象模型之間比較類似,那麼進行轉換所需的映射關係更加天然,實現起來也更加容易。對於一個特定的應用來講,其領域對象模型由應用自己的特徵來決定,通常採用最天然和最直觀的方式來進行建模。因此恰當的選擇數據存儲格式就顯得很重要。目前最多見的數據存儲格式是關係數據庫。關係數據庫經過實體 - 關係模型(E-R 模型)來進行建模,即以表和表之間的關係來建模。在實際開發中可使用的關係數據庫的實現很是多,包括開源的和商用的。關係數據庫適合用來存儲數據條目的類型同構的表格型數據。若是領域對象模型中不一樣對象之間的關係比較複雜,則須要使用繁瑣的對象關係映射技術(Object-Relationship Mapping,ORM)來進行轉換。數據結構
對於不少應用來講,其領域對象模型並不適合於轉換成關係數據庫形式來存儲。這也是非關係型數據庫(NoSQL)得以流行的緣由。NoSQL 數據庫的種類不少,包括鍵值對數據庫、面向文檔數據庫和圖形數據庫等。本文中介紹的 Neo4j 是最重要的圖形數據庫。Neo4j 使用數據結構中圖(graph)的概念來進行建模。Neo4j 中兩個最基本的概念是節點和邊。節點表示實體,邊則表示實體之間的關係。節點和邊均可以有本身的屬性。不一樣實體經過各類不一樣的關係關聯起來,造成複雜的對象圖。Neo4j 同時提供了在對象圖上進行查找和遍歷的功能。app
對於不少應用來講,其中的領域對象模型自己就是一個圖結構。對於這樣的應用,使用 Neo4j 這樣的圖形數據庫進行存儲是最適合的,由於在進行模型轉換時的代價最小。以基於社交網絡的應用爲例,用戶做爲應用中的實體,經過不一樣的關係關聯在一塊兒,如親人關係、朋友關係和同事關係等。不一樣的關係有不一樣的屬性。好比同事關係所包含的屬性包括所在公司的名稱、開始的時間和結束的時間等。對於這樣的應用,使用 Neo4j 來進行數據存儲的話,不只實現起來簡單,後期的維護成本也比較低。框架
Neo4j 使用「圖」這種最通用的數據結構來對數據進行建模使得 Neo4j 的數據模型在表達能力上很是強。鏈表、樹和散列表等數據結構均可以抽象成用圖來表示。Neo4j 同時具備通常數據庫的基本特性,包括事務支持、高可用性和高性能等。Neo4j 已經在不少生產環境中獲得了應用。流行的雲應用開發平臺 Heroku 也提供了 Neo4j 做爲可選的擴展。
在簡要介紹完 Neo4j 以後,下面介紹 Neo4j 的基本用法。
Neo4j 基本使用
在使用 Neo4j 以前,須要首先了解 Neo4j 中的基本概念。
節點和關係
Neo4j 中最基本的概念是節點(node)和關係(relationship)。節點表示實體,由 org.neo4j.graphdb.Node
接口來表示。在兩個節點之間,能夠有不一樣的關係。關係由 org.neo4j.graphdb.Relationship
接口來表示。每一個關係由起始節點、終止節點和類型等三個要素組成。起始節點和終止節點的存在,說明了關係是有方向,相似於有向圖中的邊。不過在某些狀況,關係的方向可能並無意義,會在處理時被忽略。全部的關係都是有類型的,用來區分節點之間意義不一樣的關係。在建立關係時,須要指定其類型。關係的類型由 org.neo4j.graphdb.RelationshipType
接口來表示。節點和關係均可以有本身的屬性。每一個屬性是一個簡單的名值對。屬性的名稱是 String
類型的,而屬性的值則只能是基本類型、String
類型以及基本類型和 String
類型的數組。一個節點或關係能夠包含任意多個屬性。對屬性進行操做的方法聲明在接口 org.neo4j.graphdb.PropertyContainer
中。Node
和 Relationship
接口都繼承自 PropertyContainer
接口。PropertyContainer 接口中
經常使用的方法包括獲取和設置屬性值的 getProperty 和 setProperty。
下面經過具體的示例來講明節點和關係的使用。
該示例是一個簡單的歌曲信息管理程序,用來記錄歌手、歌曲和專輯等相關信息。在這個程序中,實體包括歌手、歌曲和專輯,關係則包括歌手與專輯之間的發佈關係,以及專輯與歌曲之間的包含關係。清單 1 給出了使用 Neo4j 對程序中的實體和關係進行操做的示例。
清單 1. 節點和關係的使用示例
private static enum RelationshipTypes implements RelationshipType { PUBLISH, CONTAIN } public void useNodeAndRelationship() { GraphDatabaseService db = new EmbeddedGraphDatabase("music"); Transaction tx = db.beginTx(); try { Node node1 = db.createNode(); node1.setProperty("name", "歌手 1"); Node node2 = db.createNode(); node2.setProperty("name", "專輯 1"); node1.createRelationshipTo(node2, RelationshipTypes.PUBLISH); Node node3 = db.createNode(); node3.setProperty("name", "歌曲 1"); node2.createRelationshipTo(node3, RelationshipTypes.CONTAIN); tx.success(); } finally { tx.finish(); } }
在 清單 1 中,首先定義了兩種關係類型。定義關係類型的通常作法是建立一個實現了 RelationshipType 接口的枚舉類型。RelationshipTypes 中的 PUBLISH 和 CONTAIN 分別表示發佈和包含關係。在 Java 程序中能夠經過嵌入的方式來啓動 Neo4j 數據庫,只須要建立 org.neo4j.kernel.EmbeddedGraphDatabase 類的對象,並指定數據庫文件的存儲目錄便可。在使用 Neo4j 數據庫時,進行修改的操做通常須要包含在一個事務中來進行處理。經過 GraphDatabaseService 接口的 createNode 方法能夠建立新的節點。Node 接口的 createRelationshipTo 方法能夠在當前節點和另一個節點之間建立關係。
另一個與節點和關係相關的概念是路徑。路徑有一個起始節點,接着的是若干個成對的關係和節點對象。路徑是在對象圖上進行查詢或遍歷的結果。Neo4j 中使用 org.neo4j.graphdb.Path 接口來表示路徑。Path 接口提供了對其中包含的節點和關係進行處理的一些操做,包括 startNode 和 endNode 方法來獲取起始和結束節點,以及 nodes 和 relationships 方法來獲取遍歷全部節點和關係的 Iterable 接口的實現。關於圖上的查詢和遍歷,在下面小節中會進行具體的介紹。
使用索引
當 Neo4j 數據庫中包含的節點比較多時,要快速查找知足條件的節點會比較困難。Neo4j 提供了對節點進行索引的能力,能夠根據索引值快速地找到相應的節點。清單 2 給出了索引的基本用法。
清單 2. 索引的使用示例
public void useIndex() { GraphDatabaseService db = new EmbeddedGraphDatabase("music"); Index<Node> index = db.index().forNodes("nodes"); Transaction tx = db.beginTx(); try { Node node1 = db.createNode(); String name = "歌手 1"; node1.setProperty("name", name); index.add(node1, "name", name); node1.setProperty("gender", "男"); tx.success(); } finally { tx.finish(); } Object result = index.get("name", "歌手 1").getSingle() .getProperty("gender"); System.out.println(result); // 輸出爲「男」 }
在 清單 2 中,經過 GraphDatabaseService 接口的 index 方法能夠獲得管理索引的 org.neo4j.graphdb.index.IndexManager 接口的實現對象。Neo4j 支持對節點和關係進行索引。經過 IndexManager 接口的 forNodes 和 forRelationships 方法能夠分別獲得節點和關係上的索引。索引經過 org.neo4j.graphdb.index.Index 接口來表示,其中的 add 方法用來把節點或關係添加到索引中,get 方法用來根據給定值在索引中進行查找。
圖的遍歷
在圖上進行的最實用的操做是圖的遍歷。經過遍歷操做,能夠獲取到與圖中節點之間的關係相關的信息。Neo4j 支持很是複雜的圖的遍歷操做。在進行遍歷以前,須要對遍歷的方式進行描述。遍歷的方式的描述信息由下列幾個要素組成。
- 遍歷的路徑:一般用關係的類型和方向來表示。
- 遍歷的順序:常見的遍歷順序有深度優先和廣度優先兩種。
- 遍歷的惟一性:能夠指定在整個遍歷中是否容許通過重複的節點、關係或路徑。
- 遍歷過程的決策器:用來在遍歷過程當中判斷是否繼續進行遍歷,以及選擇遍歷過程的返回結果。
- 起始節點:遍歷過程的起點。
Neo4j 中遍歷方式的描述信息由 org.neo4j.graphdb.traversal.TraversalDescription 接口來表示。經過 TraversalDescription 接口的方法能夠描述上面介紹的遍歷過程的不一樣要素。類 org.neo4j.kernel.Traversal 提供了一系列的工廠方法用來建立不一樣的 TraversalDescription 接口的實現。清單 3 中給出了進行遍歷的示例。
清單 3. 遍歷操做的示例
TraversalDescription td = Traversal.description() .relationships(RelationshipTypes.PUBLISH) .relationships(RelationshipTypes.CONTAIN) .depthFirst() .evaluator(Evaluators.pruneWhereLastRelationshipTypeIs(RelationshipTypes.CONTAIN)); Node node = index.get("name", "歌手 1").getSingle(); Traverser traverser = td.traverse(node); for (Path path : traverser) { System.out.println(path.endNode().getProperty("name")); }
在 清單 3 中,首先經過 Traversal 類的 description 方法建立了一個默認的遍歷描述對象。經過 TraversalDescription 接口的 relationships 方法能夠設置遍歷時容許通過的關係的類型,而 depthFirst 方法用來設置使用深度優先的遍歷方式。比較複雜的是表示遍歷過程的決策器的 evaluator 方法。該方法的參數是 org.neo4j.graphdb.traversal.Evaluator 接口的實現對象。Evalulator 接口只有一個方法 evaluate。evaluate 方法的參數是 Path 接口的實現對象,表示當前的遍歷路徑,而 evaluate 方法的返回值是枚舉類型 org.neo4j.graphdb.traversal.Evaluation,表示不一樣的處理策略。處理策略由兩個方面組成:第一個方面爲是否包含當前節點,第二個方面爲是否繼續進行遍歷。Evalulator 接口的實現者須要根據遍歷時的當前路徑,作出相應的決策,返回適當的 Evaluation 類型的值。類 org.neo4j.graphdb.traversal.Evaluators 提供了一些實用的方法來建立經常使用的 Evalulator 接口的實現對象。清單 3 中使用了 Evaluators 類的 pruneWhereLastRelationshipTypeIs 方法。該方法返回的 Evalulator 接口的實現對象會根據遍歷路徑的最後一個關係的類型來進行判斷,若是關係類型知足給定的條件,則再也不繼續進行遍歷。
清單 3 中的遍歷操做的做用是查找一個歌手所發行的全部歌曲。遍歷過程從表示歌手的節點開始,沿着 RelationshipTypes.PUBLISH 和 RelationshipTypes.CONTAIN 這兩種類型的關係,按照深度優先的方式進行遍歷。若是當前遍歷路徑的最後一個關係是 RelationshipTypes.CONTAIN 類型,則說明路徑的最後一個節點包含的是歌曲信息,能夠終止當前的遍歷過程。經過 TraversalDescription 接口的 traverse 方法能夠從給定的節點開始遍歷。遍歷的結果由 org.neo4j.graphdb.traversal.Traverser 接口來表示,能夠從該接口中獲得包含在結果中的全部路徑。結果中的路徑的終止節點就是表示歌曲的實體。
Neo4j 實戰開發
在介紹了 Neo4j 的基本使用方式以後,下面經過具體的案例來講明 Neo4j 的使用。做爲一個數據庫,Neo4j 能夠很容易地被使用在 Web 應用開發中,就如同一般使用的 MySQL、SQL Server 和 DB2 等關係數據庫同樣。不一樣之處在於如何對應用中的數據進行建模,以適應後臺存儲方式的需求。一樣的領域模型,既能夠映射爲關係數據庫中的 E-R 模型,也能夠映射爲圖形數據庫中的圖模型。對於某些應用來講,映射爲圖模型更爲天然,由於領域模型中對象之間的各類關係會造成複雜的圖結構。
本節中使用的示例是一個簡單的微博應用。在微博應用中,主要有兩種實體,即用戶和消息。用戶之間能夠互相關注,造成一個圖結構。用戶發佈不一樣的微博消息。表示微博消息的實體也是圖中的一部分。從這個角度來講,使用 Neo4j 這樣的圖形數據庫,能夠更好地描述該應用的領域模型。
如同使用關係數據庫同樣,在使用 Neo4j 時,既可使用 Neo4j 自身的 API,也可使用第三方框架。Spring 框架中的 Spring Data 項目提供了對 Neo4j 的良好支持,能夠在應用開發中來使用。Spring Data 項目把 Neo4j 數據庫中的 CRUD 操做、使用索引和進行圖的遍歷等操做進行了封裝,提供了更加抽象易用的 API,並經過使用註解來減小開發人員所要編寫的代碼量。示例的代碼都是經過 Spring Data 來使用 Neo4j 數據庫的。下面經過具體的步驟來介紹如何使用 Spring Data 和 Neo4j 數據庫。
開發環境
使用 Neo4j 進行開發時的開發環境的配置比較簡單。只須要根據 參考資源中給出的地址,下載 Neo4j 自己的 jar 包以及所依賴的 jar 包,並加到 Java 程序的 CLASSPATH 中就能夠了。不過推薦使用 Maven 或 Gradle 這樣的工具來進行 Neo4j 相關依賴的管理。
定義數據存儲模型
前面已經提到了應用中有兩種實體,即用戶和消息。這兩種實體須要定義爲對象圖中的節點。清單 1 中給出的建立實體的方式並不直觀,並且並無專門的類來表示實體,後期維護成本比較高。Spring Data 支持在通常的 Java 類上添加註解的方式來聲明 Neo4j 中的節點。只須要在 Java 類上添加 org.springframework.data.neo4j.annotation.NodeEntity 註解便可,如 清單 4 所示。
清單 4. 使用 NodeEntity 註解聲明節點類
@NodeEntity public class User { @GraphId Long id; @Indexed String loginName; String displayName; String email; }
如 清單 4 所示,User 類用來表示用戶,做爲圖中的節點。User 類中的域自動成爲節點的屬性。註解 org.springframework.data.neo4j.annotation.GraphId 代表該屬性做爲實體的標識符,只能使用 Long 類型。註解 org.springframework.data.neo4j.annotation.Indexed 代表爲屬性添加索引。
節點之間的關係一樣用註解的方式來聲明,如 清單 5 所示。
清單 5. 使用 RelationshipEntity 註解聲明關係類
@RelationshipEntity(type = "FOLLOW") public class Follow { @StartNode User follower; @EndNode User followed; Date followingDate = new Date(); }
在 清單 5 中,RelationshipEntity 註解的屬性 type 表示關係的類型,StartNode 和 EndNode 註解則分別表示關係的起始節點和終止節點。
在表示實體的類中也能夠添加對關聯的節點的引用,如 清單 6 中給出的 User 類中的其餘域。
清單 6. User 類中對關聯節點的引用
@RelatedTo(type = "FOLLOW", direction = Direction.INCOMING) @Fetch Set<User> followers = new HashSet<User>(); @RelatedTo(type = "FOLLOW", direction = Direction.OUTGOING) @Fetch Set<User> followed = new HashSet<User>(); @RelatedToVia(type = "PUBLISH") Set<Publish> messages = new HashSet<Publish>();
如 清單 6 所示,註解 RelatedTo 表示與當前節點經過某種關係關聯的節點。由於關係是有向的,能夠經過 RelatedTo 的 direction 屬性來聲明關係的方向。對當前用戶節點來講,若是 FOLLOW 關係的終止節點是當前節點,則說明關係的起始節點對應的用戶是當前節點對應的用戶的粉絲,用「direction = Direction.INCOMING」來表示。所以 followers 域表示的是當前用戶的粉絲的集合,而 followed 域表示的是當前用戶所關注的用戶的集合。註解 RelatedToVia 和 RelatedTo 的做用相似,只不過 RelatedToVia 不關心關係的方向,只關心類型。所以 messages 域包含的是當前用戶所發佈的消息的集合。
數據操做
在定義了數據存儲模型以後,須要建立相應的類來對數據進行操做。數據操做的對象是數據模型中的節點和關係類的實例,所涉及的操做包括常見的 CRUD,即建立、讀取、更新和刪除,還包括經過索引進行的查找和圖上的遍歷操做等。因爲這些操做的實現方式都比較相似,Spring Data 對這些操做進行了封裝,提供了簡單的使用接口。Spring Data 所提供的數據操做核心接口是 org.springframework.data.neo4j.repository.GraphRepository。GraphRepository 接口繼承自三個提供不一樣功能的接口:org.springframework.data.neo4j.repository.CRUDRepository 接口提供 save、delete、findOne 和 findAll 等方法,用來進行基本的 CRUD 操做;org.springframework.data.neo4j.repository.IndexRepository 則提供了 findByPropertyValue、findAllByPropertyValue 和 findAllByQuery 等方法,用來根據索引來查找;org.springframework.data.neo4j.repository.TraversalRepository 則提供了 findAllByTraversal 方法,用來根據 TraversalDescription 接口的描述來進行遍歷操做。
Spring Data 爲 GraphRepository 接口提供了默認的實現。在大多數狀況下,只須要聲明一個接口繼承自 GraphRepository 接口便可,Spring Data 會在運行時建立相應的實現類的對象。對錶示用戶的節點類 User 進行操做的接口 UserRepository 如 清單 7 所示。
清單 7. 操做 User 類的 UserRepository 接口
public interface UserRepository extends GraphRepository<User> { }
如 清單 7 所示,UserRepository 接口繼承自 GraphRepository 接口,並經過泛型聲明要操做的是 User 類。對節點類的操做比較簡單,而對於關係類的操做就相對複雜一些。清單 8 中給出了對發佈關係進行操做的接口 PublishRepository 的實現。
清單 8. 操做 Publish 類的 PublishRepository 接口
public interface PublishRepository extends GraphRepository<Publish> { @Query("start user1=node({0}) " + " match user1-[:FOLLOW]->user2-[r2:PUBLISH]->followedMessage" + " return r2") List<Publish> getFollowingUserMessages(User user); @Query("start user=node({0}) match user-[r:PUBLISH]->message return r") List<Publish> getOwnMessages(User user); }
在 清單 8 中,getFollowingUserMessages 方法用來獲取某個用戶關注的全部其餘用戶所發佈的消息。該方法的實現是經過圖上的遍歷操做來完成的。Spring Data 提供了一種簡單的查詢語言來描述遍歷操做。經過在方法上添加 org.springframework.data.neo4j.annotation.Query 註解來聲明所使用的遍歷方式便可。以 getFollowingUserMessages 方法的遍歷聲明爲例,「node({0})」表示當前節點,「start user1=node({0})」表示從當前節點開始進行遍歷,並用 user1 表示當前節點。「match」用來表示遍歷時選中的節點應該知足的條件。條件「user1-[:FOLLOW]->user2-[r2:PUBLISH]->followedMessage」中,先經過類型爲 FOLLOW 的關係找到 user1 所關注的用戶,以 user2 來表示;再經過類型爲 PUBLISH 的關係,查找 user2 所發佈的消息。「return」用來返回遍歷的結果,r2 表示的是類型爲 PUBLISH 的關係,與 getFollowingUserMessages 方法的返回值類型 List<Publish> 相對應。
在應用中的使用
在定義了數據操做的接口以後,就能夠在應用的服務層代碼中使用這些接口。清單 9 中給出了用戶發佈新微博時的操做方法。
清單 9. 用戶發佈新微博的方法
@Autowired UserRepository userRepository; @Transactional public void publish(User user, String content) { Message message = new Message(content); messageRepository.save(message); user.publish(message); userRepository.save(user); }
如 清單 9 所示,publish 方法用來給用戶 user 發佈內容爲 content 的微博。域 userRepository 是 UserRepository 接口的引用,由 Spring IoC 容器在運行時自動注入依賴,該接口的具體實現由 Spring Data 提供。在 publish 方法中,首先建立一個 Message 實體類的對象,表示消息節點;再經過 save 方法把該節點保存到數據庫中。User 類的 publish 方法的實現如 清單 10 所示,其邏輯是建立一個 Publish 類的實例表示發佈關係,並創建用戶和消息實體之間的關係。最後再更新 user 對象便可。
清單 10. User 類的 publish 方法
@RelatedToVia(type = "PUBLISH") Set<Publish> messages = new HashSet<Publish>(); public Publish publish(Message message) { Publish publish = new Publish(this, message); this.messages.add(publish); return publish; }
在建立了相關的服務層類以後,就能夠從服務層中暴露出相關的使用 JSON 的 REST 服務,而後在 REST 服務的基礎上建立應用的前端展現界面。界面的實現部分與 Neo4j 並沒有關係,在這裏再也不贅述。整個程序基於 Spring 框架來開發。Spring Data 爲 Neo4j 提供了獨立的配置文件名稱空間,能夠方便在 Spring 配置文件中對 Neo4j 進行配置。清單 11 給出了與 Neo4j 相關的 Spring 配置文件。
清單 11. Neo4j 的 Spring 配置文件
<neo4j:config storeDirectory="data/neo-mblog.db" /> <neo4j:repositories base-package="com.chengfu.neomblog.repository" />
在 清單 11 中,config 元素用來設置 Neo4j 數據庫的數據保存目錄,repositories 元素用來聲明操做 Neo4j 中的節點和關係類的 GraphRepository 接口的子接口的包名。Spring Data 會負責在運行時掃描該 Java 包,併爲其中包含的接口建立出對應的實現對象。
示例應用的完整代碼存放在 GitHub 上,見 參考資源。
使用 Neo4j 原生 API
若是不使用 Spring Data 提供的 Neo4j 支持,而使用 Neo4j 的原生 API,也是同樣能夠進行開發。只不過因爲 Neo4j 的原生 API 的抽象層次較低,使用起來不是很方便。下面以示例應用中用戶發佈微博的場景來展現原生 API 的基本用法,見 清單 12。
清單 12. 使用 Neo4j 原生 API
public void publish(String username, String message) { GraphDatabaseService db = new EmbeddedGraphDatabase("mblog"); Index<Node> index = db.index().forNodes("nodes"); Node ueserNode = index.get("user-loginName", username).getSingle(); if (ueserNode != null){ Transaction tx = db.beginTx(); try { Node messageNode = db.createNode(); messageNode.setProperty("message", message); ueserNode.createRelationshipTo(messageNode, RelationshipTypes.PUBLISH); tx.success(); } finally { tx.finish(); } } }
從 清單 12 中能夠看出,原生 API 的基本用法是先經過 Neo4j 數據庫的索引找到須要操做的表示用戶的節點,而後再建立出表示微博消息的節點,最後在兩個節點之間創建關係。這些步驟都使用 Neo4j 的基本 API 來完成。
與 清單 10 中使用 Spring Data 的功能相同的方法進行比較,能夠發現使用原生 API 的代碼要複雜很多,而使用 Spring Data 的則簡潔不少。所以,在實際開發中推薦使用 Spring Data。
小結
關係數據庫在很長一段時間都是大多數應用採用的數據存儲方式的首要選擇。隨着技術的發展,愈來愈多的 NoSQL 數據庫開始流行起來。對於應用開發人員來講,不該該老是盲目使用關係數據庫,而是要根據應用自己的特色,選用最合適的存儲方式。Neo4j 數據庫以「圖」做爲數據之間關係的描述方式,很是適合於使用在數據自己就以圖結構來組織的應用中。本文對 Neo4j 數據庫的使用作了詳細的介紹,能夠幫助開發人員瞭解和使用 Neo4j 數據庫。