写点什么

使用 Neo4j 进行全栈 Web 开发

  • 2015-07-15
  • 本文字数:5150 字

    阅读完需:约 17 分钟

在开发一个全栈 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,而整个技术栈的其余部分,我的选择是:

所有代码都已开源,可以在 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

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

在这个查询中,我们将选择性地匹配所有相关的 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 如今正与他的妻儿在全球旅行。可以在 Twitter 上找到 Brian,或在 LinkedIn 上联系他。

查看英文原文: Full Stack Web Development Using Neo4j

公众号推荐:

AGI 概念引发热议。那么 AGI 究竟是什么?技术架构来看又包括哪些?AI Agent 如何助力人工智能走向 AGI 时代?现阶段营销、金融、教育、零售、企服等行业场景下,AGI应用程度如何?有哪些典型应用案例了吗?以上问题的回答尽在《中国AGI市场发展研究报告 2024》,欢迎大家扫码关注「AI前线」公众号,回复「AGI」领取。

2015-07-15 10:2027354
用户头像

发布了 428 篇内容, 共 173.3 次阅读, 收获喜欢 38 次。

关注

评论

发布
暂无评论
发现更多内容

用SpreadJS实现在线Excel的录入与展示,提升企业医保信息化服务水平

葡萄城技术团队

SpreadJS 医保信息化 在线excel

使用jdbcSstoragerHandler 处理mysql、oracle 、hive数据

飞哥

工作两年简历写成这样,谁要你呀!

小傅哥

面试 小傅哥 简历优化 找工作

Dubbo集成Sentinel实现限流

Java收录阁

sentinel

编写制度的几点实用建议

石君

制度 编写制度 安全管理

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (四)关于单元测试的常见错误观念和做法

编程道与术

Java 编程 软件测试 TDD 单元测试

基于XGB单机训练VS基于SPARK并行预测(XGBoost4j-spark无痛人流解决方案)

黄崇远@数据虫巢

学习 算法

爱是恒久忍耐,又有恩慈

霍太稳@极客邦科技

身心健康 心理

从波音747学项目管理

顾强

项目管理 读书感悟 沟通

Spring 中不同依赖注入方式的对比与剖析

Deecyn

spring

借助第一性原理开启中台建设

数字圣杯

数据中台 数字化转型

通过一个聊天应用学习 Deno

寇云

typescript 后端

智浪

Neil

后浪 智能时代 智浪

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (五)第一个单元测试

编程道与术

Java 编程 软件测试 TDD 单元测试

你竞争我得利之零售变革

孙苏勇

行业资讯

业务信息化操作系统(BIOS)——中台的核心产出物

孤岛旭日

中台 操作系统 企业信息化

回“疫”录(15):在家SOHO,是你想要的工作方式吗?

小天同学

疫情 回忆录 现实纪录 纪实 远程办公

面向页面的移动端架构设计

稻子

flutter ios android 大前端 架构模式

交易上链——中心化数字资产交易所的完美解决之道

Tux Hu

区块链 智能合约 数字货币 去中心化网络 数字资产

认识数据产品经理(三 成为数据产品经理)

马踏飞机747

大数据 数据中台 数据分析 产品经理

“字节”不断“跳动”,卡拉永远 OK?

无量靠谱

字节跳动 诺基亚 危机

高效阅读,成就自我-《麦肯锡精英高效阅读法》读后感

顾强

读书笔记 读书 读书方式

有了容器为什么kubernetes还需要Pod?

架构师修行之路

Kubernetes 分布式 云原生 pod

延时任务的几种实现方式

郭儿的跋涉

Java 延时任务 延时消息

我常用的在线工具清单

彭宏豪95

效率 效率工具 工具

算法工程师的发展路径

XYZ

编程的门槛 - 抄作业的得与失

顿晓

编程门槛 编程思维 动手能力 抄作业

反对996,但是选择996是一个怎样的矛盾心态?

顾强

职场 加班

《硅谷革命:成就苹果公司的疯狂往事》读后感

顾强

21天养不成习惯,28天也不行。不要痴心妄想。

赵新龙

TGO鲲鹏会 习惯养成

高仿瑞幸小程序 08 创建第一个云函数

曾伟@喵先森

小程序 微信小程序 大前端 移动

使用Neo4j进行全栈Web开发_DevOps & 平台工程_Brian Underwood_InfoQ精选文章