显示导航

GORM事件监听器

学习编写和测试GORM事件监听器

扎卡里·克莱因·塞尔吉奥·德尔阿莫

365bet地区版本 3.3.2

训练

365bet地区培训由创建并积极维护365bet地区框架的人们开发和交付

入门

在本指南中,您将学习如何编写和测试GORM事件监听器365bet地区GORM事件侦听器允许您编写在对象保存时更新或从数据库中删除时调用的方法,以执行自定义逻辑,例如创建日志条目,更新创建相关对象或修改持久对象中的属性。这些侦听器利用异步的功能,但是添加了一些有用的快捷方式,这些快捷方式特定于从您的域类捕获事件

您可能熟悉域类(例如)上可用的持久性事件处理程序, afterInsert, 更新之前等等事件监听器本质上允许您执行与您在这些域类方法中可能放置的逻辑相同的逻辑。但是事件监听器通常可以促进更好的代码组织,并且由于它们被添加到Spring上下文中,因此它们可以调用服务方法和其他Spring Bean,默认情况下不会自动连线且不参与依赖项注入的域类

您将需要什么

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

  • 花些时间在你手上

  • 体面的文本编辑器或IDE

  • 安装了JDK或更高版本JAVA首页适当配置

如何完成指南

要开始,请执行以下操作

要么

  • 克隆资料库
    去克隆

365bet地区指南存储库包含两个文件夹

  • 初始初始项目通常是一个简单的365bet地区应用程序,其中包含一些其他代码,可以帮助您快速入门

  • 完成一个完整的示例它是按照指南中介绍的步骤进行操作并将这些更改应用于文档的结果。初始

要完成指南,请转到初始

  • 光盘进入

您可以直接前往如果你光盘进入

编写申请

, , 书签审计下表描述了这些类的角色

表域类

角色

核心领域模型

审计

记录消息以记录给定的持久性事件

创建域类并进行编辑,如下所示

365bet地区应用程序域演示书groovy
演示进口 grails编译器365bet地区CompileStatic

365bet地区365bet地区CompileStatic
  {

    作者标题friendlyUrl整数页数序列号静态的约束serialNumber可为空: 真正friendlyUrl可为空: 真正标题可为空: 页数: 0序列号可为空: 真正
    }
}
grails应用程序域演示审计常规
演示进口 365bet地区grails编译器365bet地区CompileStatic

365bet地区CompileStatic
 审计 {

    事件longbookId静态的约束事件可为空: , 空白: bookId可为空: 
    }
}

数据服务

为了处理应用程序中的持久性逻辑,例如更新和删除书籍和标签,我们将创建几个GORM数据服务.

数据服务使我们能够集中化应用程序的数据访问和持久性功能,而不是直接调用动态查找器或更新域对象,我们可以在接口或抽象类中定义所需的查询和持久性操作类型,从而使GORM能够提供实施数据服务是事务性的,可以像其他任何Spring Bean一样注入到其他服务或控制器中。所有相同的GORM魔术都在起作用,例如在服务中,我们可以指定一种方法,例如图书findByTitleAndPagesGreaterThan字符串标题长页和GORM将提供与使用具有相同名称的动态查找器所获得的实现相同的实现

为什么使用数据服务GORM数据服务相对于动态查找器的主要优点是类型检查,如果没有以下内容,则上面显示的方法将无法编译字符串标题长页财产类和静态编译的能力此外,如果您选择在数据服务中优化查询以提高性能,则使用该方法的所有代码都可以从中受益,从而可以集中化数据服务中的常见查询和更新可能会带来一些体系结构上的好处无需更改即可跟踪更改,而无需您跟踪每个动态查找器或哪里分别查询和更新

在下面创建以下文件365bet地区应用程序服务演示:

触摸AuditDataService groovy触摸BookDataService groovy

如下所示编辑文件

grails应用程序服务演示AuditDataService groovy
演示进口 grails gorm services服务
进口 grails gorm服务在哪里
进口 常规转换CompileStatic

静态编译
服务审计接口AuditDataService审核保存事件longbookId计数清单找到所有地图args哪里bookId ID虚空deleteByBookIdlongID
365bet地区应用服务演示BookDataService groovy
演示进口 grails gorm services服务
进口 常规转换CompileStatic

静态编译
服务()
接口BookDataService保存标题作者整数页数清单<找到所有更新可序列化ID标题虚空删除可序列化ID

请注意,上面的两个数据服务是介面没有我们自己的任何实现这意味着GORM将为该类中的每个方法提供其默认实现。但是有时您可能需要在Data Service方法中提供一些自定义逻辑,为此,我们可以定义我们的Data Service作为一个抽象类并创建非抽象方法来处理我们的自定义代码抽象GORM仍将实施这些方法

异步侦听来自GORM的事件

我们的第一个听众将保存审计新的实例以及何时创建已更新和删除

365bet地区创建一个名为365bet地区的新服务AuditListenerService:

grails创建服务演示AuditListenerService
365bet地区编写事件侦听器不需要365bet地区服务。您可以将我们将要编写的相同方法放在Groovy类中src主界面365bet地区但是,侦听器确实需要连接到Spring上下文中,因此如果我们不使用365bet地区服务,则必须手动执行此操作。为方便起见,我们将使用365bet地区服务

编辑AuditListenerService如下所示

免费应用程序服务演示AuditListenerService groovy
演示进口 grails事件注释订户
进口 grails事件注释gorm侦听器
进口 常规转换CompileStatic
进口 groovy util日志记录Slf j
进口 组织grails数据存储区映射引擎事件AbstractPersistenceEvent
进口 组织grails数据存储区映射引擎事件PostDeleteEvent
进口 组织grails数据存储区映射引擎事件PostInsertEvent
进口 组织grails数据存储区映射引擎事件PostUpdateEvent

自我
静态编译
 AuditListenerServiceAuditDataService auditDataServicelongbookId AbstractPersistenceEvent事件如果事件entityObject实例  ) {
            返回 ((事件EntityObject ID(2)
        }
        空值
    }

    订户 (1)
    虚空afterInsert PostInsertEvent事件longbookId bookId事件如果bookId日志信息'图书保存后'auditDataService保存'图书已保存'bookId订户 (1)
    虚空afterUpdate PostUpdateEvent事件(3)
        longbookId bookId事件如果bookId日志信息"书籍更新后"auditDataService保存'图书已更新'bookId订户 (1)
    虚空afterDelete PostDeleteEvent事件longbookId bookId事件如果bookId日志信息'删除书籍后'auditDataService保存'书籍已删除'bookId
1 Subscriber批注和方法签名指示此方法对哪个事件感兴趣,例如,名为afterInsert具有类型的参数PostInsertEvent表示仅在保存对象后才调用此方法
2 我们可以通过来访问触发事件的域对象事件entityObject为了访问该对象的ID,我们将其强制转换为并获取我们用作的IDbookId我们的财产审计实例
3 同样,这里的方法签名指示将为类型的事件调用此方法PostUpdateEvent对象更新后
请记住,方法bookId将为所有类返回null因此,上一类创建审计的实例插入更新或删除操作

编写单元测试以验证auditDataService每当一个事件类型PostInsertEvent, PostUpdateEvent要么PostDeleteEvent收到了下一个内容,说明了GORM数据服务的另一个重大优势。它们创建易于测试的场景,成为易于模拟的界面。

src测试groovy演示AuditListenerServiceSpec groovy
演示进口 grails测试gorm DataTest
进口 免费测试服务ServiceUnitTest
进口 组织grails数据存储区映射引擎事件PostDeleteEvent
进口 组织grails数据存储区映射引擎事件PostInsertEvent
进口 组织grails数据存储区映射引擎事件PostUpdateEvent
进口 spock lang规格

 AuditListenerServiceSpec 延伸规格实施ServiceUnitTest(1)

    虚空setupSpec模拟域 (2)
    }

    虚空 "预订PostInsertEvent触发auditDataService保存"(){
        给定服务auditDataService模拟AuditDataService (标题: '实用365bet地区',
                作者: '埃里克·赫尔格森',
                页数: 1保存(3)PostInsertEvent事件PostInsertEvent数据存储书(4)

        什么时候服务afterInsert事件(5)

        然后:
        1服务auditDataService保存(6)
    }

    虚空 "预订PostUpdateEvent触发auditDataService保存"(){
        给定服务auditDataService模拟AuditDataService (标题: '实用365bet地区',
                作者: '埃里克·赫尔格森',
                页数: 1保存(3)PostUpdateEvent事件PostUpdateEvent数据存储书(4)

        什么时候服务afterUpdate事件(5)

        然后:
        1服务auditDataService保存(6)
    }

    虚空 "预订PostDeleteEvent触发auditDataService保存"(){
        给定服务auditDataService模拟AuditDataService (标题: '实用365bet地区',
                作者: '埃里克·赫尔格森',
                页数: 1保存(3)PostDeleteEvent事件PostDeleteEvent数据存储书(4)

        什么时候服务afterDelete事件(5)

        然后:
        1服务auditDataService保存(6)
    }
}
1 我们实施免费测试服务ServiceUnitTest单元测试服务的特征,以及数据测试特质,因为我们将使用GORM功能
2 数据测试特质提供了模拟域我们提供要在测试中使用的域类的方法
3 因为数据测试
4 我们现在可以使用建立一个实例PostInsertEvent请注意数据存储数据测试特征
5 我们现在可以将afterSave事件中传递的方法
6 我们现在可以断言审计实例通过保存auditDataService保存方法调用

Spock轮询条件它将反复评估一个或多个条件,直到满足条件或超时

src集成测试groovy演示AuditListenerServiceIntegrationSpec groovy
演示进口 grails测试mixin集成集成
进口 grails交易回滚
进口 spock lang规格
进口 spock util并发PollingConditions


 AuditListenerServiceIntegrationSpec 延伸规格BookDataService bookDataService AuditDataService auditDataService虚空 "保存书籍会导致保存审核实例"() {
        什么时候:
        定义条件轮询条件暂停: 30)

        图书bookDataService保存'实用365bet地区', '埃里克·赫尔格森', 1)

        然后断言auditDataService计数旧的auditDataService计数1
        }

        什么时候审核lastAudit这个去年审计然后lastAudit事件"图书已保存"lastAudit bookId图书ID什么时候: '一本书已更新'图书bookDataService更新图书ID'365bet地区')

        然后: ''最终情况断言auditDataService计数旧的auditDataService计数1
        }

        什么时候去年审计这个去年审计然后书名'ils子'lastAudit事件'图书已更新'lastAudit bookId图书ID什么时候: ''bookDataService删除书籍ID然后: ''最终情况断言auditDataService计数旧的auditDataService计数1
        }

        什么时候去年审计这个去年审计然后lastAudit事件'书籍已删除'lastAudit bookId图书ID清理auditDataService deleteByBookId图书ID审核lastAudit整型抵销数学最大auditDataService计数 整型) - 1), 0auditDataService findAll: 1, 抵销首先偏移

同步收听GORM中的事件

通常,您将需要访问甚至修改事件侦听器中的域对象的属性。例如,您可能希望在保存到数据库之前对用户的密码进行编码,或者针对禁止的单词字符的黑名单检查字符串属性的值GORM事件提供了实体访问允许您在触发事件的实体上获取和设置属性的对象

我们将看到如何使用实体访问在我们的下一个GORM事件监听器中

我们需要生成序列号并将其分配给我们的实例域类对于此样本,序列号将只是一个随机的全大写字符串,以书名的前几个字符为前缀。例如,书名为Groovy在行动可能具有序列号格劳·维克勒奇德克.

创建一个名为365bet地区的新服务SerialNumberGeneratorService:

grails创建服务演示SerialNumberGeneratorService
grails应用程序服务演示SerialNumberGeneratorService groovy
演示进口 常规转换CompileStatic
进口 org apache commons long RandomStringUtils

静态编译
 SerialNumberGeneratorService {

    生成书名randomString RandomStringUtils随机8, 真正, )
        titleChars"${书名}"采取4) (1)
        "${titleChars}-${randomString}"至大写
1 我们使用采取从标题安全地抓取第一个字符的方法采取不会抛出IndexOutOfBoundsException异常如果字符串长度短于所需范围

此外,我们希望为我们的书名生成友好的人类可读网址,而不是使用诸如http localhost书展我们要使用http本地主机书实用grails

365bet地区创建一个名为365bet地区的新服务FriendlyUrlService:

grails创建服务演示FriendlyUrlService
grails应用程序服务演示DemoFriendlyUrlService groovy
演示进口 常规转换CompileStatic

进口 Java文本规范化器

静态编译
 FriendlyUrlService {

    该方法将作为参数传递的文本转换为不带空格的文本html实体仅点点重音和突出字符z到Z并允许从Wordpress文件中借用wp包括格式化php函数清理带有破折号的标题http核心svn wordpress org主干wp包括格式化php
    sanitizeWithDashes文本如果文本返回 ''
        }

        保留转义的八位位组文字文字replaceAll'到fA F到fA F','---$1---'文字文字replaceAll'%',''文字文字replaceAll'到fA F到fA F','%$1')

        去除口音文字removeAccents文字小写文字文字到小写杀死实体文字文字replaceAll'&.+?;','')

        文字文字replaceAll'\\.','')

        删除除zA Z之外的所有字符文字文字replaceAll'对于Z', '')

        修剪文字文字修剪破折号文字文字replaceAll'\\s', '-')

        破折号文字文字replaceAll'-+', '-')

        它必须以字母或数字结尾,否则我们将删除最后一个字符
        如果文本1字符0isLetterOrDigit文字文字0..-2]

        返回文本将所有重音字符转换为ASCII字符如果没有重音字符,则仅返回给定的字符串
    私人的 removeAccents文本规范化器规范化文本规范化器形式NFD replaceAll"\\p InCombiningDiacriticalMarks", "")
    }
}

编辑为我们生成的单元测试规范FriendlyUrlService如下所示

src测试groovy演示FriendlyUrlServiceSpec groovy
演示进口 免费测试服务ServiceUnitTest
进口 spock lang规格
进口 spock lang展开

 FriendlyUrlServiceSpec 延伸规格实施ServiceUnitTest {

    展开
    定义 "期望标题的友好网址"(标题预期期望预期的服务sanitizeWithDashes标题哪里预期标题'实用365bet地区' | '实际情况'
    }
}

365bet地区现在,我们将创建一个侦听器来处理新书和更新书名创建一个名为365bet地区的服务TitleListenerService

grails创建服务演示TitleListenerService

每当我们插入新的序列号时,我们都希望异步填充序列号实例要同步捕获GORM事件,我们可以使用听众注解代替订户.

grails应用程序服务演示TitleListenerService groovy
演示进口 grails事件注释gorm侦听器
进口 常规转换CompileStatic
进口 组织grails数据存储区映射引擎事件AbstractPersistenceEvent
进口 组织grails数据存储区映射引擎事件PreInsertEvent
进口 组织grails数据存储区映射引擎事件PreUpdateEvent

静态编译
 TitleListenerServiceFriendlyUrlService friendlyUrlService SerialNumberGeneratorService serialNumberGeneratorService听众() (1)
    虚空onBookPreInsert PreInsertEvent事件populateSerialNumber事件populateFriendlyUrl事件听众() (1)
    虚空onBookPreUpdate PreUpdateEvent事件(2)
        事件entityObject如果书是脏的'标题') ) { (3)populateFriendlyUrl事件虚空populateSerialNumber AbstractPersistenceEvent事件标题事件entityAccess getProperty'标题')   (4)
        serialNumber serialNumberGeneratorService生成标题事件实体访问setProperty'序列号'序列号(5)
    }

    虚空populateFriendlyUrl AbstractPersistenceEvent事件标题事件entityAccess getProperty'标题')  
        friendlyUrl friendlyUrlService sanitizeWithDashes标题事件实体访问setProperty'friendlyUrl'friendlyUrl
1 听众批注会将此方法转换为GORM事件侦听器。当GORM触发持久性事件时,任何标记为听众并将使用适当的方法签名进行调用。批注采用值参数,该参数可以是单个域类,也可以是域类的列表,在这种情况下,仅针对其触发事件实例将触发此方法
2 onBookPreUpdate侦听器检查书名是否脏,意味着属性已更改。如果是,请更新friendlyUrl属性
3 是脏的方法允许我们检查持久对象上已更改的属性
4 我们检索标题域对象使用事件实体访问getProperty方法
5 设置序列号域对象的属性,我们使用事件实体访问setProperty方法
您可能想知道为什么不能简单地将事件entityObject作为一个然后设置序列号直接使用属性此处避免这种方法的原因是,直接分配本身将触发另一个事件,从而可能导致同一事件侦听器多次触发。实体访问对象,我们所做的任何更改都将与当前持久性会话同步,并将与原始对象同时保存

再次使用GORM数据服务可简化单元测试。在下一个测试中,我们对GORM数据服务进行存根,并确认已填充序列号

src测试groovy演示TitleListenerServiceSpec groovy
演示进口 grails测试gorm DataTest
进口 免费测试服务ServiceUnitTest
进口 组织grails数据存储区映射引擎事件PreInsertEvent
进口 org springframework测试注释回滚
进口 spock lang规格

 TitleListenerServiceSpec 延伸规格实施ServiceUnitTest定义 setupSpec模拟域闭包doWithSpring(1)friendlyUrlService FriendlyUrlService回滚
    虚空 "测试序列号生成"() {
        给定:
         (标题: '实用365bet地区', 作者: '埃里克·赫尔格森', 页数: 100)

        什么时候服务serialNumberGeneratorService存根SerialNumberGeneratorService生成 ) >> 'XXXX'onBookPreInsert服务PreInsertEvent数据存储书然后图书序列号'XXXX'预定friendlyUrl'实际情况'
    }
}
1 要在上下文中提供或替换bean,可以在测试中覆盖doWithSpring方法。

整合测试

接下来,我们创建一个集成测试以验证friendlyUrl属性在以下时间刷新标题属性已更新

src集成测试groovy演示TitleListenerServiceIntegrationSpec groovy
演示进口 grails测试mixin集成集成
进口 spock lang规格


 TitleListenerServiceIntegrationSpec 延伸规格BookDataService bookDataService AuditDataService auditDataService定义 "保存书籍会自动生成一个序列号"() {
        什么时候:
        图书bookDataService保存'实用365bet地区', '埃里克·赫尔格森', 100)
        serialNumber图书serialNumberfriendlyUrl图书friendlyUrl然后书书hasErrors serialNumber friendlyUrl'实际情况'

        什么时候: '更新书名'图书bookDataService更新图书ID'ils子')

        然后: '序列号保持不变'serialNumber图书serialNumber: '友好的网址更改'友好的网址电子书友好的网址电子书友好的网址''

        清理auditDataService deleteByBookId图书ID bookDataService删除图书ID

高级单元测试

可以说,对事件的正确处理的测试应该是整合测试如前所示,但是您可以创建一个等效的单元测试。我们可以将应用程序中需要验证的部分仅连接在一起,而无需进行完整的集成测试,而无需进行完整的集成测试启动应用程序以执行测试

创建文件TitleListenerService365bet地区UnitSpecsrc测试groovy演示并编辑内容,如下所示

365bet地区src测试groovy演示TitleListenerService365bet地区UnitSpec groovy
演示进口 grails gorm事务回滚
进口 组织grails orm休眠HibernateDatastore
进口 365bet地区组织grails测试365bet地区UnitTest
进口 org springframework交易PlatformTransactionManager
进口 只是自动清理
进口 spock lang共享
进口 spock lang规格

 365bet地区TitleListenerService365bet地区UnitSpec 延伸规格实施365bet地区365bet地区UnitTest(1)

    共享
    自动清理HibernateDatastore hibernateDatastore(2)

    共享PlatformTransactionManager transactionManager虚空setupSpec hibernateDatastore applicationContext getBean HibernateDatastore(2)transactionManager hibernateDatastore getTransactionManager覆写闭包doWithSpring(3)friendlyUrlService FriendlyUrlService serialNumberGeneratorService SerialNumberGeneratorService titleListenerService TitleListenerService friendlyUrlService ref'friendlyUrlService'serialNumberGeneratorService引用'serialNumberGeneratorService'数据存储HibernateDatastore])
        }
    }

    回滚
    定义 "保存书籍后填充serialNumber和friendyUrl"() { (4)

        什么时候:
         (标题: '实用365bet地区', 作者: '埃里克·赫尔格森', 页数: 100图书保存齐平: 真正)

        然后本书hasErrors什么时候findByTitle'实用365bet地区')

        serialNumber图书serialNumberfriendlyUrl图书friendlyUrl然后serialNumber friendlyUrl'实际情况'

        什么时候: '更新书名'书名'ils子'图书保存齐平: 真正)

        然后本书hasErrors什么时候findByTitle'ils子')

        然后: '序列号保持不变'serialNumber图书serialNumber: '友好的网址更改'友好的网址电子书友好的网址电子书友好的网址''
    }
}
1 因为我们将自己连接GORM Spring上下文和事件系统,所以我们将简单地实现基本365bet地区365bet地区UnitTest特质而不是更具体ServiceUnitTest特征
2 我们声明一个共享自动清理我们数据存储区的属性setupSpec方法我们获得HibernateDatastore包含在applicationContext由...提供365bet地区UnitTest特质transactionManager我们在规范中定义的共享属性
3 在我们覆盖doWithSpring我们为我们创建Spring bean的方法friendlyUrlService, serialNumberGeneratorService, titleListenerService以及我们的数据存储使用我们测试所需的域类列表实例化后者。这将在测试的Spring上下文中配置我们的服务和GORM数据存储
4 我们的测试现在微不足道,几乎与以前的集成测试相同

运行测试

运行测试

grailsw grails测试应用程序grails打开测试报告

要么

gradlew检查打开构建报告测试索引html

结论

365bet地区事件侦听器是一个强大的概念,GORM的实现将使您能够围绕持久性事件编写智能业务逻辑,而不会使您的域类变得混乱。此外,最新的365bet地区版本中改进的测试支持使为事件编写简单而全面的测试比以往更加容易听众快乐编码

365bet地区帮助365bet地区

OCI赞助了本指南的创建OCI提供了几种365bet地区服务:

免费咨询

OCI 365bet地区团队包括365bet地区联合创始人Jeff Scott Brown和Graeme Rocher检查我们的365bet地区课程并向发展和维护365bet地区的工程师学习

Grails OCI团队