使用 MongoDB 构建 REST 应用程序
本指南将演示如何使用 Grails、GORM 和 MongoDB 构建 REST 应用程序
作者:格雷姆·罗切
Grails 版本 4.0.1
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发并交付!.
2 开始使用
在本指南中,你将构建一个使用 用于 MongoDB 的 GORM 访问 MongoDB 并以 RESTful 方式生成 JSON 响应的 Grails 应用程序。
2.1 你需要什么
要完成本指南,你需要以下内容
2.2 如何完成本指南
若要开始使用,请执行以下操作
-
下载并解压源代码
或
-
克隆 Git 代码库
git clone https://github.com/grails-guides/rest-mongodb.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码,以便为您提供先机。 -
complete
一个完整的示例。它是对指南中提出的步骤进行演练,并对initial
文件夹应用这些更改的结果。
要完成该指南,请转到 initial
文件夹
-
cd
进入grails-guides/rest-mongodb/initial
并按照下一部分中的说明进行操作。
如果您 cd 到 grails-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:
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的每个属性。例如
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Product {
}
应用程序中定义的每个域类都将被编译为实现MongoEntity 特性。如果您愿意,您可以显式定义此项
import grails.mongodb.*
class Product implements MongoEntity<Product> {
..
}
3.3 应用域约束
如果您希望对 GORM 域类中定义的属性定义验证约束,则可以使用 constraints
属性来执行此项操作
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”扩展。
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”中
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 创建控制器。
控制器的内容应如下所示
@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”类
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”映射
"/products"(resources:"product")
在这种情况下,控制器将映射到“/products”而不是“/product”。
3.6 实现搜索端点
如果您希望向 REST API 添加其他端点,只需执行对应的操作即可。
例如,假设您希望使用 /products/search
URI 和查询搜索产品。要执行此操作,第一步是在控制器中实现一个 search
操作
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
端点
'/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
文件。
编写下一个单元测试
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。该测试断言 response
的 json
属性的值。Grails 呈现的 JSON 结果如下所示:
[{"id":1,"name":"Apple","price":2.0}]
让我们来看看如何自定义此 JSON。
3.8 自定义 JSON 输出
Grails 使用 JSON 视图 表示渲染的 JSON 响应。这样做的理念是继续分离控制器逻辑与视图逻辑的理念。
如果找不到特定视图,则渲染的默认视图是 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.gson
和show.gson
视图共用,用于实际显示数据。
_product.gson
的内容在默认情况下如下所示。
import demo.Product
model {
Product product
}
json g.render(product)
对 g.render(product)
的调用将输出所有属性。
但是,json
属性是 Groovy StreamingJsonBuilder 的实例,你可以根据需要使用它来改变输出。
例如
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 应用程序!
请记住,你可以使用右侧的链接获取完成的示例的源代码。 |