使用 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 如何完成本指南
要开始,请执行以下操作
-
下载并解压源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/rest-hibernate.git
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
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 带有三个内置环境development
、test
和production
,它们对应于应用程序将在其中运行的不同环境。
要针对 MySQL 修改此配置,你应该指定驱动程序类名称、用户名和密码。为了仅针对development
环境更改它,你可以在development
块下指定它
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 域类中定义属性的 验证约束,您可以使用“约束”属性。
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”类导入的部分,如下所示:
import grails.test.hibernate.HibernateSpec
然后确保“ProductSpec”继承“HibernateSpec”基本类。
@SuppressWarnings(['MethodName', 'DuplicateNumberLiteral'])
class ProductSpec extends HibernateSpec {
“HibernateSpec”超类将在事务中包装每个测试方法,该事务将在测试结束时回滚,以确保测试之间的清理。
准备好测试后,就可以编写测试。在 Grails 中编写测试使用 Spock Framework,这允许您定义更具可读性的测试,包括名称中有空格。
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
以上示例将“名称”设为空值,将“价格”设为负值,然后尝试使用 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)
}
}
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) {
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
端点。
'/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。
接下来,为了准备测试,我们使用 setup
方法设置一些测试数据
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 来执行搜索
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'
}
}
正如您从上面的示例中看到的,我们正在对 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 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 视图非常灵活,如需详细了解如何根据需要自定义输出,请参见 文档。 |
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 应用程序!
请记住,你可以使用右侧的链接获取完整示例的源代码。 |