Grails GORM 数据服务
在本指南中,我们将学习如何在 Grails 应用程序中创建 GORM 数据服务。
作者:Nirav Assar、Sergio del Amo
Grails 版本 3.3.8
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!。
2 入门
在本指南中,我们将对 GORM 数据服务 进行深入研究。在 GORM 6.1 中引入的 GORM 数据服务免去了实现服务层逻辑的麻烦,因为它增加了使用 GORM 逻辑自动实现抽象类或接口的能力。这会减少需要编写的代码,优化编译时间,并且易于测试。
本指南将详细介绍如何使用示例 Grails 应用程序创建和使用 GORM 数据服务。本指南将始终专注于与持久性一致的应用程序的服务层。
2.1 您需要什么
要完成本指南,您需要以下条件
-
一些空闲时间
-
一个合适的文本编辑器或 IDE
-
已安装 JDK 1.7 或更高版本,并已适当地配置了
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-gorm-data-services.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是带有附加代码以助你快速上手的简单 Grails 应用程序。 -
complete
已完成示例。它是由按照指南的步骤操作并在initial
文件夹中应用这些更改后产生的。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-gorm-data-services/initial
并按照下一部分中的说明操作即可。
如果你 cd 到 grails-guides/grails-gorm-data-services/complete ,则可以直接转到已完成示例 |
3 编写应用程序
我们将使用域 Person
和 Address
编写一个简单的应用程序。这些域具有一对多的关系。我们将开发执行查询和写入操作的 GORM 数据服务。
3.1 域对象
创建 Person
和 Address
域类。它们具有一对多的关系。
package example.grails
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Person {
String name
Integer age
Set<Address> addresses (1)
static hasMany = [addresses: Address]
}
1 | 通常,你不需要在 hasMany 关联中指定默认类型 java.util.Set 。但是,我们将在本教程的后面部分在 JPA-QL 查询中查询关联,因此我们需要在此明确指定。 |
package example.grails
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Address {
String streetName
String city
String state
String country
static belongsTo = [person: Person]
}
4 为什么使用 GORM 数据服务
自动实现接口或抽象类减少了编写的代码量。此外,GORM 数据服务自动定义事务边界语义。例如,所有公共方法均标记为 @Transactional
(对于查询方法为只读)。
数据服务优势
总结一下这些优势
-
类型安全 - 数据服务方法签名在编译时进行检查,如果任何参数的类型与域类中的属性不匹配,则编译将失败。
-
测试 - 由于数据服务是接口,因此很容易对其进行模拟。
-
性能 - 生成的服务是静态编译的,并且与 Java 领域中的竞争技术不同的是,不会创建任何代理,因此运行时性能不会受到影响。此外,更新操作以高效的方式执行。
-
正确定义事务语义。用户通常无法正确定义事务语义。数据服务中的每个方法都包含在适当的事务中(在读取操作的情况下为只读事务)。
-
支持多租户。结合
@CurrentTenant
注释,GORM 数据服务可简化多租户的开发。
5 使用 GORM 数据服务
要编写数据服务,请创建一个接口,并使用 grails.gorm.services.Service
和已应用的域类对此接口添加注释。@Service
转换会查看接口的方法签名以推断应生成什么实现。它会查看返回类型和方法主体以实现功能。请查看数据服务约定以了解详细信息。
5.1 类似于动态查找器
如果您过去使用过 GORM,那么您可能已经使用 动态查找器 进行查询。
动态查找器看起来就像静态方法调用,但这些方法本身并不以任何形式存在于代码级别。相反,在运行时使用代码合成,根据给定类属性自动生成方法
使用 GORM 数据服务,您可以创建一个带有方法的接口,其表达能力与动态查找器相同,但没有缺点。例如,GORM 数据服务是静态编译的,因此您可以获得类型安全性。
要通过动态查找器按名称查找 Person
,您将使用 Person.findByName
。我们实现一个数据服务来实现相同查询。
使用 import grails.gorm.services.Service
创建一个带有注释的接口,并声明一个具有相同签名的方法。
package example.grails
import grails.gorm.services.Query
import grails.gorm.services.Service
@Service(Person) (1)
interface PersonDataService {
Person findByName(String name) (1)
void delete(Serializable id) (3)
}
1 | 使用您要使用的域类限定 @Service 。 |
2 | 在 findByName 方法中,“find”是词干,它告诉 GORM 它是一个查询(因此将使用只读事务),而 “Name” 与域属性匹配。 |
3 | delete 方法获取应删除的人员的 id 。它会自动包装在一个可写事务中。 |
5.2 单元测试
您可以按照如下方式为前一个数据服务编写单元测试
package example.grails
import grails.gorm.transactions.Transactional
import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.test.annotation.Rollback
import org.springframework.transaction.PlatformTransactionManager
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
@Transactional
class PersonDataServiceWithoutHibernateSpec extends Specification {
@Shared (2)
PersonDataService personDataService
@Shared (2)
@AutoCleanup (3)
HibernateDatastore hibernateDatastore
@Shared (2)
PlatformTransactionManager transactionManager
void setupSpec() {
hibernateDatastore = new HibernateDatastore(Person) (4)
transactionManager = hibernateDatastore.getTransactionManager() (5)
personDataService = this.hibernateDatastore.getService(PersonDataService)
}
@Rollback (6)
void "test find person by name"() {
given:
Person p = new Person(name: "Nirav", age: 39).save()
when:
Person person = personDataService.findByName("Nirav")
then:
person.name == "Nirav"
person.age == 39
}
}
1 | 该测试应扩展 spock.lang.Specification |
2 | Shared 注释用于指示 Spock,HibernateDatastore 、personDataService 和 transactionManager 属性在所有测试中共享。 |
3 | AutoCleanup 注释确保在完成执行所有测试时关闭 HibernateDatastore。 |
4 | 在 setupSpec 方法中,使用域类构造一个新的 HibernateDatastore,并将该域类作为构造函数的参数。 |
5 | 通常,您必须将测试执行逻辑包装在会话或事务中。您可以从 HibernateDatastore 获取 PlatformTransactionManager。 |
6 | 通常,您需要使用 Rollback 注释来注释功能方法,该注释用于回滚在每个测试中做出的任何更改。 |
幸运的是,Grails Hibernate 插件 包含一个实用类 grails.test.hibernate.HibernateSpec
,您可以从中扩展并简化前一个单元测试
package example.grails
import grails.test.hibernate.HibernateSpec
import spock.lang.Shared
class PersonDataServiceSpec extends HibernateSpec {
@Shared
PersonDataService personDataService
def setup() {
personDataService = hibernateDatastore.getService(PersonDataService)
}
void "test find person by name"() {
given:
new Person(name: "Nirav", age: 39).save(flush: true)
when:
Person person = personDataService.findByName("Nirav")
then:
person.name == "Nirav"
person.age == 39
cleanup:
personDataService.delete(person.id)
}
}
5.3 属性投影
向返回投影的 GORM 数据服务添加一个方法。这有好处,只需带回一列,而不是整个对象。有几种实现投影的方法。一种方法是使用约定 T find[域类][属性]
。
例如,对于域类 Person
的属性 age
,方法将是
Integer findPersonAge(String name)
添加到单元测试中以验证正确性。
void "test find persons age projection"() {
given:
Person person = new Person(name: "Nirav", age: 39).save(flush: true)
when:
Integer age = personDataService.findPersonAge("Nirav")
then:
age == 39
cleanup:
personDataService.delete(person.id)
}
5.4 保存操作
数据服务还可以执行编写操作。
Person save(String name, Integer age)
添加到单元测试中以验证正确性。
void "test save person"() {
when:
Person person = personDataService.save("Bob", 22)
then:
person.name == "Bob"
person.age == 22
personDataService.count() == old(personDataService.count()) + 1 (1)
cleanup:
personDataService.delete(person.id)
}
1 | 使用 Spock 的旧方法,我们可以获取在 when: 之前语句的值区块已执行。 |
运行所有测试以确保它们通过 → gradlew check 。 |
5.5 联接查询
数据服务还可以使用 grails.gorm.services.Join
注解来实现联接查询。Grails 关联默认情况下延迟加载。对于多对一关系,加载多方会导致 n + 1 问题,导致许多选择语句。这会严重阻碍性能。
本质上,我们可以使用 @Join
执行急切加载。将此概念应用于对 Person
和 Address
的查询。
@Join('addresses') (1)
Person findEagerly(String name)
在这一点上,我们已经使用接口定义了 Person GORM 数据服务。您可以改用抽象类。接口和使用一些自定义方法实现该接口的抽象类的组合是一种常见模式。如下所示重写 PersonDataService
package example.grails
import grails.gorm.services.Query
import grails.gorm.services.Service
import grails.gorm.services.Join
import grails.gorm.transactions.Transactional
import groovy.util.logging.Slf4j
interface IPersonDataService {
Person findByName(String name)
Integer findPersonAge(String name)
Number count()
@Join('addresses') (1)
Person findEagerly(String name)
void delete(Serializable id)
Person save(Person person)
Person save(String name, Integer age)
}
@Service(Person)
abstract class PersonDataService implements IPersonDataService {
@Transactional
Person saveWithListOfAddressesMap(String name, Integer age, List<Map<String, Object>> addresses) {
saveWithAddresses(name, age, addresses.collect { Map m ->
new Address(streetName: m.streetName as String,
city: m.city as String,
state: m.state as String,
country: m.country as String)
} as List<Address>)
}
@Transactional
Person saveWithAddresses(String name, Integer age, List<Address> addresses) {
Person person = new Person(name: name, age: age)
addresses.each { Address address ->
person.addToAddresses(address)
}
save(person)
}
}
创建集成测试
package example.grails
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
@Integration (1)
class PersonDataServiceIntSpec extends Specification {
PersonDataService personDataService (2)
void "test join eager load"() {
given:
Person p = personDataService.saveWithListOfAddressesMap('Nirav',39, [
[streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
[streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
[streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]
])
when:
Person person = personDataService.findEagerly("Nirav")
then:
person.name == "Nirav"
person.age == 39
when:
List<String> cities = person.addresses*.city
then:
cities.contains("Grapevine")
cities.contains("Austin")
cities.contains("Sewickley")
cleanup:
personDataService.delete(p.id)
}
}
1 | 将您的集成测试放在 src/integration-test 中,并使用 grails.testing.mixin.integration.Integration 对它们进行注解。 |
2 | 注入数据服务。 |
为了观察数据服务操作如何翻译成 SQL 语句,我们将调整日志记录配置。
在 logback.groovy
中,追加下一行行以获得正在执行的 SQL 查询的更详细输出
logger 'org.hibernate.SQL', TRACE, ['STDOUT'], false
执行集成测试 ./gradlew integrationTest --tests example.grails.PersonDataServiceIntSpec
前面的测试输出 SQL 日志语句,说明已发出联接。以下是示例。
2018-08-20 16:28:40.460 DEBUG --- [ Test worker] org.hibernate.SQL : insert into person (id, version, age, name) values (null, ?, ?, ?)
2018-08-20 16:28:40.472 DEBUG --- [ Test worker] org.hibernate.SQL : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.474 DEBUG --- [ Test worker] org.hibernate.SQL : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.474 DEBUG --- [ Test worker] org.hibernate.SQL : insert into address (id, version, person_id, street_name, city, country, state) values (null, ?, ?, ?, ?, ?, ?)
2018-08-20 16:28:40.533 DEBUG --- [ Test worker] org.hibernate.SQL : select this_.id as id1_1_1_, this_.version as version2_1_1_, this_.age as age3_1_1_, this_.name as name4_1_1_, addresses2_.person_id as person_i3_0_3_, addresses2_.id as id1_0_3_, addresses2_.id as id1_0_0_, addresses2_.version as version2_0_0_, addresses2_.person_id as person_i3_0_0_, addresses2_.street_name as street_n4_0_0_, addresses2_.city as city5_0_0_, addresses2_.country as country6_0_0_, addresses2_.state as state7_0_0_ from person this_ left outer join address addresses2_ on this_.id=addresses2_.person_id where this_.name=?
如您所见,只执行了一个选择查询。联接地址的查询。
您可以在测试报告 build/reports/tests/classes/example.grails.PersonDataServiceIntSpec.html
中看到这些语句的标准输出。
6 JPA-QL 查询
JPA-QL(JPA 查询语言)查询使用类似 SQL 的文本查询语言查询实体模型。查询使用实体、属性和关系表示。
GORM 数据服务通过 grails.gorm.services.Query
注解支持 JPA-QL 查询。
JPA-QL 查询允许更复杂的查询。尽管如此,它们仍然是静态编译的并且可以防止 SQL 注入攻击。
创建按国家/地区搜索人员的查询。
(1)
@Query("""\
select distinct ${p}
from ${Person p} join ${p.addresses} a
where a.country = $country
""") (2)
List<Person> findAllByCountry(String country) (3)
1 | @Query 用于定义 JPA-QL 查询。 |
2 | 您可以使用多行字符串定义 JPA-QL 查询,这增加了可读性。 |
3 | country 参数可以在 HQL 查询中使用。 |
创建集成测试
package example.grails
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
@Integration (1)
class PersonDataServiceIntSpec extends Specification {
PersonDataService personDataService (2)
void "test search persons by country"() {
given:
[
[name: 'Nirav', age: 39, addresses: [
[streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
[streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
[streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]]],
[name: 'Jeff', age: 50, addresses: [
[streetName: "888 Olive St", city: "St Louis", state: "MO", country: "USA"],
[streetName: "1515 MLK Blvd", city: "Austin", state: "TX", country: "USA"]]],
[name: 'Sergio', age: 35, addresses: [
[streetName: "19001 Calle Mayor", city: "Guadalajara", state: 'Castilla-La Mancha', country: "Spain"]]]
].each { Map<String, Object> m ->
personDataService.saveWithListOfAddressesMap(m.name as String,
m.age as Integer,
m.addresses as List<Map<String, Object>>)
}
when:
List<Person> usaPersons = personDataService.findAllByCountry("USA")
then:
usaPersons
usaPersons.size() == 2
usaPersons.find { it.name == "Nirav"}
usaPersons.find { it.name == "Jeff"}
when:
List<Person> spainPersons = personDataService.findAllByCountry("Spain")
then:
spainPersons.size() == 1
spainPersons.find { it.name == "Sergio"}
}
7 JPA-QL 投影
您还可以使用 JPA-QL 创建到 POGO(计划 Groovy 对象)的投影。
如果您有以下 POGO
package example.grails
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
@TupleConstructor (1)
@CompileStatic
class Country {
String name
}
1 | 使用 Groovy 的 TupleConstructor AST 转换来生成一个带一个参数的构造函数。 |
为Address
域类创建 GORM 数据服务
package example.grails
import grails.gorm.services.Query
import grails.gorm.services.Service
@Service(Address)
interface AddressDataService {
@Query("select new example.grails.Country(${a.country}) from ${Address a} group by ${a.country}") (1)
List<Country> findCountries()
}
1 | TupleConstructor 生成的构造函数用于 JPA-QL 查询。 |
你可以创建一个单元测试来验证行为
package example.grails
import grails.test.hibernate.HibernateSpec
import spock.lang.Shared
class AddressDataServiceSpec extends HibernateSpec {
@Shared
AddressDataService addressDataService
@Shared
PersonDataService personDataService
def setup() {
addressDataService = hibernateDatastore.getService(AddressDataService)
personDataService = hibernateDatastore.getService(PersonDataService)
}
def "projections to POGO work"() {
given:
List<Long> personIds = []
[
[name: 'Nirav', age: 39, addresses: [
[streetName: "101 Main St", city: "Grapevine", state: "TX", country: "USA"],
[streetName: "2929 Pearl St", city: "Austin", state: "TX", country: "USA"],
[streetName: "21 Sewickly Hills Dr", city: "Sewickley", state: "PA", country: "USA"]]],
[name: 'Jeff', age: 50, addresses: [
[streetName: "888 Olive St", city: "St Louis", state: "MO", country: "USA"],
[streetName: "1515 MLK Blvd", city: "Austin", state: "TX", country: "USA"]]],
[name: 'Sergio', age: 35, addresses: [
[streetName: "19001 Calle Mayor", city: "Guadalajara", state: 'Castilla-La Mancha', country: "Spain"]]]
].each { Map<String, Object> m ->
personIds << personDataService.saveWithListOfAddressesMap(m.name as String,
m.age as Integer,
m.addresses as List<Map<String, Object>>).id
}
when:
List<Country> countries = addressDataService.findCountries()
then:
countries
countries.size() == 2
countries.any { it.name == 'USA' }
countries.any { it.name == 'Spain' }
cleanup:
personIds.each {
personDataService.delete(it)
}
}
}
8 总结
允许 GORM 为你实现方法并管理事务,而你只需要定义接口。数据服务可以减少错误、性能问题,并且可测试。此外,它还有一个很高的炫酷系数!
阅读 GORM 数据服务 文档以了解更多信息。
观看 Greach Conf 2018 上 Graeme Rocher 的演讲