使用 Grails 和 Neo4j 构建图形应用程序
本指南将演示如何使用 Grails 和 GORM 构建 Neo4j 电影示例应用程序
作者:Graeme·罗切
Grails 版本 3.3.0
1 Grails 培训
Grails 培训 - 由 Grails 框架的创建者和积极维护者开发并提供!
2 入门
对于不熟悉 Neo4j 的人来说,它是一个 图形数据库,它优化了图形遍历,而这在传统关系数据库中相对较慢。
GORM 的目标是轻松将现有 Groovy 领域模型与 Neo4j 节点和关系进行映射。
2.1 需要什么
要完成本指南,你将需要以下内容
-
一些时间
-
一个像样的文本编辑器或 IDE
-
已安装 JDK 1.7 或更高版本,并正确配置了
JAVA_HOME
你还需要 下载并安装 Neo4j 的“社区版”。
对于 Mac,Neo4j 以包含 Neo4j 服务器的 DMG
的形式出现,可以轻松地直接安装到 Applications
文件夹中
对于 Windows,可执行安装程序将安装 Neo4j 服务器,并可从开始菜单中使用。对于 *nix 操作系统,你可以使用 Unix 控制台应用程序。 |
新 Neo4j 服务器应用程序运行后,指定数据目录,然后单击“启动”按钮
这样会启动 Neo4j,它使用默认端口(7474
),你现在可以通过应用程序提供的链接访问 Neo4j 管理 UI
你现在可以用默认用户名和密码登录 Neo4j,它们分别是 neo4j
和 neo4j
系统会提示你登录,然后要求你更改密码。针对本教程,将值改成“movies”
2.2 如何完成指南
要开始,请执行下列操作
-
下载并解压源文件
或者
-
克隆Git代码库
git clone https://github.com/grails-guides/neo4j-movies.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是简单的 Grails 应用,带有一些附加代码,为你提早做准备。 -
complete
完整的范例。这是浏览本指南并对initial
文件夹应用这些更改之后的成果。
要完成指南,请进入 initial
文件夹
-
cd
进入grails-guides/neo4j-movies/initial
然后按照下一部分的说明进行操作。
如果你 cd 进入 grails-guides/neo4j-movies/complete ,就可以直接转到完成的范例 |
3 填充数据模型
在开始之前,首先要做的是填充数据模型。为此,请在 Neo4j 浏览器访问 http://localhost:7474/browser/,然后输入 :play movies
Cypher 是 Neo4j 使用的查询语言。例如,以下语句在图形中创建了两个新的 Neo4j 节点(一个表示 Movie
,另一个表示 Person
)
CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})
CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})
表达式 TheMatrix:Movie
左侧的值是节点名称,犹如变量名称,它不会存储在数据库中。
右侧的值是 节点
标签(在本例中是 Movie
)。
大括号内的值是节点属性。
可以在后面的语句中使用节点名称,在节点之间创建关系
CREATE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)
上面的 CREATE
语句在 Keanu
节点和 TheMatrix
节点之间创建了关系。关系类型是 ACTED_IN
,就像节点一样,关系可以具有属性(本例中是 roles
属性)。
单击代码以将其填充到 Neo4j 浏览器编辑器
此操作将必要的 Cypher 语句放入 Neo4j 浏览器编辑器,它将填充示例数据库。然后,你可以按“播放”按钮以运行代码,下方窗口将显示数据库模型的图形化可视效果
4 为 Neo4j 配置 GORM
首先,此指南基于 GORM 6.1.6.RELEASE 构建,因此,为了完成本指南,我们必须先将 Grails 配置为使用 GORM 6.1.6.RELEASE。
若要执行此操作,请将 gradle.properties
中的 gormVersion
属性设置为 6.1.6.RELEASE
grailsVersion=3.3.0
gormVersion=6.1.6.RELEASE
neo4jVersion=3.1.2
gradleWrapperVersion=3.5
build.gradle
已包含对 neo4j 插件的依赖关系。你需要修改 neo4j
依赖关系,也要使用 GORM 6.1.6.RELEASE
compile "org.grails.plugins:neo4j"
现在,修改 grails-app/conf/application.yml
文件中已存在的 neo4j 配置块。将 Neo4j 连接设置配置为使用你之前设置的“movies”密码
grails:
neo4j:
url: bolt://localhost
username: "neo4j"
password: "movies"
5 编写应用程序
如本指南先前所述,你将实现 Neo4j 示例影片应用程序。Neo4j 网站对该示例的描述如下所示
这是一个简单的单页面 Web 应用程序,它使用 Neo4j 的电影演示数据库(电影、演员、导演)作为数据集。所有应用程序中的同一个前端 Web 页面利用后端在不同编程语言和驱动程序中提供的 3 个 REST 端点。
要实现的 REST 端点有
-
按标题列出单个影片
-
按标题搜索影片
-
域的图形化可视效果
5.1 定义 GORM 域模型
现在你已正确配置了 GORM,下一步是定义一个映射 Neo4j 图形的域模型。
为此,我们准备创建域类来表示域模型中的两种类型的节点。你可以使用 CLI 的 create-domain-class
命令或你最喜欢的 IDE 或文本编辑器来执行此操作
$ grails create-domain-class neo4j.movies.Person
$ grails create-domain-class neo4j.movies.Movie
此外,我们将创建一个域类来建模两个域类之间的关系,称为 CastMember
$ grails create-domain-class neo4j.movies.CastMember
现在,打开位于 grails-app/domain/neo4j/movies/Person.groovy
的 Person
域类,并将内容修改为如下所示
package neo4j.movies
import grails.compiler.GrailsCompileStatic
/**
* Models a Person node in the graph database
*/
@GrailsCompileStatic
class Person {
String name
int born
static hasMany = [appearances: CastMember]
static constraints = {
name blank:false
born min:1900
}
}
如你所见,一个 Person
有一个 name
、一个出生年份和一个与 CastMember
的关联关系(稍后详细介绍)。
现在,打开位于 grails-app/domain/neo4j/movies/Movie.groovy
的 Movie
域类,并将内容修改为如下所示
package neo4j.movies
import grails.compiler.GrailsCompileStatic
/**
* Models a movie node in the graph database
*/
@GrailsCompileStatic
class Movie {
String title
String tagline
int released
static hasMany = [cast: CastMember]
static constraints = {
released min:1900
title blank:false
}
}
一个Movie
有title
、tagline
、发行year
,并带有与CastMember
关联。
CastMember
领域类打算对Person
和Movie
之间的关系进行建模。为此,我们将使用grails.neo4j.Relationship
特征,这样我们就能将域类映射到Neo4j关系(而不是节点)。
package neo4j.movies
import grails.neo4j.Relationship
import groovy.transform.CompileStatic
/**
* Models a relationship between a Person and a Movie
*/
@CompileStatic
class CastMember implements Relationship<Person, Movie> { (1)
List<String> roles = [] (2)
CastMember() {
type = RoleType.ACTED_IN (3)
}
static enum RoleType { (4)
ACTED_IN, DIRECTED
}
}
1 | Relationship 特征需要2个泛型参数。from 实体和to 实体。 |
2 | 关系实体也可以有属性,本例中有一个roles 属性。 |
3 | 我们将默认关系类型设置为ACTED_IN 。 |
4 | 关系类型由RoleType 枚举表示。 |
5.2 实现REST端点
如前所述,需要实现的REST端点有
-
按标题列出单个影片
-
按标题搜索影片
-
域的图形化可视效果
要实现这些不同的REST端点,请先创建一个名为MovieController
的控制器。你可以使用grails
CLI或在grails-app/controllers/neo4j/movies
目录中创建一个名称以Controller
结尾的类来通过你的文本编辑器或IDE创建。
$ grails create-controller neo4j.movies.MovieController
控制器的初始内容应如下所示
@CompileStatic
class MovieController {
static responseFormats = ['json', 'xml']
...
}
要将不同的端点映射到控制器,请将以下内容添加到grails-app/controllers/neo4j/movies/UrlMappings.groovy
文件。
"/movie/$title"(controller: 'movie', action: 'show') (1)
'/search'(controller: 'movie', action: 'search') (2)
'/graph'(controller: 'movie', action: 'graph') (3)
1 | 将/movie/{title} URI映射到MovieController 的show 操作。 |
2 | 将/search URI映射到MovieController 的search 操作。 |
3 | 将/graph URI映射到MovieController 的graph 操作。 |
当然,这些控制器操作还没有实现。让我们从第一个需求开始。
5.2.1 按标题查找端点
要按标题查找Movie
,我们首先创建一个名为MovieService
的GORM数据服务来封装数据访问逻辑和与Neo4j的交互。
你可以使用grails
CLI或在grails-app/services/neo4j/movies目录中创建一个名称以Service结尾的类来通过你的文本编辑器或IDE创建。
$ grails create-service neo4j.movies.MovieService
将服务设为abstract
,并向其中添加@Service
注释来告诉GORM该服务应自动实现。
import grails.gorm.services.Service
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
@SuppressWarnings(['UnusedVariable', 'SpaceAfterOpeningBrace', 'SpaceBeforeClosingBrace'])
@CompileStatic
@Service(Movie)
abstract class MovieService {
...
}
现在,向MovieService
添加一个名为find
的abstract
方法,该方法将title
作为参数。
@Join('cast')
abstract Movie find(String title)
GORM将自动为你实现该方法,但请注意,我们使用@Join
注释来表示我们要使用同一个查询获取cast
关联。
现在让我们实现将调用此方法的控制器操作。首先在MovieController
类中注入服务
MovieService movieService
这使用 Spring 注入依赖项并使服务实现可用。接下来添加一个名为show
的操作,它调用find
方法并以结果做出响应
def show(String title) {
respond movieService.find(title)
}
如果你现在运行应用程序并访问http://localhost:8080/movie/The%20Matrix%20Reloaded
,你将看到如下响应(为简洁起见已缩短)
{
"cast": [...],
"id": 9,
"released": 2003,
"tagline": "Free your mind",
"title": "The Matrix Reloaded"
}
如果你希望调试 GORM 执行的 Cypher 查询,则可在grails-app/conf/logback.groovy 中为org.grails.datastore.gorm.neo4j 包启用调试日志记录 |
虽然有效 JSON,但不幸的是这不是实现 Neo4j 示例应用程序所需的格式。
为自定义 JSON 创建一个grails-app/views/movie/_movie.gson
JSON 视图,并使用以下内容填充
import groovy.transform.Field
import neo4j.movies.Movie
@Field Movie movie (1)
json {
title movie.title (2)
cast tmpl.cast( 'castMember', movie.cast ) (3)
}
1 | 在模型中定义要呈现的movie |
2 | 输出Movie 标题作为 JSON |
3 | 为演出团体中的每个成员呈现其他模板 |
对tmpl.cast(..)
的调用需要定义第二个模板。模板命名空间使用方法名称调用相同名称的模板。因此,在这种情况下,我们需要创建一个grails-app/views/movies/_cast.gson
模板。
import groovy.transform.Field
import neo4j.movies.CastMember
@Field CastMember castMember
json {
job castMember.type.split("_")[0].toLowerCase()
name castMember.from.name
role castMember.roles
}
_cast.gson
模板以所需的格式格式化CastMember
关系作为 JSON。现在,如果你运行应用程序并访问与先前描述相同的 URL,则生成的输出将采用正确的 JSON 格式(为简洁起见已缩短)
{
"title": "The Matrix Reloaded",
"cast": [
{
"job": "acted",
"name": "Carrie-Anne Moss",
"role": [
"Trinity"
]
},
{
"job": "acted",
"name": "Keanu Reeves",
"role": [
"Neo"
]
},
...
]
}
第一个端点完成,让我们实现搜索!
5.2.2 搜索端点
搜索端点允许客户端按标题搜索电影,而无需知道确切的标题。要实现持久性逻辑,请将一个新方法添加到MovieService
中,它实现搜索逻辑
List<Movie> search(String q, int limit = 100) { (1)
List<Movie> results
if (q) {
results = Movie.where {
title ==~ "%${q}%" (2)
}.list(max:limit)
}
else {
results = [] (3)
}
results
}
1 | search 方法采用查询参数和最大结果的limit 参数 |
2 | 将Where Query与 like 表达式结合使用,GORM 将其转换为 Cypher CONTAINS 查询 |
3 | 如果没有指定查询,则返回一个空列表 |
执行查询时会产生以下 Cypher
MATCH (n:Movie) WHERE ( n.title CONTAINS {1} ) RETURN n as data LIMIT {2}
{1}
和{2}
是通过参数填充的命名参数,确保正确转义并防止注入攻击。
此示例还演示了 GORM 数据服务的一个重要概念,即你可以将 GORM 自动实现的abstract 方法与自定义逻辑混合。 |
现在让我们实现将调用此方法的控制器操作
def search(String q) {
respond movieService.search(q)
}
最后,search
端点以不同于show
端点的 JSON 格式返回结果。因此,我们创建一个grails-app/views/movie/search.gson
视图来格式化 JSON 结果
import groovy.transform.Field
import neo4j.movies.Movie
@Field Iterable<Movie> movieList = []
json(movieList) { Movie movie ->
released movie.released
tagline movie.tagline
title movie.title
}
如果你现在访问此 http://localhost:8080/search?q=Matrix URL 则结果 JSON 会如下所示
[
{
"released": 1999,
"tagline": "Welcome to the Real World",
"title": "The Matrix"
},
{
"released": 2003,
"tagline": "Free your mind",
"title": "The Matrix Reloaded"
},
{
"released": 2003,
"tagline": "Everything that has a beginning has an end",
"title": "The Matrix Revolutions"
}
]
5.2.3 D3 图形端点
要实现的最后一个端点是 graph
端点。该端点输出图表中数据的 JSON 格式,可由示例应用程序 UI 所使用的 D3 JavaScript 库 解析。
第一步是针对所需数据编写查询。Neo4j 的 GORM 很不错,因为它与 Cypher 查询语言集成强大。
若要执行 Cypher 查询,只需定义名为 findMovieTitlesAndCast
的 abstract
方法,该方法使用 @Cypher
注释
@Cypher("""MATCH ${Movie m}<-[:ACTED_IN]-${Person p}
RETURN ${m.title} as movie, collect(${p.name}) as cast
LIMIT $limit""")
protected abstract List<Map<String, Iterable<String>>> findMovieTitlesAndCast(int limit)
使用 @Cypher
注解,GORM 即可自动实现可为你执行 Cypher 查询的方法。请注意,你可以在查询正文中使用类名和引用属性,它们将进行类型检查以确保查询有效。
下一步是将此查询的结果转换为 D3 预期使用的格式
@ReadOnly
Map<String, Object> graph(int limit = 100) {
toD3Format(findMovieTitlesAndCast(limit))
}
@SuppressWarnings('NestedForLoop')
@CompileDynamic
private static Map<String, Object> toD3Format(List<Map<String, Iterable<String>>> result) {
List<Map<String,String>> nodes = []
List<Map<String,Object>> rels= []
int i = 0
for (entry in result) {
nodes << [title: entry.movie, label: 'movie']
int target=i
i++
for (String name : (Iterable<String>) entry.cast) {
def actor = [title: name, label: 'actor']
int source = nodes.indexOf(actor)
if (source == -1) {
nodes << actor
source = i++
}
rels << [source: source, target: target]
}
}
[nodes: nodes, links: rels]
}
通过 graph
方法获取 Neo4j 结果,然后将其转换为适当的格式。最后,我们可以向 MovieController
添加一个方法,以返回必需的数据
def graph() {
respond movieService.graph(params.int('limit', 100))
}
5.3 添加 Neo4j 电影 UI
最后一块拼图是包含官方 Neo4j 示例应用程序 UI,这是一个简单的 HTML 页面。
index.html
页面可在本教程的 complete/src/main/webapp
目录中找到,只需将其复制到应用程序的 src/main/webapp
目录中即可。
将资源配置模式添加到 grails-app/conf/application.yml
文件中
grails:
resources:
pattern: '/**'
修改 grails-app/controllers/neo4j/movies/UrlMappings.groovy
将应用程序根目录映射到此 index.html
文件。
'/'(uri: '/index.html')
完成这些操作后,即可运行应用程序!
6 运行应用程序
在启动应用程序之前,请确保 Neo4j 服务器正在运行,而且你已按照 开始 部分中的说明填充电影数据。
若要运行应用程序,请使用 ./gradlew bootRun
命令,这将会在端口 8080 上启动应用程序。
应用程序运行时请访问 http://localhost:8080,此时你应该会看到正确呈现的应用程序 UI,并附有图形可视化效果