显示导航

使用 MongoDB 构建 REST 应用程序

本指南将演示如何使用 Grails、GORM 和 MongoDB 构建 REST 应用程序

作者:格雷姆·罗切

Grails 版本 4.0.1

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发并交付!.

2 开始使用

在本指南中,你将构建一个使用 用于 MongoDB 的 GORM 访问 MongoDB 并以 RESTful 方式生成 JSON 响应的 Grails 应用程序

2.1 你需要什么

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

  • 一些时间

  • 一个合适的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本并适当地配置了 JAVA_HOME

  • 安装MongoDB 3.0.0 或更高版本

2.2 如何完成本指南

若要开始使用,请执行以下操作

Grails 指南代码库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码,以便为您提供先机。

  • complete 一个完整的示例。它是对指南中提出的步骤进行演练,并对 initial 文件夹应用这些更改的结果。

要完成该指南,请转到 initial 文件夹

  • cd 进入 grails-guides/rest-mongodb/initial

并按照下一部分中的说明进行操作。

如果您 cdgrails-guides/rest-mongodb/complete,则可以直接进入完成的示例

或者,如果您已经安装了 Grails 4.0.1,那么您可以使用以下命令在终端窗口中创建一个新应用程序

$ grails create-app mongodb-example --profile rest-api --features mongodb
$ cd mongodb-example

create-app 命令执行完后,Grails 会创建一个 mongodb-example 目录,该目录中的应用程序默认情况下配置为通过 REST 应用程序创建(使用 rest-api 配置文件),并且配置为使用 mongodb 功能。

创建完应用程序后,您需要启动 MongoDB。通常通过 MONGODB_HOME/bin/mongod 可执行文件完成此操作。

$ sudo ./mongod
2016-11-22T12:42:30.569+0100 I CONTROL  [initandlisten] db version v3.0.4
2016-11-22T12:42:30.569+0100 I CONTROL  [initandlisten] git version: 0481c958daeb2969800511e7475dc66986fa9ed5
2016-11-22T12:42:30.569+0100 I CONTROL  [initandlisten] build info: Darwin mci-osx108-11.build.10gen.cc 12.5.0 Darwin Kernel Version 12.5.0: Sun Sep 29 13:33:47 PDT 2013; root:xnu-2050.48.12~1/RELEASE_X86_64 x86_64 BOOST_LIB_VERSION=1_49
2016-11-22T12:42:30.569+0100 I CONTROL  [initandlisten] allocator: system
2016-11-22T12:42:30.569+0100 I CONTROL  [initandlisten] options: {}
2016-11-22T12:42:31.297+0100 I NETWORK  [initandlisten] waiting for connections on port 27017

如您所见,默认情况下 MongoDB 可用在端口 27017 上。

3 编写应用程序

现在,您已准备好开始编写该应用程序。

3.1 配置应用程序

启动 MongoDB 服务器后的第一步是确保应用程序配置正确。默认情况下,可在 grails-app/conf/application.yml 文件中找到 MongoDB 客户端的配置

grails-app/conf/application.yml
grails:
    mongodb:
        host: localhost
        port: 27017
        #username: ""
        #password: ""
        #databaseName: "mydatabase"

该配置已设置为使用默认值,但如果您的 MongoDB 服务器需要身份验证或任何自定义配置,则您可能想要更改配置

3.2 创建域类

每个MongoCollection 通过一个GORM 域类表示。

您可以使用项目的根目录中的 create-domain-class CLI 命令创建域类

$ ./grailsw create-domain-class Product
| Created grails-app/domain/demo/Product.groovy
| Created src/test/groovy/demo/ProductSpec.groovy

或者,您还可以使用您最喜欢的文本编辑器或 IDE 创建域类。

域类是一个简单的 Groovy 类,您可以使用属性来表示 MongoDB Document的每个属性。例如

grails-app/domain/demo/Product.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Product {
}

应用程序中定义的每个域类都将被编译为实现MongoEntity 特性。如果您愿意,您可以显式定义此项

grails-app/domain/demo/Product.groovy
import grails.mongodb.*

class Product implements MongoEntity<Product> {
    ..
}

3.3 应用域约束

如果您希望对 GORM 域类中定义的属性定义验证约束,则可以使用 constraints 属性来执行此项操作

grails-app/domain/demo/Product.groovy
static constraints = {
    name blank: false
    price range: 0.0..1000.00
}

上述示例应用了两个约束

  • name 属性受到约束,因此它不能是空白字符串。

  • 使用 range 约束对 price 属性进行了约束,因此它必须大于 0 而小于一千。

为了验证这些约束是否按预期工作,您可以编写一个测试。如果您使用“create-domain-class”,将会为您生成一个名为“src/test/groovy/demo/ProductSpec.groovy”的测试。否则,只需使用您的 IDE 或文本编辑器在“src/test/groovy”中创建一个适当命名的测试即可。

Grails 中的测试使用 Spock Framework 编写,包括在测试名称中使用空格的测试。

要使用 MongoDB 和 Spock 编写单元测试,您可以简单地从“grails.test.mongodb.MongoSpec”扩展。

src/test/groovy/demo/ProductSpec.groovy
import grails.test.mongodb.MongoSpec
import grails.testing.gorm.DomainUnitTest
import com.github.fakemongo.Fongo
import com.mongodb.MongoClient

class ProductSpec extends MongoSpec (3)
        implements DomainUnitTest<Product> {  (4)

    @Override
    MongoClient createMongoClient() {  (2)
        new Fongo(getClass().name).mongo
    }

    void 'a negative value is not a valid price'() { (1)
        given:
        domain.price = -2.0d

        expect:
        !domain.validate(['price'])
    }

    void 'a blank name is not save'() { (1)
        given:
        domain.name = ''

        expect:
        !domain.validate(['name'])
    }

    void 'A valid domain is saved'() {  (1)
        given:
        domain.name = 'Banana'
        domain.price = 2.15d

        when:
        domain.save()

        then:
        Product.count() == old(Product.count()) + 1
    }
}
1 Spock 允许您定义更具可读性的代码
2 覆盖“MongoSpec”基本类的“createMongoClient”方法。使用 Fongo 之类的东西;MongoDB 的内存中 Java 实现。
3 MongoSpec 是一个抽象类,它将在正在执行的规范的设置阶段初始化 GORM
4 使用 grails.testing.gorm.DomainUnitTest 特性对单个领域类进行单元测试。

将 fongo 依赖项添加到“build.gradle”中

build.gradle
    testCompile 'com.github.fakemongo:fongo:2.1.0'

现在要运行测试,您可以从终端窗口运行“./gradlew check”或在 IDE 中运行测试(如果 IDE 支持的话)

$ ./gradlew check

签出 如何测试领域类约束? 指南以了解更多有关测试约束的信息。

3.4 创建控制器

在定义了数据模型之后,现在开始编写 控制器

最快的方法是通过终端窗口使用“create-restful-controller”命令

$ ./grailsw create-restful-controller demo.Product
| Created grails-app/controllers/demo/ProductController.groovy

但是,您也可以使用您最喜欢的文本编辑器或 IDE 创建控制器。

控制器的内容应如下所示

grails-app/controllers/demo/ProductController.groovy
@CompileStatic
class ProductController extends RestfulController {
    static responseFormats = ['json', 'xml']
    ProductController() {
        super(Product)
    }
}

“RestfulController”超类将实现执行常见 REST 动词的所有必要操作,例如“GET”、“POST”、“PUT”和“DELETE”。如果您希望覆盖或禁止某些动词,那么可以覆盖父级中的等效方法(例如“DELETE”的“delete”方法)以返回替代或禁止的响应。

3.5 将控制器映射到 URI

默认情况下,控制器将在“/product”URI 下公开。这是因为默认的“grails-app/conf/demo/UrlMappings.groovy”类

grails-app/controllers/demo/UrlMappings.groovy
delete "/$controller/$id(.$format)?"(action: 'delete')
get "/$controller(.$format)?"(action: 'index')
get "/$controller/$id(.$format)?"(action: 'show')
post "/$controller(.$format)?"(action: 'save')
put "/$controller/$id(.$format)?"(action: 'update')
patch "/$controller/$id(.$format)?"(action: 'patch')

如您在上面看到的,每个 HTTP 动词都已映射,从而使用“$controller”语法从 URI 本身建立控制器名称。

如果您希望使用另一个名称或明确指定所用的 URI,则可以定义一个附加映射,使用“resources”映射

grails-app/controllers/demo/UrlMappings.groovy
"/products"(resources:"product")

在这种情况下,控制器将映射到“/products”而不是“/product”。

3.6 实现搜索端点

如果您希望向 REST API 添加其他端点,只需执行对应的操作即可。

例如,假设您希望使用 /products/search URI 和查询搜索产品。要执行此操作,第一步是在控制器中实现一个 search 操作

grails-app/controllers/demo/ProductController.groovy
def search(String q, Integer max ) { (1)
    if (q) {
        def query = Product.where { (2)
            name ==~ ~/$q/
        }
        respond query.list(max: Math.min( max ?: 10, 100)) (3)
    }
    else {
        respond([]) (4)
    }
}
1 定义一个名为 search 的操作,该操作采用一个名为 q 的查询参数和一个 max 参数
2 执行一个 where 查询,该查询使用正则表达式 (regex) 来搜索产品。
3 使用 respond 方法响应结果列表
4 对于未指定查询的情况,我们使用 respond 响应一个空列表

在编写操作之后,您需要在 grails-app/conf/demo/UrlMappings.groovy 中定义适当的映射来展示 /products/search 端点

grails-app/controllers/demo/UrlMappings.groovy
'/products'(resources: 'product') {
    collection {
        '/search'(controller: 'product', action: 'search')
    }
}

上述示例使用 collection 方法来将 URI 直接嵌套在 /products URI 下方(例如 /product/search),而不是将其嵌套在资源标识符下方(例如 /product/1/search)。

查阅 Grails 用户指南中的 映射 REST 资源 以获取更多有关如何控制 URI 如何映射到控制器的信息。

3.7 测试搜索端点

要为 search 操作编写一个单元测试,可以使用 CLI 的 create-unit-test 命令创建一个单元测试

$ ./grailsw create-unit-test demo.ProductController

或者,只需使用您最喜欢的文本编辑器或 IDE 创建 src/test/groovy/demo/ProductControllerSpec.groovy 文件。

编写下一个单元测试

src/test/groovy/demo/ProductControllerSpec.groovy
import com.github.fakemongo.Fongo
import com.mongodb.MongoClient
import grails.test.mongodb.MongoSpec
import grails.testing.web.controllers.ControllerUnitTest

class ProductControllerSpec extends MongoSpec (1)
        implements ControllerUnitTest<ProductController> {  (2)

    @Override
    MongoClient createMongoClient() {  (3)
        new Fongo(getClass().name).mongo
    }
    void setup() { (4)
        Product.saveAll(
            new Product(name: 'Apple', price: 2.0),
            new Product(name: 'Orange', price: 3.0),
            new Product(name: 'Banana', price: 1.0),
            new Product(name: 'Cake', price: 4.0)
        )
    }
    void 'test the search action finds results'() {
        when: 'A query is executed that finds results'
        controller.search('pp', 10)

        then: 'The response is correct'
        response.json.size() == 1
        response.json[0].name == 'Apple'
    }
}
1 MongoSpec 是一个抽象类,它将在正在执行的规范的设置阶段初始化 GORM
2 覆盖“MongoSpec”基本类的“createMongoClient”方法。使用 Fongo 之类的东西;MongoDB 的内存中 Java 实现。
3 使用 grails.testing.web.controllers.ControllerUnitTest 特征对控制器进行单元测试。
4 setup 方法使用 saveAll 方法保存多个域类用作测试数据。

该测试通过执行 search 方法执行搜索,传递适当的参数并验证写到响应的 JSON。该测试断言 responsejson 属性的值。Grails 呈现的 JSON 结果如下所示:

[{"id":1,"name":"Apple","price":2.0}]

让我们来看看如何自定义此 JSON。

3.8 自定义 JSON 输出

Grails 使用 JSON 视图 表示渲染的 JSON 响应。这样做的理念是继续分离控制器逻辑与视图逻辑的理念。

如果找不到特定视图,则渲染的默认视图是 grails-app/views/object/_object.gson

grails-app/views/object/_object.gson
import groovy.transform.*

@Field Object object

json g.render(object)

默认 _object.gson 视图仅使用 g.render(..) 方法自动生成对象的 JSON 表示形式。

如果您想要更改 JSON 的输出,则在 Grails 中完成此操作的方式是创建一个视图。您可以使用 CLI 的 generate-views 命令生成一个起始点

./grailsw generate-views demo.Product
| Rendered template index.gson to destination grails-app/views/product/index.gson
| Rendered template show.gson to destination grails-app/views/product/show.gson
| Rendered template _domain.gson to destination grails-app/views/product/_product.gson

如你所见,共生成了 3 个模板

  • grails-app/views/product/index.gson - 在控制器中通过 respond 方法呈现集合(通常是来自 GORM 的结果列表)时将使用此视图。

  • grails-app/views/product/show.gson - 通过 respond 方法在控制器中呈现单个 Product 实例时将渲染此视图。

  • grails-app/views/product/_product.gson - 此模板由 index.gsonshow.gson 视图共用,用于实际显示数据。

_product.gson 的内容在默认情况下如下所示。

grails-app/views/product/_product.gson
import demo.Product

model {
    Product product
}

json g.render(product)

g.render(product) 的调用将输出所有属性。

但是,json 属性是 Groovy StreamingJsonBuilder 的实例,你可以根据需要使用它来改变输出。

例如

grails-app/views/product/_product.gson
import demo.Product

model {
        Product product
}

Currency currency = locale?.country ? Currency.getInstance(locale) : Currency.getInstance('USD')
json {
    id product.id
    name product.name
    price "${currency.symbol}${product.price}"
}

在这个简单的示例中,我们根据用户的语言环境输出货币符号。现在,生成 JSON 的外观会类似于

[{id:1,"name":"Apple","price":"$2.0"}]
JSON 视图非常灵活,有关根据你的需要自定义输出的更多信息,请参阅文档

4 运行应用程序

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

4.1 使用 POST 创建数据

应用程序启动后,你可以使用你喜欢的 HTTP 客户端创建一个 Product 实例。在以下示例中,我们将使用 curl

要在终端窗口中提交 POST 请求,请使用以下内容

$ curl -i -H "Content-Type:application/json" -X POST localhost:8080/products -d '{"name":"Orange","price":2.0}'

生成的输出为

HTTP/1.1 201
X-Application-Context: application:development
Location: http://localhost:8080/products/2
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:46:40 GMT

{"name":"Orange","price":"$2.0"}

如你所见,返回一个 HTTP 201 状态代码。

4.2 使用 GET 读取数据

你可以使用 GET 请求读回所有 Product 实例

$ curl -i  localhost:8080/products

或者只按 id 读取单个实例

$ curl -i  localhost:8080/products/1

返回结果为

HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:50:58 GMT

{"id":1,"name":"Orange","price":"$2.0"}

4.3 使用 PUT 更新数据

要更新数据,你可以使用带有 ID 和 URI 中你要更改的数据的 PUT 请求

$ curl -i -H "Content-Type:application/json" -X PUT localhost:8080/products/1 -d '{"price":3.0}'

在这种情况下,生成的输出为

HTTP/1.1 200
X-Application-Context: application:development
Location: http://localhost:8080/products/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:52:14 GMT

{"id":1,"name":"Orange","price":"$3.0"}

如果你尝试使用无效值更新数据

$ curl -i -H "Content-Type:application/json" -X PUT localhost:8080/products/1 -d '{"price":-2.0}'

则会收到错误响应

HTTP/1.1 422
X-Application-Context: application:development
Location: http://localhost:8080/products/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:54:25 GMT

{"message":"Property [price] of class [class demo.Product] with value [-2] does not fall within the valid range from [0] to [1,000]","path":"","_links":{"self":{"href":"http://localhost:8080/products/1"}}}
错误响应由 grails-app/views/error.gson 视图呈现,消息从位于 grails-app/i18n 的消息包中获取

4.4 使用 DELETE 删除数据

要删除 Product,只需发送 DELETE 请求

$ curl -i -X DELETE localhost:8080/products/1

如果删除实例成功,输出将为

HTTP/1.1 204
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Date: Wed, 23 Nov 2016 08:57:27 GMT

恭喜!你已经使用 Grails、GORM 和 MongoDB 构建了你的第一个 REST 应用程序!

请记住,你可以使用右侧的链接获取完成的示例的源代码。

5 你在使用 Grails 的过程中是否需要帮助?

Object Computing, Inc. (OCI) 赞助了本指南的创建。各种咨询和支持服务均有提供。

OCI 是 Grails 的家园

结识团队