使用 GORM 和 Hibernate 5 构建 REST 应用程序
本指南将演示如何使用 Grails、GORM 和 Hibernate 5 来构建一个 REST 应用程序
作者: 格雷厄姆·罗切
Grails 版本 4.0.1
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供!
2 入门
在本指南中,你将构建一个 Grails 应用程序,该应用程序使用 用于 Hibernate 的 GORM 访问 SQL 数据库并在 RESTful 方式下生成 JSON 响应。
2.1 需要准备的工具
要完成本指南,你需要准备好以下工具
-
一定的时间
-
一个好的文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,并适当地配置了
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 仓库
git clone https://github.com/grails-guides/rest-hibernate.git
Grails 指南仓库包含两个文件夹
-
初始
初始项目。通常是一个带有额外代码的简单 Grails 应用,可帮助你抢先一步。 -
已完成
已完成示例。它是 1) 演练指南介绍的步骤并 2) 将这些更改应用于初始
文件夹的结果。
为完成指南,请转至 初始
文件夹
-
在
grails-guides/rest-hibernate/初始
中输入cd
然后按照后续章节中的说明进行操作。
如果你在 grails-guides/rest-hibernate/完整 中输入 cd ,可以直接进入 已完成示例 |
或者,如果你已安装 Grails 4.0.1,则可以通过在终端窗口中使用以下命令创建新应用程序
$ grails create-app hibernate-example --profile rest-api
$ cd hibernate-example
当 create-app
命令完成后,Grails 将创建一个 hibernate-example
目录,其中包含一个已配置为默认创建 REST 应用程序(使用 rest-api
配置文件)并已配置为使用 hibernate
功能的应用程序。
3 编写应用程序
现在开始编写应用程序。
3.1 配置应用程序
默认情况下,Grails 会创建一个使用 H2 内存 SQL 数据库 的应用程序,从而无需设置数据库即可开发 Grails。
但是,如果你确实要配置数据库,则可以通过将运行时依赖项添加到 build.gradle
中相应的 JDBC 驱动程序来实现。例如,对于 MySQL
dependencies {
...
runtime 'mysql:mysql-connector-java:6.0.5'
}
然后,修改在 grails-app/conf/application.yml
中找到的配置。可以在下方查看默认全局配置
dataSource:
pooled: true
jmxExport: true
driverClassName: org.h2.Driver
username: sa
password: ''
你会注意到有特定环境的配置
environments:
development:
dataSource:
dbCreate: create-drop
url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
test:
dataSource:
dbCreate: update
url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
Grails 带有三个内置环境 开发
、测试
和 生产
,分别对应于该应用程序将运行在其中不同的环境。
如要为 MySQL 更改此配置,你应该指定驱动程序类名称、用户名和密码。为了只为 开发
环境更改它,你可以在 开发
块下指定它
development:
dataSource:
driverClassName: com.mysql.jdbc.Driver
dbCreate: create-drop
url: jdbc:mysql://localhost/test
username: xxxxx
password: yyyyy
3.2 创建域类
SQL 数据库中的每个表都由 GORM 域类 表示。
要创建域类,你可以从项目根目录中的终端窗口使用 create-domain-class
CLI 命令
$ ./grailsw create-domain-class Product
| Created grails-app/domain/hibernate/example/Product.groovy
| Created src/test/groovy/hibernate/example/ProductSpec.groovy
此外,你还可以使用自己喜欢的文本编辑器或 IDE 创建域类。
域类是一个简单的 Groovy 类,你可以使用属性来表示数据库中每个表列。例如
package hibernate.example
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Product {
String name
Double price
}
应用程序中定义的每个域类都将编译为实现GormEntity特征。如果你愿意,你可以明确定义这一点
import org.grails.datastore.gorm.*
class Product implements GormEntity<Product> {
..
}
3.3 应用域约束
如果你希望在 GORM 域类中定义的属性上定义验证约束,可以使用`constraints`属性
static constraints = {
name blank:false
price range:0.0..1000.00
}
以上示例应用了两条约束
-
`name`属性受到约束,使其不能为一个空字符串。
-
`price`属性受到约束,使其必须大于 0 并小于一千,使用范围约束`range`。
若要验证这些约束是否符合我们的期望,你可以编写一个测试。如果你使用了`create-domain-class`,那么一个名为`src/test/groovy/hibernate/example/ProductSpec.groovy` 的文件已经为你生成了。否则,只需使用你的 IDE 或文本编辑器在`src/test/groovy`中创建一个适当命名的测试即可。
为了在单元测试中有效地测试与 Hibernate 的交互,建议你使用`grails.test.hibernate.HibernateSpec`,你可以将它与内存数据库 H2 一起使用。
要实现此功能,首先修改`ProductSpec`类的导入如下
import grails.test.hibernate.HibernateSpec
然后确保`ProductSpec`扩展基类`HibernateSpec`
@SuppressWarnings(['MethodName', 'DuplicateNumberLiteral'])
class ProductSpec extends HibernateSpec {
`HibernateSpec`超级类会用一个事务包装每个测试方法,该事务会在测试结束时回滚,从而确保在测试之间进行清理。
准备好测试后,你就可以编写一个测试了。Grails 中的测试使用Spock 框架编写,它允许你定义更易于阅读的测试,包括名称中有空格
void 'test domain class validation'() {
...
}
首先,让我们编写一个无效案例的测试。你可以使用 Spock 的`when`和`then`块来使测试更具可读性
when: 'A domain class is saved with invalid data'
Product product = new Product(name: '', price: -2.0d)
product.save()
then: 'There were errors and the data was not saved'
product.hasErrors()
product.errors.getFieldError('price')
product.errors.getFieldError('name')
Product.count() == 0
上述示例将`name`设置为一个空值,将`price`设置为一个负值,并尝试使用save()方法保存实例。
`then`块断言该对象无效且未保存。
若要编写一个有效案例的测试,请填充一些有效数据,并尝试断言该对象已保存
when: 'A valid domain is saved'
product.name = 'Banana'
product.price = 2.15d
product.save()
then: 'The product was saved successfully'
Product.count() == 1
Product.first().price == 2.15d
一切都完成了!现在,运行测试,你可以从终端窗口运行`./gradlew check`或在你的 IDE 内运行测试(如果 IDE 支持的话)
$ ./gradlew check
3.4 创建控制器
定义好数据模型后,现在是编写控制器的时候了。
最快的办法是通过终端窗口使用 `create-restful-controller` 命令
$ ./grailsw create-restful-controller hibernate.example.Product
| Created grails-app/controllers/hibernate/example/ProductController.groovy
但是,你也可以简单地使用你最喜欢的文本编辑器或 IDE 创建控制器。
控制器的内容应如下所示:
import groovy.transform.CompileStatic
@CompileStatic
class ProductController extends RestfulController {
static responseFormats = ['json', 'xml']
ProductController() {
super(Product)
}
ProductService productService
}
RestfulController
超类将实现执行常见 REST 动词(如 GET
、POST
、PUT
和 DELETE
)所需的所有操作。如果您希望重写或禁止某些动词,则可以重写该超类中的等效方法(例如,DELETE
的 delete
方法),以便返回备用或禁止的响应。
3.5 将控制器映射到 URI
默认情况下,控制器将显示在 /product
URI 下。这是由于默认 grails-app/conf/hibernate/example/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) {
respond productService.findByNameLike("%${q}%".toString(), [max: Math.min( max ?: 10, 100)]) (3)
}
else {
respond([]) (4)
}
}
1 | 定义了一个名为 search 的操作,它采用查询参数(称为 q )和 max 参数 |
2 | 执行一个 where 查询,它使用 SQL like 查询。 |
3 | respond 方法用于响应结果列表 |
4 | 对于未指定查询的情况,我们使用空列表来 respond |
在编写操作之后,您现在需要通过在 grails-app/conf/hibernate/example/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 hibernate.example.ProductController
或者,只需使用您喜欢的文本编辑器或 IDE 创建一个 src/test/groovy/hibernate/example/ProductControllerSpec.groovy
文件。
如前所述在 ProductSpec
中实现的,它应扩展 HibernateSpec
,但此时间定义TestFor
应该针对 ProductController
@SuppressWarnings('MethodName')
class ProductControllerSpec extends HibernateSpec implements ControllerUnitTest<ProductController> {
...
}
此外,测试还应定义一个 doWithSpring
数据块来启用 JSON 视图
static doWithSpring = {
jsonSmartViewResolver(JsonViewResolver)
}
doWithSpring
数据块用于为测试上下文注册附加 Spring 配置(bean)。在此示例中,我们要测试控制器响应 JSON 视图,因此注册了一个 jsonSmartViewResolver
bean。
最后,我们准备实现测试并可以通过执行 search
方法、传递适当的参数和验证写入响应的 JSON 来执行搜索
void 'test the search action finds results'() {
given:
controller.productService = Stub(ProductService) {
findByNameLike(_, _) >> [new Product(name: 'Apple', price: 2.0)]
}
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'
}
}
从上面的示例可以看出,我们正在断言 response
的 json
属性的值。Grails 渲染的结果 JSON 如下所示
[{"id":1,"name":"Apple","price":2.0}]
让我们来看看如何自定义此 JSON。
3.8 自定义 JSON 输出
Grails 使用 JSON Views 来表示呈现 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 hibernate.example.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 hibernate.example.Product
model {
Product product
}
json g.render(product)
对 g.render(product)
的调用输出所有属性。
但是,json
属性是 Groovy 的 StreamingJsonBuilder 的一个实例,你可以根据需要使用它来更改输出。
例如
import hibernate.example.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 视图非常灵活,有关如何根据你的需要自定义输出的更多信息,请参见 documentation。 |
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 更新数据
若要更新数据,您可以使用 PUT
请求,其中包含要更改的 URI 和数据中的 id
$ 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 hibernate.example.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 和 Hibernate 5 构建了您的第一个 REST 应用程序!
请记住,您可以使用右侧的链接获取已完成示例的源代码。 |