使用Neo4j進行全棧Web開發
在開發一個全棧web應用時,作為整個棧的底層,你可以在多種數據庫之間進行選擇。作為事實的數據源,你當然希望選擇一種可靠的數據庫,但同時也希望它能夠允許你以良好的方式進行數據建模。在本文中,我將為你介紹Neo4j,當你的數據模型包含大量關聯數據以及關系時,它可以成為你的web應用棧的基礎的一個良好選擇。
Neo4j是什么?
圖1. Neo4j Web控制臺
Neo4j是一個圖形數據庫,這也就意味著它的數據并非保存在表或集合中,而是保存為節點以及節點之間的關系。在Neo4j中,節點以及關系都能夠包含保存值的屬性,此外:
- 可以為節點設置零或多個標簽(例如Author或Book)
- 每個關系都對應一種類型(例如WROTE或FRIEND_OF)
- 關系總是從一個節點指向另一個節點(但可以在不考慮指向性的情況下進行查詢)
為什么要選擇Neo4j?
在考慮為web應用選擇某個數據庫時,我們需要考慮對它有哪些方面的期望,其中最重要的一些條件包括:
- 它是否易于使用?
- 它是否允許你方便地回應對需求的變更?
- 它是否支持高性能查詢?
- 是否能夠方便地對其進行數據建模?
- 它是否支持事務?
- 它是否支持大規模應用?
- 它是否足夠有趣(很遺憾的是對于數據庫的這方面要求經常被忽略)?
從這幾個方面來說,Neo4j是一個合適的選擇。Neo4j……
- 自帶一套易于學習的查詢語言(名為Cypher)
- 不使用schema,因此可以滿足你的任何形式的需求
- 與關系型數據庫相比,對于高度關聯的數據(圖形數據)的查詢快速要快上許多
- 它的實體與關系結構非常自然地切合人類的直觀感受
- 支持兼容ACID的事務操作
- 提供了一個高可用性模型,以支持大規模數據量的查詢,支持備份、數據局部性以及冗余
- 提供了一個可視化的查詢控制臺,你不會對它感到厭倦的
什么時候不應使用Neo4j?
作為一個圖形NoSQL數據庫,Neo4j提供了大量的功能,但沒有什么解決方案是完美的。在以下這些用例中,Neo4j就不是非常適合的選擇:
- 記錄大量基于事件的數據(例如日志條目或傳感器數據)
- 對大規模分布式數據進行處理,類似于Hadoop
- 二進制數據存儲
- 適合于保存在關系型數據庫中的結構化數據
在上面的示例中,你看到了由Author、City、Book和Category以及它們之間的關系所組成的一個圖形。如果你希望通過Cypher語句在Neo4j web控制臺中列出這些數據結果,可以執行以下語句:
MATCH (city:City)<-[:LIVES_IN]-(:Author)-[:WROTE]-> (book:Book)-[:HAS_CATEGORY]->(category:Category) WHERE city.name = “Chicago” RETURN *
請注意這種ASCII風格的語法,它在括號內表示節點名稱,并用箭頭表示一個節點指向另一個節點的關系。Cypher通過這種方式允許你匹配某個指定的子圖形模式。
當然,Neo4j的功能不僅僅在于展示漂亮的圖片。如果你希望按照作者所處的地點(城市)計算書籍的分類數目,你可以通過使用相同的MATCH模式,返回一組不同的列,例如:
MATCH (city:City)<-[:LIVES_IN]-(:Author)-[:WROTE]-> (book:Book)-[:HAS_CATEGORY]->(category:Category) RETURN city.name, category.name, COUNT(book)
執行這條語句將返回以下結果:
|
city.name |
category.name |
COUNT(category) |
|
Chicago |
Fantasy |
1 |
|
Chicago |
Non-Fiction |
2 |
雖然Neo4j也能夠處理“大數據”,但它畢竟不是Hadoop、HBase或Cassandra,通常來說不會在Neo4j數據庫中直接處理海量 數據(以PB為單位)的分析。但如果你樂于提供關于某個實體及其相鄰數據關系(比如你可以提供一個web頁面或某個API返回其結果),那么它是一種良好 的選擇。無論是簡單的CRUD訪問,或是復雜的、深度嵌套的資源視圖都能夠勝任。
你應該選擇哪種技術棧以配合Neo4j?
所有主流的編程語言都通過HTTP API的方式支持Neo4j,或者采用基本的HTTP類庫,或是通過某些原生的類庫提供更高層的抽象。此外,由于Neo4j是以Java語言編寫的,因此所有包含JVM接口的語言都能夠充分利用Neo4j中的高性能API。
Neo4j本身也提供了一個“技術棧”,它允許你選擇不同的訪問方式,包括簡單訪問乃至原生性能等等。它提供的特性包括:
- 通過一個HTTP API執行Cypher查詢,并獲取JSON格式的結果
- 一種“非托管擴展”機制,允許你為Neo4j數據庫編寫自己的終結點
- 通過一個高層Java API指定節點與關系的遍歷
- 通過一個低層的批量加載API處理海量初始數據的獲取
- 通過一個核心Java API直接訪問節點與關系,以獲得最大的性能
一個應用程序示例
最近我正好有機會將一個項目擴展為基于Neo4j的應用程序。該應用程序(可以訪問graphgist.neo4j.com查 看)是關于GraphGist的一個門戶網站。GraphGist是一種通過交互式地渲染(在你的瀏覽器中)生成的文檔,它基于一個簡單的文本文件 (AsciiDoctor),其中用文字描述以及圖片描述了整個數據模型、架構以及用例查詢,可以在線執行它們,并使它們保持可視化。它非常類似一個iPython notebook或是一張交互式的白紙。GraphGist也允許讀者在瀏覽器中編寫自己定義的查詢,以查看整個數據集。
Neo4j的原作者Neo Technology希望為GraphGist提供一個由社區創建的展示項目。當然,后端技術選用了Neo4j,而整個技術棧的其余部分,我的選擇是:
- Node.js配合Express.js,其中引入了neo4j包
- Angular.js
- Swagger UI
所有代碼都已開源,可以在GitHub上任意瀏覽。
從概念上講,GraphGist門戶網站是一個簡單的應用,它提供了一個GraphGist列表,允許用戶查看每個GraphGist的詳細內容。 數據領域是由Gist、Keyword/Domain/Use Case(作為Gist分類)以及Person(作為Gist的作者)所組成的:
現在你已經熟悉這個模型了,在繼續深入學習之前,我想為你快速地介紹一下Cypher這門查詢語言。舉例來說,如果我們需要返回所有的Gist和它們的關鍵字,可以通過以下語句實現:
MATCH (gist:Gist)-[:HAS_KEYWORD]->(keyword:Keyword) RETURN gist.title, keyword.name
這段語句將返回一張表,其中的每一行是由每個Gist和Keyword的組合構成的,正如同SQL join的行為一樣。現在我們更深入一步,假設我們想要找到某個人所編寫的Gist對應的所有Domain,我們可以執行下面這條查詢語句:
MATCH (person:Person)-[:WRITER_OF]->(gist:Gist)-[:HAS_DOMAIN]->(domain:Domain) WHERE person.name = “John Doe” RETURN domain.name, COUNT(gist)
該語句將返回另一個結果表,其中的每一行包含Domain的名稱,以及這個Person對于這一Domain所編寫的全部Gist的數量。這里無需使用GROUP BY語句,因為當我們使用例如COUNT()這樣的聚合函數時,Neo4j會自動在RETURN語句中對其它列進行分組操作。
現在你對Cypher已經有一點感覺了吧?那么讓我們來看一個來自實際應用中的查詢。在創建這個門戶時,如果能夠通過某種方式,只需對數據庫進行一次請求就能夠返回我們所需的所有數據,并且以一種我們需要的格式進行結構組織,那將十分有用。
讓我們開始創建這個用于門戶的API(可以在GitHub上找到)的查詢吧。首先,我們需要按照Gist的title屬性進行匹配,并匹配所有相關的Gist節點:
// Match Gists based on title
MATCH (gist:Gist) WHERE gist.title =~ {search_query}
// Optionally match Gists with the same keyword
// and pass on these related Gists with the
// most common keywords first
OPTIONAL MATCH (gist)-[:HAS_KEYWORD]->(keyword)<-[:HAS_KEYWORD]-(related_gist) 這里有幾個要注意的地方。首先,WHERE語句是通過一個正則表達式(即=~操作符)和一個參數對title屬性進行匹配的。參數 (Parameter)是Neo4j的一項特性,它能夠將查詢與其所代表的數據進行分離。使用參數能夠讓Neo4j對查詢和查詢計劃進行緩存,這也意味著 你無需擔心遭遇查詢注入攻擊。其次,我們在這里使用了一個OPTIONAL MATCH語句,它表示我們希望始終返回原始的Gist,即使它并沒有相關的Gist。
現在讓我們對之前的查詢進行擴展,將RETURN語句替換為WITH語句:
MATCH (gist:Gist) WHERE gist.title =~ {search_query}
OPTIONAL MATCH (gist)-[:HAS_KEYWORD]->(keyword)<-[:HAS_KEYWORD]-(related_gist)
WITH gist, related_gist, COUNT(DISTINCT keyword.name) AS keyword_count
ORDER BY keyword_count DESC
RETURN
gist,
COLLECT(DISTINCT {related: { id: related_gist.id, title:
related_gist.title, poster_image: related_gist.poster_image, url:
related_gist.url }, weight: keyword_count }) AS related</pre>
在RETURN語句中的COLLECT()作用是將由Gist和相關Gist所組成的節點轉換為一個結果集,讓其中每一行Gist只出現一次,并對應一個相關Gist的節點數組。在COLLECT()語句中,我們在相關Gist中僅指定了所需的部分數據,以減小整個響應的大小。
最后,我們將產生這樣一條查詢語句,這也是最后一次使用WITH語句了:
MATCH (gist:Gist) WHERE gist.title =~ {search_query}
OPTIONAL MATCH (gist)-[:HAS_KEYWORD]->(keyword)<-[:HAS_KEYWORD]-(related_gist)
WITH gist, related_gist, COUNT(DISTINCT keyword.name) AS keyword_count
ORDER BY keyword_count DESC
WITH
gist,
COLLECT(DISTINCT {related: { id: related_gist.id, title: related_gist.title, poster_image: related_gist.poster_image, url: related_gist.url }, weight: keyword_count }) AS related
// Optionally match domains, use cases, writers, and keywords for each Gist
OPTIONAL MATCH (gist)-[:HAS_DOMAIN]->(domain:Domain)
OPTIONAL MATCH (gist)-[:HAS_USECASE]->(usecase:UseCase)
OPTIONAL MATCH (gist)<-[:WRITER_OF]-(writer:Person)
OPTIONAL MATCH (gist)-[:HAS_KEYWORD]->(keyword:Keyword)
// Return one Gist per row with arrays of domains, use cases, writers, and keywords
RETURN
gist,
related,
COLLECT(DISTINCT domain.name) AS domains,
COLLECT(DISTINCT usecase.name) AS usecases,
COLLECT(DISTINCT keyword.name) AS keywords
COLLECT(DISTINCT writer.name) AS writers,
ORDER BY gist.title</pre>
在這個查詢中,我們將選擇性地匹配所有相關的Domain、Use Case、Keyword和Person節點,并且將它們全部收集起來,與我們對相關Gist的處理方式相同。現在我們的結果不再是平坦的、反正規化的, 而是包含一列Gist,其中每個Gist都對應著相關Gist的數組,形成了一種“has many”的關系,并且沒有任何重復數據。太酷了!
不僅如此,如果你覺得用表的形式返回數據太老土,那么Cypher也可以返回對象:
RETURN
{
gist: gist,
domains: collect(DISTINCT domain.name) AS domains,
usecases: collect(DISTINCT usecase.name) AS usecases,
writers: collect(DISTINCT writer.name) AS writers,
keywords: collect(DISTINCT keyword.name) AS keywords,
related_gists: related
}
ORDER BY gist.title
通常來說,在稍具規模的web應用程序中,需要進行大量的數據庫調用以返回HTTP響應所需的數據。雖然你可以并行地執行查詢,但通常來說你需要首 先返回某個查詢的結果集,才能發送另一個數據庫請求以獲取相關的數據。在SQL中,你可以通過生成復雜的、開銷很大的表join語句,通過一個查詢從多張 表中返回結果。但只要你在同一個查詢中進行了多次SQL join,這個查詢的復雜性將會飛快地增長。更不用說數據庫仍然需要進行表或索引掃描才能夠獲得相應的數據了。而在Neo4j中,通過關系獲取實體的方式 是直接使用對應于相關節點的指針,因此服務器可以隨意進行遍歷。
盡管如此,這種方式也存在著諸多缺陷。雖然這種方式能夠通過一個查詢返回所有數據,但這個查詢會相當長。我至今也沒有找到一種方式能夠對進行模塊化 以便重用。進一步考慮:我們可以在其它場合同樣調用這個終結點,但讓它顯示相關Gist的更多信息。我們可以選擇修改這個查詢以返回更多的數據,但也意味 著對于原始的用例來說,它返回了額外的不必要數據。
我們是幸運的,因為有這么多優秀的數據庫可以選擇。雖然關系型數據庫對于保存結構化數據來說依然是最佳的選擇,但NoSQL數據庫更適合于管理半結 構化數據、非結構化數據以及圖形數據。如果你的數據模型中包括大量的關聯數據,并且希望使用一種直觀的、有趣的并且快速的數據庫進行開發,那么你就應當嘗 試一下Neo4j。
本文由Brian Underwood撰寫,而Michael Hunger也為本文作出了許多貢獻。
關于作者
Brian Underwood是一位軟件工程師,喜愛任何與數據相關的東西。作為一名Neo4j 的Developer Advocate,以及neo4j ruby gem的維護者,Brian經常通過一些演講,以及在他的博客上的文章宣傳圖形數據庫的強大與簡潔。Brian如今正與他的妻兒在全球旅行。可以在推ter 上找到Brian,或在LinkedIn上聯系他。
查看英文原文:Full Stack Web Development Using Neo4j
來自:http://www.infoq.com/cn/articles/full-stack-web-development-using-neo4j