GORM 逻辑删除
了解如何使用 GORM 逻辑删除插件
作者:Sergio del Amo
Grails 版本 4.0.1
1 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和交付!
2 开始
在本指南中,我们将介绍 GORM 逻辑删除 插件。
2.1 所需条件
要完成本指南,您需要满足以下条件
-
留出一些时间
-
正规文本编辑器或 IDE
-
安装 JDK 1.8 或更高版本,并适当地配置了
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 代码库
git 克隆 https://github.com/grails-guides/grails-logicaldelete.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用,它具有提供一定帮助的附加代码。 -
complete
一个完整的示例。这是完成指南中介绍的步骤并对initial
文件夹应用这些更改的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-logicaldelete/initial
并按照下一部分中的说明进行操作。
如果 cd 到 grails-guides/grails-logicaldelete/complete ,则可以转到已完成示例 |
3 应用程序概述
逻辑删除
仍然在数据库中但未包含在比较中或在搜索中检索到的记录的引用。
本指南介绍如何使用 GORM 逻辑删除 插件在 Grails 应用程序中创建逻辑删除功能。
我们将在书籍集合中创建删除和撤消功能。
已经为您将必要的资产(书籍封面图像)添加到 initial/grails-app/assets/images 文件夹中。 |
3.1 安装插件
向 build.gradle
中添加插件依赖性。
compile 'org.grails.plugins:gorm-logical-delete:2.0.0.M2'
3.2 域
添加 Book
域类
package demo
import gorm.logical.delete.LogicalDelete
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Book implements LogicalDelete<Book> { (1)
String image
String title
String author
String about
String href
static mapping = {
about type: 'text'
}
}
1 | 实现 gorm.logical.delete.LogicalDelete 特征,可以应用于 any 域类,以表明域类应该参与逻辑删除。该特征向域类中添加了一个名为 deleted 的布尔持久属性。当此属性值为 true 时,表明记录已逻辑删除,从默认情况下查询结果中排除。 |
3.3 控制
创建一个控制器,与服务合作,以检索书籍列表、书籍详细信息,并提供操作以删除和撤消书籍删除。
package demo
import groovy.transform.CompileStatic
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
@CompileStatic
class BookController {
static allowedMethods = [
index: 'GET',
show: 'GET',
delete: 'POST',
undoDelete: 'POST',
]
BookDataService bookDataService
MessageSource messageSource
def index() {
[
total: bookDataService.count(),
bookList: bookDataService.findAll(),
undoId: params.long('undoId')
]
}
def show(Long id) {
[bookInstance: bookDataService.findById(id)]
}
def delete(Long id) {
bookDataService.delete(id)
flash.message = messageSource.getMessage('book.delete.undo',
[id] as Object[],
'Book deleted',
LocaleContextHolder.locale
)
redirect(action: 'index', params: [undoId: id])
}
def undoDelete(Long id) {
bookDataService.unDelete(id)
flash.message = messageSource.getMessage('book.unDelete',
[] as Object[],
'Book restored',
LocaleContextHolder.locale
)
redirect(action: 'index')
}
}
3.4 服务
在 src/main/groovy
目录中创建一个 POGO BookImage
package demo
import groovy.transform.CompileStatic
@CompileStatic
class BookImage {
Long id
String image
}
为 Book
创建默认 CRUD 操作利用 GORM 数据服务。
package demo
import grails.gorm.services.Service
import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
interface IBookDataService {
Book save(String title, String author, String about, String href, String image)
Number count()
Book findById(Long id)
void delete(Long id) (1)
}
@Service(Book)
abstract class BookDataService implements IBookDataService {
@ReadOnly
List<BookImage> findAll() { (2)
Book.where {}.projections {
property('id')
property('image')
}.list().collect { new BookImage(id: it[0] as Long, image: it[1] as String) }
}
@Transactional
void unDelete(Long id) {
Book.withDeleted { (3)
Book book = Book.get(id)
book?.unDelete() (4)
}
}
}
1 | 如常删除一个域类。书籍的 deleted 属性设置为 true,但是书籍保留在持久存储中。 |
2 | 此 Where Query 不检索已逻辑删除的书籍。 |
3 | 对于想要逻辑删除的记录包含在查询结果中的情况,可以将查询包装在对 withDeleted 的调用中。 |
4 | 为了反转已删除字段使用 unDelete() 。 |
3.5 视图
创建一个 GSP 视图以列出书籍。对于每一本书籍,我们添加一个删除按钮。如果 undoId
存在,我们呈现一个 撤消
按钮。
<html>
<head>
<title>Groovy & Grails Books</title>
<meta name="layout" content="main" />
<style type="text/css">
.book {
width: 150px;
height: 300px;
float: left;
margin: 10px;
}
.book form {
text-align: center;
margin-bottom: 10px;
}
.message {
overflow: auto;
}
#undo {
margin: 10px;
float: right;
}
</style>
</head>
<body>
<div id="content" role="main">
<g:if test="${flash.message}">
<g:if test="${undoId}">
<g:form method="post" controller="book" action="undoDelete" id="${undoId}" class="message">
<g:submitButton name="submit" value="${g.message(code: 'book.undoDelete', default: 'Undo')}"/>
${flash.message}
</g:form>
</g:if>
<g:else>
<div class="message">${flash.message}</div>
</g:else>
</g:if>
<b><g:message code="books.total" default="Total number of Books"/><span id="totalNumberOfBooks">${total}</span></b>
<section class="row">
<g:each in="${bookList}" var="${book}">
<div class="book">
<g:form method="post" controller="book" action="delete" id="${book.id}">
<g:submitButton name="submit" value="${g.message(code: 'book.delete', default: 'Delete')}"/>
</g:form>
<g:link controller="book" id="${book.id}" action="show">
<asset:image src="${book.image}" width="150" />
</g:link>
</div>
</g:each>
</section>
</div>
</body>
</html>
3.6 BootStrap
在发展环境中运行该应用程序时,在 Bootstrap.groovy
中创建示例数据。
package demo
import grails.util.Environment
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
public final static List< Map<String, String> > GRAILS_BOOKS = [
[
title : 'Grails 3 - Step by Step',
author: 'Cristian Olaru',
href: 'https://grailsthreebook.com/',
about : 'Learn how a complete greenfield application can be implemented quickly and efficiently with Grails 3 using profiles and plugins. Use the sample application that accompanies the book as an example.',
image: 'grails_3_step_by_step.png',
],
[
title : 'Practical Grails 3',
author: ' Eric Helgeson',
href : 'https://www.grails3book.com/',
about : 'Learn the fundamental concepts behind building Grails applications with the first book dedicated to Grails 3. Real, up-to-date code examples are provided, so you can easily follow along.',
image: 'pratical-grails-3-book-cover.png',
],
[
title : 'Falando de Grails',
author: 'Henrique Lobo Weissmann',
href : 'http://www.casadocodigo.com.br/products/livro-grails',
about : 'This is the best reference on Grails 2.5 and 3.0 written in Portuguese. It's a great guide to the framework, dealing with details that many users tend to ignore.',
image: 'grails_weissmann.png',
],
[
title : 'Grails Goodness Notebook',
author: 'Hubert A. Klein Ikkink',
href : 'https://leanpub.com/grails-goodness-notebook',
about : 'Experience the Grails framework through code snippets. Discover (hidden) Grails features through code examples and short articles. The articles and code will get you started quickly and provide deeper insight into Grails.',
image: 'grailsgood.png',
],
[
title : 'The Definitive Guide to Grails 2',
author: 'Jeff Scott Brown and Graeme Rocher',
href : 'http://www.apress.com/9781430243779',
about : 'As the title states, this is the definitive reference on the Grails framework, authored by core members of the development team.',
image: 'grocher_jbrown_cover.jpg',
],
[
title : 'Grails in Action',
author: 'Glen Smith and Peter Ledbrook',
href : 'http://www.manning.com/gsmith2/',
about : 'The second edition of Grails in Action is a comprehensive introduction to Grails 2 focused on helping you become super-productive fast.',
image: 'gsmith2_cover150.jpg',
],
[
title : 'Grails 2: A Quick-Start Guide',
author: 'Dave Klein and Ben Klein',
href : 'http://www.amazon.com/gp/product/1937785777?tag=misa09-20',
about : 'This revised and updated edition shows you how to use Grails by iteratively building a unique, working application.',
image : 'bklein_cover.jpg',
],
[
title : 'Programming Grails',
author: 'Burt Beckwith',
href : 'http://shop.oreilly.com/product/0636920024750.do',
about : 'Dig deeper into Grails architecture and discover how this application framework works its magic.',
image: 'bbeckwith_cover.gif'
]
] as List< Map<String, String> >
public final static List< Map<String, String> > GROOVY_BOOKS = [
[
title: 'Making Java Groovy',
author: 'Ken Kousen',
href: 'http://www.manning.com/kousen/',
about: 'Make Java development easier by adding Groovy. Each chapter focuses on a task Java developers do, like building, testing, or working with databases or restful web services, and shows ways Groovy can make those tasks easier.',
image: 'Kousen-MJG.png',
],
[
title: 'Groovy in Action, 2nd Edition',
author: 'Dierk König, Guillaume Laforge, Paul King, Cédric Champeau, Hamlet D\'Arcy, Erik Pragt, and Jon Skeet',
href: 'http://www.manning.com/koenig2/',
about: 'This is the undisputed, definitive reference on the Groovy language, authored by core members of the development team.',
image: 'regina.png',
],
[
title: 'Groovy for Domain-Specific Languages',
author: 'Fergal Dearle',
href: 'http://www.packtpub.com/groovy-for-domain-specific-languages-dsl/book',
about: 'Learn how Groovy can help Java developers easily build domain-specific languages into their applications.',
image: 'gdsl.jpg',
],
[
title: 'Groovy 2 Cookbook',
author: 'Andrey Adamovitch, Luciano Fiandeso',
href: 'http://www.packtpub.com/groovy-2-cookbook/book',
about: 'This book contains more than 90 recipes that use the powerful features of Groovy 2 to develop solutions to everyday programming challenges.',
image: 'g2cook.jpg',
],
[
title: 'Programming Groovy 2',
author: 'Venkat Subramaniam',
href: 'http://pragprog.com/book/vslg2/programming-groovy-2',
about: 'This book helps experienced Java developers learn to use Groovy 2, from the basics of the language to its latest advances.',
image: 'vslg2.jpg'
],
] as List< Map<String, String> >
BookDataService bookDataService
def init = { servletContext ->
if ( Environment.current == Environment.DEVELOPMENT ) {
for (Map<String, String> bookInfo : (GRAILS_BOOKS + GROOVY_BOOKS)) {
bookDataService.save(bookInfo.title, bookInfo.author, bookInfo.about, bookInfo.href, bookInfo.image)
}
}
}
def destroy = {
}
}
3.7 URL 映射
更新我们的 UrlMappings.groovy
以便默认 URL 显示书籍网格。
package demo
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view: "/book'") (1)
"500"(view:'/error')
"404"(view:'/notFound')
}
}
1 | 更新后的默认 URL |
3.8 集成测试
创建一个 Geb 功能测试,验证撤消按钮工作
package demo
import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration
@Integration
class BooksUndoSpec extends GebSpec {
BookDataService bookDataService
def "verify a book can be deleted and deletion can be undone"() {
given:
Map bookInfo = [
title: 'Programming Groovy 2',
author: 'Venkat Subramaniam',
href: 'http://pragprog.com/book/vslg2/programming-groovy-2',
about: 'This book helps experienced Java developers learn to use Groovy 2, from the basics of the language to its latest advances.',
image: 'vslg2.jpg'
]
Book book = bookDataService.save(bookInfo.title, bookInfo.author, bookInfo.about, bookInfo.href, bookInfo.image)
when:
BooksPage booksPage = to BooksPage
then:
booksPage.count()
when:
booksPage.delete(0)
then:
bookDataService.count() == old(bookDataService.count()) - 1
booksPage.count() == (old(booksPage.count()) - 1)
when:
booksPage.undo()
then:
bookDataService.count() == old(bookDataService.count()) + 1
booksPage.count() == (old(booksPage.count()) + 1)
cleanup:
bookDataService.delete(book.id)
}
}
以前的测试使用 Geb 页面和模块来封装实现细节并关注正在验证的行为。
package demo
import geb.Module
class BookModule extends Module {
static content = {
deleteButton { $('input', type: 'submit') }
}
void delete() {
deleteButton.click()
}
}
package demo
import geb.Page
class BooksPage extends Page {
static url = '/book'
static at = { title == 'Groovy & Grails Books'}
static content = {
totalNumberOfBooks { $('#totalNumberOfBooks', 0) }
bookDivs { $('div.book') }
bookDiv { $('div.book', it).module(BookModule) }
undoButton(required: false) { $('input', type: 'submit', value: 'Undo') }
}
void delete(int i) {
bookDiv(i).delete()
}
void undo() {
undoButton.click()
}
Integer count() {
Integer.valueOf(totalNumberOfBooks.text())
}
}
4 结论
本指南的目的是向您展示逻辑删除插件的潜力,而不是撤销操作的完美实现。例如,在这个应用程序中,我们要保护 undoDelete 操作不被随机调用。 |
要了解更多,请阅读 介绍 Grails 3 GORM 逻辑删除插件博文和阅读 插件文档。