显示导航

GORM 逻辑删除

了解如何使用 GORM 逻辑删除插件

作者:Sergio del Amo

Grails 版本 3.3.2

1 培训

Grails 培训 - 由创建并一直维护 Grails 框架的人员开发和提供!

2 入门

在本指南中,我们将向你介绍 GORM 逻辑删除 插件。

2.1 需要什么

若要完成本指南,你需要以下内容

  • 一些时间

  • 合适的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本,并适当地配置了 JAVA_HOME

2.2 如何完成指南

要开始,请执行以下操作

Grails 指南仓库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用,其中包含一些其他代码,可以帮助你快速入门。

  • complete 一个已完成示例。它是按照指南中提供的步骤操作并对 initial 文件夹应用这些更改的结果。

若要完成本指南,请转到 initial 文件夹

  • cdgrails-guides/grails-logicaldelete/initial

然后按照下一节中的说明进行操作。

如果你 cd 进入 grails-guides/grails-logicaldelete/complete,则可以直接转到已完成示例

3 应用程序概述

逻辑删除

对数据库中一个仍然存在但未包含在比较中或未在搜索中检索到的记录的引用。

在此指南中,我们使用 GORM Logical Delete 插件在 Grails 应用程序中创建逻辑删除。

我们将在书籍集合中创建删除按钮和撤消功能。

undo
我们已向 initial/grails-app/assets/images 文件夹添加了必要的资产(书籍封面图像)。

3.1 安装插件

build.gradle 中添加插件依赖项。

build.gradle
    compile 'org.grails.plugins:gorm-logical-delete:2.0.0.M2'

3.2 域

添加 Book 域类

grails-app/domain/demo/Book.groovy
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 特征,将其可应用于任何域类以指示域类应参与逻辑删除。此特征向域类中添加名为 deleted 的布尔持久化属性。当此属性的值为真时,指示已逻辑删除该记录,并因此默认情况下将其从查询结果中排除在外。

3.3 控制器

创建控制器,该控制器与服务协作以检索书籍列表、书籍详细信息,并提供删除和撤消书籍删除操作。

grails-app/controllers/demo/BookController.groovy
package demo

import groovy.transform.CompileStatic
import org.springframework.context.MessageSource

@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',
                request.locale
        )
        redirect(action: 'index', params: [undoId: id])
    }

    def undoDelete(Long id) {
        bookDataService.unDelete(id)
        flash.message = messageSource.getMessage('book.unDelete',
                [] as Object[],
                'Book restored',
                request.locale
        )
        redirect(action: 'index')
    }

}

3.4 服务

src/main/groovy 目录中创建 POGO BookImage

src/main/groovy/demo/BookImage.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BookImage {
    Long id
    String image
}

利用 GORM 数据服务Book 创建默认 CRUD 操作。

grails-app/services/demo/BookDataService.groovy
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 属性设置为真,但书籍保留在持久性存储中。
2 Where Query 不会检索已逻辑删除的书籍。
3 对于希望在查询结果中包含逻辑删除的记录的情况,查询可能会包含在对 withDeleted 的调用中。
4 要反转已删除字段,请使用 unDelete()

3.5 视图

创建 GSP 视图以列出书籍。对于每本书,我们添加一个删除按钮。如果 undoId 存在,我们显示“撤消”按钮。

grails-app/views/book/index.gsp
<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 中创建样本数据。

grails-app/init/demo/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&#39;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 显示书籍网格。

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }

        "/"(controller: "book") (1)
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}
1 更新的默认 URL

3.8 集成测试

创建一个 Geb 功能测试,用于验证撤消按钮是否正常工作

src/integration-test/groovy/demo/BooksUndoSpec.groovy
package demo

import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration
import spock.lang.IgnoreIf

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@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 页面和模块来封装实现细节并专注于正在验证的行为。

src/integration-test/groovy/demo/BookModule.groovy
package demo

import geb.Module

class BookModule extends Module {

    static content = {
        deleteButton { $('input', type: 'submit') }
    }

    void delete() {
        deleteButton.click()
    }
}
src/integration-test/groovy/demo/BooksPage.groovy
package demo

import geb.Page

class BooksPage extends Page {

    static url = '/'

    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 Logical Delete 插件 的博客文章,并查看 插件文档

5 您需要 Grails 的帮助吗?

Object Computing, Inc. (OCI) 赞助了本指南的创建。可提供各种咨询和支持服务。

OCI 是 Grails 的家园

结识团队