- name
- grails-developer
- description
- Comprehensive guide for Grails development, covering web applications, REST APIs, GORM, controllers, services, views, plugins, and testing with Spock and Geb
- license
- Apache-2.0
<!--
SPDX-License-Identifier: Apache-2.0
Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements; and to You under the Apache License, Version 2.0.
-->
## What I Do
- Provide detailed guidance for building Grails web applications and REST APIs.
- Assist with GORM for data modeling, controllers for request handling, services for business logic, and views (GSP, JSON, Markup).
- Support testing with Spock (unit/integration tests) and Geb for browser automation.
- Guide plugin usage and development, security implementation, and deployment strategies.
- Help with configuration, internationalization, async programming, and performance optimization.
## When to Use Me
Activate this skill when developing with Grails, including:
- Building CRUD applications, RESTful APIs, or full-stack web applications.
- Working with GORM domain classes, constraints, and relationships.
- Creating controllers, services, interceptors, or tag libraries.
- Writing Spock specifications or Geb functional tests.
- Configuring plugins, security, caching, or database migrations.
- Deploying Grails applications to containers or cloud platforms.
## Technology Stack
Grails is built on:
- **Spring Boot**: 4.1.x
- **Spring Framework**: 7.0.x
- **Groovy**: 5.1.x
- **Gradle**: 9.7 or later
- **Spock**: 2.4-groovy-5.0
- **Jakarta EE**: 10 (migrated from javax.*)
- **Micronaut**: Optional via `grails-micronaut` plugin
## Project Structure
```
myapp/
├── grails-app/
│ ├── conf/ # Configuration
│ │ ├── application.yml # Main config
│ │ ├── logback-spring.xml # Logging config
│ │ └── spring/resources.groovy # Spring bean definitions
│ ├── controllers/ # Request handlers
│ ├── domain/ # GORM domain classes
│ ├── i18n/ # Message bundles
│ ├── init/ # Bootstrap classes
│ ├── services/ # Business logic
│ ├── taglib/ # Custom GSP tags
│ ├── utils/ # Utility classes
│ └── views/ # GSP templates
├── src/
│ ├── main/groovy/ # Additional Groovy classes
│ ├── main/java/ # Java classes
│ ├── test/groovy/ # Test specifications
│ └── integration-test/groovy/ # Integration test specifications
├── build.gradle # Build configuration
└── gradle.properties # Project properties
```
## Domain Classes (GORM)
### Basic Domain Class
```groovy
class Book {
String title
String author
Date datePublished
Integer pages
static constraints = {
title blank: false, maxSize: 255
author blank: false
datePublished nullable: true
pages min: 1, nullable: true
}
static mapping = {
table 'books'
title column: 'book_title'
datePublished type: 'date'
}
}
```
### Relationships
```groovy
class Author {
String name
static hasMany = [books: Book]
static mapping = {
books cascade: 'all-delete-orphan'
}
}
class Book {
String title
static belongsTo = [author: Author]
}
// Many-to-Many
class Book {
static hasMany = [categories: Category]
static belongsTo = Category
}
class Category {
String name
static hasMany = [books: Book]
}
```
### GORM Queries
```groovy
// Dynamic finders
Book.findByTitle("Grails Guide")
Book.findAllByAuthorLike("%Smith%")
Book.findByTitleAndAuthor("Guide", "Smith")
Book.countByPagesGreaterThan(100)
// Where queries
Book.where {
title =~ "%Grails%" && pages > 100
}.list()
// Criteria queries
Book.withCriteria {
like('title', '%Grails%')
gt('pages', 100)
order('datePublished', 'desc')
maxResults(10)
}
// HQL queries
Book.executeQuery(
"from Book b where b.author.name = :name",
[name: 'Smith']
)
// Detached criteria (reusable)
def criteria = Book.where {
pages > 100
}
criteria.list()
criteria.count()
```
### Transactions
```groovy
// In services (default transactional)
@Transactional
class BookService {
def saveBook(Book book) {
book.save(failOnError: true)
}
}
// Manual transaction control
Book.withTransaction { status ->
def book = new Book(title: "Test")
book.save()
if (someCondition) {
status.setRollbackOnly()
}
}
```
## Controllers
### Basic Controller
```groovy
class BookController {
BookService bookService
static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE']
def index() {
respond Book.list(), model: [bookCount: Book.count()]
}
def show(Long id) {
respond Book.get(id)
}
def create() {
respond new Book(params)
}
def save(Book book) {
if (book.hasErrors()) {
respond book.errors, view: 'create'
return
}
bookService.save(book)
redirect action: 'show', id: book.id
}
def edit(Long id) {
respond Book.get(id)
}
def update(Book book) {
if (book.hasErrors()) {
respond book.errors, view: 'edit'
return
}
bookService.save(book)
redirect action: 'show', id: book.id
}
def delete(Long id) {
bookService.delete(id)
redirect action: 'index'
}
}
```
### REST Controller
```groovy
import grails.rest.RestfulController
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml']
BookController() {
super(Book)
}
// Override to customize
@Override
protected Book queryForResource(Serializable id) {
Book.where { id == id && active == true }.find()
}
}
```
### Command Objects
```groovy
class BookCommand implements Validateable {
String title
String author
Integer pages
static constraints = {
title blank: false
author blank: false
pages min: 1, nullable: true
}
}
class BookController {
def save(BookCommand cmd) {
if (cmd.hasErrors()) {
respond cmd.errors
return
}
// Process valid command
}
}
```
### Content Negotiation
```groovy
class BookController {
def show(Long id) {
def book = Book.get(id)
respond book // Auto-negotiates based on Accept header
}
}
// Or explicit
def show(Long id) {
def book = Book.get(id)
withFormat {
html { render view: 'show', model: [book: book] }
json { render book as JSON }
xml { render book as XML }
}
}
```
## Services
### Basic Service
```groovy
import grails.gorm.transactions.Transactional
@Transactional
class BookService {
def findByTitle(String title) {
Book.findByTitle(title)
}
def save(Book book) {
book.save(failOnError: true)
}
@Transactional(readOnly = true)
def list(Map params) {
Book.list(params)
}
def delete(Long id) {
Book.get(id)?.delete()
}
}
```
### Service with Events
```groovy
import grails.events.annotation.Publisher
import grails.events.annotation.Subscriber
class BookService {
@Publisher
def save(Book book) {
book.save()
// Automatically publishes 'book.saved' event
}
}
class NotificationService {
@Subscriber('book.saved')
def onBookSaved(Book book) {
// Handle event
log.info "Book saved: ${book.title}"
}
}
```
### Async Service
```groovy
import grails.async.Promise
import static grails.async.Promises.*
class BookService {
Promise<Book> findAsync(Long id) {
task {
Book.get(id)
}
}
def processBooks() {
def p1 = task { Book.findAllByActive(true) }
def p2 = task { Author.list() }
waitAll(p1, p2)
// Both complete
}
}
```
## Views
### GSP (Groovy Server Pages)
```html
<!-- grails-app/views/book/list.gsp -->
<!DOCTYPE html>
<html>
<head>
<title>Books</title>
<asset:stylesheet src="application.css"/>
</head>
<body>
<h1>Books (${bookCount})</h1>
<g:each in="${bookList}" var="book">
<div class="book">
<h2>${book.title}</h2>
<p>By ${book.author}</p>
<g:link action="show" id="${book.id}">View</g:link>
</div>
</g:each>
<g:if test="${bookList.empty}">
<p>No books found.</p>
</g:if>
<g:paginate total="${bookCount}"/>
<asset:javascript src="application.js"/>
</body>
</html>
```
### Common GSP Tags
```html
<!-- Links -->
<g:link controller="book" action="show" id="${book.id}">View</g:link>
<g:createLink action="list"/>
<!-- Forms -->
<g:form action="save">
<g:textField name="title" value="${book?.title}"/>
<g:textArea name="description" value="${book?.description}"/>
<g:select name="category" from="${categories}" optionKey="id" optionValue="name"/>
<g:checkBox name="active" value="${book?.active}"/>
<g:submitButton name="submit" value="Save"/>
</g:form>
<!-- Conditionals -->
<g:if test="${condition}">...</g:if>
<g:elseif test="${other}">...</g:elseif>
<g:else>...</g:else>
<!-- Iteration -->
<g:each in="${items}" var="item" status="i">
${i}: ${item.name}
</g:each>
<!-- Rendering -->
<g:render template="bookRow" model="[book: book]"/>
<g:render template="bookRow" collection="${books}" var="book"/>
<!-- Security (with Spring Security plugin) -->
<sec:ifLoggedIn>Welcome, ${sec.username()}</sec:ifLoggedIn>
<sec:ifNotLoggedIn><g:link controller="login">Login</g:link></sec:ifNotLoggedIn>
```
### JSON Views
```groovy
// grails-app/views/book/show.gson
import myapp.Book
model {
Book book
}
json {
id book.id
title book.title
author book.author
datePublished book.datePublished?.format('yyyy-MM-dd')
_links {
self {
href "/books/${book.id}"
}
}
}
```
### Layouts
```html
<!-- grails-app/views/layouts/main.gsp -->
<!DOCTYPE html>
<html>
<head>
<title><g:layoutTitle default="My App"/></title>
<asset:stylesheet src="application.css"/>
<g:layoutHead/>
</head>
<body>
<nav><!-- Navigation --></nav>
<main>
<g:layoutBody/>
</main>
<footer><!-- Footer --></footer>
<asset:javascript src="application.js"/>
</body>
</html>
<!-- Use in views -->
<html>
<head>
<meta name="layout" content="main"/>
<title>Books</title>
</head>
<body>
<!-- Page content -->
</body>
</html>
```
## Interceptors
```groovy
class AuthInterceptor {
AuthInterceptor() {
matchAll()
.excludes(controller: 'login')
GitHub에서 보기