显示导航

使用 Grails 和 Neo4j 构建图形应用程序

本指南将演示如何使用 Grails 和 GORM 构建 Neo4j 电影示例应用程序

作者:Graeme·罗切

Grails 版本 3.3.0

1 Grails 培训

Grails 培训 - 由 Grails 框架的创建者和积极维护者开发并提供!

2 入门

在本指南中,你将学习如何构建 Neo4j 电影示例应用程序,它在 多种语言 中实现,作为 Neo4j 官方示例应用程序。

对于不熟悉 Neo4j 的人来说,它是一个 图形数据库,它优化了图形遍历,而这在传统关系数据库中相对较慢。

GORM 的目标是轻松将现有 Groovy 领域模型与 Neo4j 节点和关系进行映射。

2.1 需要什么

要完成本指南,你将需要以下内容

  • 一些时间

  • 一个像样的文本编辑器或 IDE

  • 已安装 JDK 1.7 或更高版本,并正确配置了 JAVA_HOME

你还需要 下载并安装 Neo4j 的“社区版”。

对于 Mac,Neo4j 以包含 Neo4j 服务器的 DMG 的形式出现,可以轻松地直接安装到 Applications 文件夹中

neo4j application
对于 Windows,可执行安装程序将安装 Neo4j 服务器,并可从开始菜单中使用。对于 *nix 操作系统,你可以使用 Unix 控制台应用程序

新 Neo4j 服务器应用程序运行后,指定数据目录,然后单击“启动”按钮

neo4j start

这样会启动 Neo4j,它使用默认端口(7474),你现在可以通过应用程序提供的链接访问 Neo4j 管理 UI

neo4j localhost

你现在可以用默认用户名和密码登录 Neo4j,它们分别是 neo4jneo4j

neo4j password

系统会提示你登录,然后要求你更改密码。针对本教程,将值改成“movies”

neo4j setpassword

2.2 如何完成指南

要开始,请执行下列操作

或者

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

neo4j playmovies

这样会显示电影数据库的 领域模型 预览,它通过一系列 Cypher 语句 CREATE 语句表示。

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 浏览器编辑器

neo4j clickcode

此操作将必要的 Cypher 语句放入 Neo4j 浏览器编辑器,它将填充示例数据库。然后,你可以按“播放”按钮以运行代码,下方窗口将显示数据库模型的图形化可视效果

neo4j runcode

4 为 Neo4j 配置 GORM

首先,此指南基于 GORM 6.1.6.RELEASE 构建,因此,为了完成本指南,我们必须先将 Grails 配置为使用 GORM 6.1.6.RELEASE。

若要执行此操作,请将 gradle.properties 中的 gormVersion 属性设置为 6.1.6.RELEASE

gradle.properties
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

build.gradle
    compile "org.grails.plugins:neo4j"

现在,修改 grails-app/conf/application.yml 文件中已存在的 neo4j 配置块。将 Neo4j 连接设置配置为使用你之前设置的“movies”密码

grails-app/conf/application.yml
grails:
    neo4j:
        url: bolt://localhost
        username: "neo4j"
        password: "movies"

5 编写应用程序

如本指南先前所述,你将实现 Neo4j 示例影片应用程序。Neo4j 网站对该示例的描述如下所示

这是一个简单的单页面 Web 应用程序,它使用 Neo4j 的电影演示数据库(电影、演员、导演)作为数据集。所有应用程序中的同一个前端 Web 页面利用后端在不同编程语言和驱动程序中提供的 3 个 REST 端点。
— Neo4j.com

要实现的 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.groovyPerson 域类,并将内容修改为如下所示

grails-app/domain/neo4j/movies/Person.groovy
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.groovyMovie 域类,并将内容修改为如下所示

grails-app/domain/neo4j/movies/Movie.groovy
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
    }
}

一个Movietitletagline、发行year,并带有与CastMember关联。

CastMember领域类打算对PersonMovie之间的关系进行建模。为此,我们将使用grails.neo4j.Relationship特征,这样我们就能将域类映射到Neo4j关系(而不是节点)。

grails-app/domain/neo4j/movies/CastMember.groovy
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

控制器的初始内容应如下所示

grails-app/controllers/neo4j/movies/MovieController.groovy
@CompileStatic
class MovieController {
    static responseFormats = ['json', 'xml']
   ...
}

要将不同的端点映射到控制器,请将以下内容添加到grails-app/controllers/neo4j/movies/UrlMappings.groovy文件。

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映射到MovieControllershow操作。
2 /search URI映射到MovieControllersearch操作。
3 /graph URI映射到MovieControllergraph操作。

当然,这些控制器操作还没有实现。让我们从第一个需求开始。

5.2.1 按标题查找端点

要按标题查找Movie,我们首先创建一个名为MovieServiceGORM数据服务来封装数据访问逻辑和与Neo4j的交互。

你可以使用grails CLI或在grails-app/services/neo4j/movies目录中创建一个名称以Service结尾的类来通过你的文本编辑器或IDE创建。

$ grails create-service neo4j.movies.MovieService

将服务设为abstract,并向其中添加@Service注释来告诉GORM该服务应自动实现。

grails-app/services/neo4j/movies/MovieService.groovy
import grails.gorm.services.Service
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic

@SuppressWarnings(['UnusedVariable', 'SpaceAfterOpeningBrace', 'SpaceBeforeClosingBrace'])
@CompileStatic
@Service(Movie)
abstract class MovieService {
   ...
}

现在,向MovieService添加一个名为findabstract方法,该方法将title作为参数。

grails-app/services/neo4j/movies/MovieService.groovy
@Join('cast')
abstract Movie find(String title)

GORM将自动为你实现该方法,但请注意,我们使用@Join注释来表示我们要使用同一个查询获取cast关联。

现在让我们实现将调用此方法的控制器操作。首先在MovieController类中注入服务

grails-app/controllers/neo4j/movies/MovieController.groovy
MovieService movieService

这使用 Spring 注入依赖项并使服务实现可用。接下来添加一个名为show的操作,它调用find方法并以结果做出响应

grails-app/controllers/neo4j/movies/MovieController.groovy
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 视图,并使用以下内容填充

grails-app/views/movie/_movie.gson
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模板。

grails-app/views/movie/_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中,它实现搜索逻辑

grails-app/services/neo4j/movies/MovieService.groovy
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方法与自定义逻辑混合。

现在让我们实现将调用此方法的控制器操作

grails-app/controllers/neo4j/movies/MovieController.groovy
def search(String q) {
    respond movieService.search(q)
}

最后,search端点以不同于show端点的 JSON 格式返回结果。因此,我们创建一个grails-app/views/movie/search.gson视图来格式化 JSON 结果

grails-app/views/movie/search.gson
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 查询,只需定义名为 findMovieTitlesAndCastabstract 方法,该方法使用 @Cypher 注释

grails-app/services/neo4j/movies/MovieService.groovy
@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 预期使用的格式

grails-app/services/neo4j/movies/MovieService.groovy
@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 添加一个方法,以返回必需的数据

grails-app/controllers/neo4j/movies/MovieController.groovy
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-app/conf/application.yml
grails:
    resources:
        pattern: '/**'

修改 grails-app/controllers/neo4j/movies/UrlMappings.groovy 将应用程序根目录映射到此 index.html 文件。

grails-app/controllers/neo4j/movies/UrlMappings.groovy
'/'(uri: '/index.html')

完成这些操作后,即可运行应用程序!

6 运行应用程序

在启动应用程序之前,请确保 Neo4j 服务器正在运行,而且你已按照 开始 部分中的说明填充电影数据。

若要运行应用程序,请使用 ./gradlew bootRun 命令,这将会在端口 8080 上启动应用程序。

应用程序运行时请访问 http://localhost:8080,此时你应该会看到正确呈现的应用程序 UI,并附有图形可视化效果

neo4j ui