Show Navigation

GORM Logical delete

Learn how to use GORM Logical delete plugin

Authors: Sergio del Amo

Grails Version: 4.0.1

1 Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide, we will introduce you GORM Logical Delete plugin.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-logicaldelete/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-logicaldelete/complete

3 Application Overview

Logical Deletion:

Reference to a record that remains in the database, but is not included in comparisons or retrieved in searches.

In this guide, we use the GORM Logical Delete plugin to create Logical deletion into a Grails application.

We will create a delete button and undo functionality in a book collection.

undo
We have already added the necessary assets (book cover images) to the initial/grails-app/assets/images folder for you.

3.1 Install Plugin

Add plugin dependency to build.gradle.

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

3.2 Domain

Add a Book domain class:

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 Implement gorm.logical.delete.LogicalDelete trait, which can applied to any domain class to indicate that the domain class should participate in logical deletes. The trait adds a boolean persistent property named deleted to the domain class. When this property has a value of true, it indicates that the record has been logically deleted and as such will be excluded from query results by default.

3.3 Controller

Create a controller which in collaboration with a service retrieves a list of books, a book’s detail and provide actions to delete and undo a book deletion.

grails-app/controllers/demo/BookController.groovy
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 Services

Create a POGO BookImage in the src/main/groovy directory

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

import groovy.transform.CompileStatic

@CompileStatic
class BookImage {
    Long id
    String image
}

Create default CRUD actions for Book leveraging GORM data services.

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 Delete a domain class as you normally would. The book’s deleted property is set to true, but book stays in the persistence storage.
2 This Where Query does not retrieve books which have been logically deleted.
3 For cases where you would like logically deleted records to be included in query results, queries may be wrapped in a call to withDeleted.
4 In order to reverse the deleted field use unDelete().

3.5 Views

Create a GSP view to list books. For each book, we add a delete button. If undoId is present, we present an Undo button.

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

Create sample data in Bootstrap.groovy when we run the app in the development environment.

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 URLMappings

Update our UrlMappings.groovy so that the default url displays the books grid.

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

class UrlMappings {

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

        "/"(view: "/book'") (1)
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}
1 Updated default URL

3.8 Integration Test

Create a Geb functional test which verifies the undo button works:

src/integration-test/groovy/demo/BooksUndoSpec.groovy
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)
    }
}

The previous test uses a Geb Page and Module to encapsulate implementation details and focus in behaviour being verified.

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 = '/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 Conclusion

The goal of this guide is to show you the potential of the logical delete plugin, not a perfect implementation of undo functionality. For example, in this app we should protect the undoDelete action to be randomly invoked.

To learn more, read more Introducing the Grails 3 GORM Logical Delete plugin's blog post and read the plugin docs.

5 Do you need help with Grails?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team