天天看點

圖形資料庫 Neo4j 開發實戰

本人用途:知識圖譜

neo4j:圖狀資料庫

官網位址:http://neo4j.com/docs/java-reference/current/

中文API:https://www.w3cschool.cn/neo4j/neo4j_building_blocks.html

Neo4j 簡介

資料存儲一般是應用開發中不可或缺的組成部分。應用運作中産生的和所需要的資料被以特定的格式持久化下來。應用開發中很常見的一項任務是在應用本身的領域對象模型與資料存儲格式之間進行互相轉換。如果資料存儲格式與領域對象模型之間比較相似,那麼進行轉換所需的映射關系更加自然,實作起來也更加容易。對于一個特定的應用來說,其領域對象模型由應用本身的特征來決定,一般采用最自然和最直覺的方式來進行模組化。是以恰當的選擇資料存儲格式就顯得很重要。目前最常見的資料存儲格式是關系資料庫。關系資料庫通過實體 - 關系模型(E-R 模型)來進行模組化,即以表和表之間的關系來模組化。在實際開發中可以使用的關系資料庫的實作非常多,包括開源的和商用的。關系資料庫适合用來存儲資料條目的類型同構的表格型資料。如果領域對象模型中不同對象之間的關系比較複雜,則需要使用繁瑣的對象關系映射技術(Object-Relationship

Mapping,ORM)來進行轉換。

對于很多應用來說,其領域對象模型并不适合于轉換成關系資料庫形式來存儲。這也是非關系型資料庫(NoSQL)得以流行的原因。NoSQL 資料庫的種類很多,包括鍵值對資料庫、面向文檔資料庫和圖形資料庫等。本文中介紹的 Neo4j 是最重要的圖形資料庫。Neo4j 使用資料結構中圖(graph)的概念來進行模組化。Neo4j 中兩個最基本的概念是節點和邊。節點表示實體,邊則表示實體之間的關系。節點和邊都可以有自己的屬性。不同實體通過各種不同的關系關聯起來,形成複雜的對象圖。Neo4j 同時提供了在對象圖上進行查找和周遊的功能。

對于很多應用來說,其中的領域對象模型本身就是一個圖結構。對于這樣的應用,使用 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. 節點和關系的使用示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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 資料庫。

來自:https://www.ibm.com/developerworks/cn/java/j-lo-neo4j/