Grails GORM 数据服务
在本指南中,我们将学习如何在 Grails 应用程序中创建 GORM 数据服务。
作者: Nirav Assar、Sergio del Amo
Grails 版本 5.0.0
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发并提供!。
2 入门
在本指南中,我们将深入了解 GORM 数据服务。在 GORM 6.1 中引入的 GORM 数据服务免除了实现服务层逻辑的工作,因为它增加了使用 GORM 逻辑自动实现抽象类或接口的能力。这样可以减少要编写的代码,优化编译时间并简化测试。
本指南将详细介绍如何使用示例 Grails 应用程序创建和使用 GORM 数据服务。本指南将重点关注与持久性一致的应用程序服务层。
2.1 您需要什么
要完成本指南,您需要以下内容
-
一些时间
-
一个不错的文本编辑器或 IDE
-
已安装 JDK 1.8 或更高版本,并且正确配置了
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压缩源文件
或
-
克隆 Git 代码库
git clone https://github.com/grails-guides/grails-gorm-data-services.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码以帮助您快速入门。 -
complete
已完成的示例。它是完成本指南列出的步骤并对initial
文件夹应用这些更改的结果。
要完成本指南,请转至 initial
文件夹
-
进入
grails-guides/grails-gorm-data-services/initial
的cd
并按照后续章节中的说明进行操作。
如果您进入 grails-guides/grails-gorm-data-services/complete 的 cd ,则可以直接转至已完成示例 |
3 编写应用程序
我们将编写一个包含 Person
和 Address
域的简单应用程序。这些域具有多对一关系。我们将开发执行查询和写入操作的 GORM Data Services。
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 Data Services
自动实现接口或抽象类可以减少编写的代码量。此外,GORM Data Services 自动定义事务边界语义。例如,所有公有方法都标记了 @Transactional
(并且标记了只读查询方法)。
Data Services 的优势
总结优势
-
类型安全 - Data Services 方法签名在编译时得到检查,如果任何参数的类型与域类中的属性不匹配,编译将失败。
-
测试 - 由于 Data Services 为接口,因此容易模拟它们。
-
性能 - 生成的服务是静态编译的,并且不同于 Java 领域中的竞争技术,生成的目的是创建代理,因而不会影响运行时性能。此外,更新操作的高效执行。
-
正确定义事务语义。用户经常无法正确定义事务语义。Data Service 中的每种方法都封装在适当的事务中(在读操作中是只读事务)。
-
支持多租户。结合使用
@CurrentTenant
注释,GORM 数据服务简化了多租户开发。
5 使用 GORM Data Services
要编写 Data Service,请创建一个接口并使用 grails.gorm.services.Service
和应用域类为其添加注释。@Service
转换查看接口的方法签名,以推断出应当生成什么实现。它会查看返回类型以及方法词干来实现功能。有关详细信息,请查看 Data 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 | 使用您想要使用的 domain 类装饰@Service 。 |
2 | 在方法findByName 中,词干是find ,它告诉 GORM 这是一条查询(因此将使用只读事务),并且Name 与 domain 属性匹配。 |
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 方法中,使用 domain 类构造一个新的 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[Domain Class][Property]
。
例如,对于域类 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 问题,从而导致许多 select 语句。这会严重妨碍性能。
实际上我们可以使用 @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=?
如你所见,只执行了一个 select 查询。一个联接地址的查询。
可以在测试报告 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 为您实现方法和管理事务,同时您只需定义界面。数据服务可以减少错误、性能问题,可测试。此外它具有很高的 COOL 系数!
阅读 GORM 数据服务 文档,了解更多信息。
观看 Graeme Rocher 在 Greach Conf 2018 上的演讲