显示导航

使用 GORM 和 Hibernate 5 构建 REST 应用程序

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

作者:Graeme Rocher

Grails 版本 3.3.0

1 Grails 培训

Grails 培训 - 由创建并积极维护 Grails 框架的合作人士开发并提供!

2 开始

在本指南中,您将构建一个 Grails 应用程序,它使用 GORM for Hibernate 访问 SQL 数据库并在 RESTful 方式中生成 JSON 响应。

2.1 所需内容

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

  • 一些时间

  • 一个不错的文本编辑器或 IDE

  • 已安装的 JDK 1.7 或更高版本,并适当配置了 JAVA_HOME

2.2 如何完成本指南

要开始,请执行以下操作

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是带有附加代码的 Grails 应用程序,让你抢先开始。

  • complete 完成的示例。是完成指南中介绍的步骤并对initial 文件夹应用这些更改的结果。

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

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

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

如果你cd 进入grails-guides/rest-hibernate/complete,则可以直接进入完成示例

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

$ 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

build.gradle
dependencies {
    ...
    runtime 'mysql:mysql-connector-java:6.0.5'
}

然后修改在grails-app/conf/application.yml 中找到的配置。可以如下查看默认全局配置

grails-app/conf/application.yml
dataSource:
    pooled: true
    jmxExport: true
    driverClassName: org.h2.Driver
    username: sa
    password: ''

你会注意到存在特定于环境的配置

grails-app/conf/application.yml
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 带有三个内置环境developmenttestproduction,它们对应于应用程序将在其中运行的不同环境。

要针对 MySQL 修改此配置,你应该指定驱动程序类名称、用户名和密码。为了仅针对development 环境更改它,你可以在development 块下指定它

grails-app/conf/application.yml
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 类,你可以使用属性来表示数据库中表中的每一列。例如

grails-app/domain/hibernate/example/Product.groovy
package hibernate.example

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Product {

    String name
    Double price
}

在应用程序中定义的每个域类都将编译为实现GormEntity 特质。如果你愿意,你也可以明确定义这一点

grails-app/domain/hibernate/example/Product.groovy
import org.grails.datastore.gorm.*

class Product implements GormEntity<Product> {
    ..
}

3.3 应用域约束

如果您想要定义 GORM 域类中定义属性的 验证约束,您可以使用“约束”属性。

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

以上示例应用了两个约束。

  • “名称”属性受到约束,因此它不能是空字符串。

  • “价格”属性受到约束,因此它必须大于 0 且小于 1000,使用“范围”约束。

要验证这些约束是否符合我们的期望,您可以编写一个测试。如果您使用了“创建域类”,则将为您自动生成一个名为“src/test/groovy/hibernate/example/ProductSpec.groovy”的测试。否则,只需使用您的 IDE 或文本编辑器在“src/test/groovy”中创建一个适当命名的测试即可。

为了在单元测试中有效测试与 Hibernate 的交互,建议您使用“grails.test.hibernate.HibernateSpec”,您可以将其与 H2 内存数据库结合使用。

要做到这一点,首先修改“ProductSpec”类导入的部分,如下所示:

src/test/groovy/hibernate/example/ProductSpec.groovy
import grails.test.hibernate.HibernateSpec

然后确保“ProductSpec”继承“HibernateSpec”基本类。

src/test/groovy/hibernate/example/ProductSpec.groovy
@SuppressWarnings(['MethodName', 'DuplicateNumberLiteral'])
class ProductSpec extends HibernateSpec {

“HibernateSpec”超类将在事务中包装每个测试方法,该事务将在测试结束时回滚,以确保测试之间的清理。

准备好测试后,就可以编写测试。在 Grails 中编写测试使用 Spock Framework,这允许您定义更具可读性的测试,包括名称中有空格。

src/test/groovy/hibernate/example/ProductSpec.groovy
void 'test domain class validation'() {
    ...
}

首先,让我们为无效案例编写一个测试。您可以使用 Spock 的“when”和“then”块来使测试更具可读性。

src/test/groovy/hibernate/example/ProductSpec.groovy
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

以上示例将“名称”设为空值,将“价格”设为负值,然后尝试使用 save() 方法保存实例。

“then”块断言对象无效且未保存。

要为有效案例编写测试,请填充一些有效数据并尝试断言对象已保存。

src/test/groovy/hibernate/example/ProductSpec.groovy
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 轻松地创建控制器。

控制器的正文应如下所示:

grails-app/controllers/hibernate/example/ProductController.groovy
import groovy.transform.CompileStatic

@CompileStatic
class ProductController extends RestfulController {
    static responseFormats = ['json', 'xml']
    ProductController() {
        super(Product)
    }
}

RestfulController超级类会实现执行常见REST动词操作所需的所以操作,比如GETPOSTPUTDELETE。如果要覆盖或禁止某些动词,可以覆盖超类(例如用于DELETEdelete方法)的同等方法,以返回替代或禁止的响应。

3.5 将控制器映射到URI

默认情况下,控制器将在/product URI下公开。这是由默认的grails-app/conf/hibernate/example/UrlMappings.groovy类造成的

grails-app/controllers/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映射定义一个附加映射

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

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

3.6 实现搜索端点

如果希望向REST API添加一个附加端点,那么只需实现相应动作即可。

例如,假设想要使用/products/search URI和查询搜索产品。为此,第一步是在控制器中实现一个search动作

grails-app/controllers/hibernate/example/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查询,该查询使用了一个SQLlike查询。
3 respond方法用于使用结果列表进行响应
4 对于未指定查询的情况,使用空列表进行respond响应

在编写了动作之后,现在需要通过在grails-app/conf/hibernate/example/UrlMappings.groovy中定义相应的映射,来公开/products/search端点。

grails-app/controllers/hibernate/example/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 hibernate.example.ProductController

或者,也可以使用你最喜欢的文本编辑器或IDE创建src/test/groovy/hibernate/example/ProductControllerSpec.groovy文件。

如同以前实现的ProductSpec那样,它应该扩展HibernateSpec,但这一次TestFor定义应该针对ProductController

src/test/groovy/hibernate/example/ProductControllerSpec.groovy
@SuppressWarnings('MethodName')
class ProductControllerSpec extends HibernateSpec implements ControllerUnitTest<ProductController> {
    ...
}

此外,测试应该定义一个doWithSpring块,以启用JSON视图

src/test/groovy/hibernate/example/ProductControllerSpec.groovy
static doWithSpring = {
    jsonSmartViewResolver(JsonViewResolver)
}

doWithSpring 块用于为测试上下文注册额外的 Spring 配置(bean)。在本例中,我们希望测试控制器是否使用 JSON 视图作为响应,因此注册了一个 jsonSmartViewResolver bean。

接下来,为了准备测试,我们使用 setup 方法设置一些测试数据

src/test/groovy/hibernate/example/ProductControllerSpec.groovy
void setup() {
    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)
    )
}

setup 方法使用 saveAll 方法将多个领域类保存为测试数据。

最后,我们准备实现该测试,并可以通过执行 search 方法、传递适当参数和验证写入响应中的 JSON 来执行搜索

src/test/groovy/hibernate/example/ProductControllerSpec.groovy
    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'
    }
}

正如您从上面的示例中看到的,我们正在对 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 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 的内容如下所示

grails-app/views/product/_product.gson
import hibernate.example.Product

model {
    Product product
}

json g.render(product)

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

但是,json 特性是 Groovy StreamingJsonBuilder 的实例,您可使用它根据需要更改输出。

例如

grails-app/views/product/_product.gson
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 视图非常灵活,如需详细了解如何根据需要自定义输出,请参见 文档

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 更新数据

要更新数据,可以在 URI 中使用 PUT 请求和 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 应用程序!

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

5 你是否需要 Grails 帮助?

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

OCI 是 Grails 的家

认识团队