Перейти к основному содержимому

Работа с базами данных из Groovy

Разработчику Архитектору

Работа с базами данных из Groovy

На JVM доступ к данным обычно идёт через JDBC или ORM. В экосистеме Groovy чаще всего встречаются три уровня:

  1. GORM — Active Record поверх Hibernate (Grails, Spring Boot).
  2. groovy.sql.Sql — тонкая обёртка над JDBC для скриптов и миграций.
  3. Чистый Java-стек — Spring Data JPA, MyBatis, jOOQ из Groovy-кода без GORM.

Ниже — GORM как главный "грувовский" путь и кратко альтернативы.

Play ITЗагрузка интерактивного демо…

Интерактив выше показывает поток запроса от доменного класса через GORM и JDBC к PostgreSQL, CRUD-лабораторию с живой таблицей books, сравнение GORM · groovy.sql.Sql · JDBC и жизненный цикл save() / транзакций. Пройдите сценарии "Book.get", "save", "findAllBy*" и "hasMany" — затем читайте разделы ниже с тем же примерами в коде.


GORM — идея и стек

GORM (Grails Object Relational Mapping) реализует паттерн Active Record: класс домена знает, как сохранить себя в БД. Под капотом — Hibernate, пулы соединений и транзакции — как в типичном Spring-приложении.

Поддерживаются PostgreSQL, MySQL, H2, Oracle и др. GORM работает в Grails "из коробки" и в Spring Boot при подключении зависимостей gorm-*.


Доменный класс

Поля класса маппятся на столбцы; ограничения — в блоке constraints:

class Book {
String title
String author
BigDecimal price
Date releaseDate

static constraints = {
title blank: false, maxSize: 100
author nullable: true
price min: 0.0
}
}

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с class Book { и задает контекст выполнения.
  • Конструкция class описывает структуру объекта: поля хранят состояние, а методы инкапсулируют поведение.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Таблица book и столбцы создаются миграциями или автогенерацией схемы (в учебных проектах — осторожно с dbCreate в продакшене).


CRUD

Код ITЗагрузка примера кода…

Разбор:

  • Create: new Book(...) — именованные аргументы Groovy заполняют поля; 49.99G — литерал BigDecimal (суффикс G).
  • save(failOnError: true) — сохранение в БД через GORM; при ошибке валидации бросится исключение, а не тихий null.
  • Read: Book.get(1L) — загрузка по первичному ключу; findAllByPriceLessThan — динамический finder по имени поля и условию.
  • Update: изменение book.price в памяти, затем save() синхронизирует строку в таблице.
  • Delete: book.delete() удаляет запись; в транзакции откат возможен, если обернуть в withTransaction.

Проверка ошибок валидации без failOnError:

def book = new Book(title: '', price: -1G)
if (!book.save()) {
book.errors.allErrors.each { println it }
}

Разбор:

  • Пустой title и отрицательная price нарушают static constraints доменного класса.
  • save() без флага возвращает false при ошибке, объект остаётся в памяти с errors.
  • book.errors.allErrors — список сообщений валидации GORM/Hibernate.
  • each { println it } печатает каждую ошибку — удобно в скриптах и при отладке API.
  • В REST-сервисе те же ошибки обычно маппят в ответ 400 с полями формы.

save() возвращает объект или null при ошибке валидации — в продакшене проверяйте hasErrors() или используйте save(failOnError: true).


Динамические методы поиска

GORM генерирует методы по имени поля:

Book.findByTitle('Groovy in Action')
Book.findAllByAuthor('Dierk König')
Book.countByAuthor('Dierk König')
Book.findAllByReleaseDateBetween(startDate, endDate)

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с Book.findByTitle('Groovy in Action') и задает контекст выполнения.
  • Ключевые операторы и выражения (def, литералы, вызовы) формируют данные, с которыми работает остальная часть примера.
  • Вызовы функций или команд выполняют полезное действие: чтение данных, вычисление результата или запуск задачи сборки.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Сложные запросы выносят в criteria, HQL или where-запросы (GORM 6+).


Связи

class Author {
String name
static hasMany = [books: Book]
}

class Book {
String title
static belongsTo = [author: Author]
}

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с class Author { и задает контекст выполнения.
  • Конструкция class описывает структуру объекта: поля хранят состояние, а методы инкапсулируют поведение.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.
def author = new Author(name: 'Dierk König').save()
author.addToBooks(new Book(title: 'Groovy in Action')).save()
author.books.each { println it.title }

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с def author = new Author(name: 'Dierk König').save() и задает контекст выполнения.
  • Ключевые операторы и выражения (def, литералы, вызовы) формируют данные, с которыми работает остальная часть примера.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения строится на итерации: код последовательно применяет одну и ту же операцию к набору значений.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Типы связей — hasMany, hasOne, belongsTo, many-to-many через hasMany + belongsTo или явную join-таблицу.


Маппинг таблицы и столбцов

class Book {
String title

static mapping = {
table 'books'
title column: 'book_title', length: 255
sort 'title'
}
}

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с class Book { и задает контекст выполнения.
  • Конструкция class описывает структуру объекта: поля хранят состояние, а методы инкапсулируют поведение.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Транзакции

Book.withTransaction { status ->
def b = new Book(title: 'New Book')
if (!b.save()) {
status.setRollbackOnly()
}
}

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с Book.withTransaction { status -> и задает контекст выполнения.
  • Ключевые операторы и выражения (def, литералы, вызовы) формируют данные, с которыми работает остальная часть примера.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения контролируется условиями: при разных состояниях кода выбирается соответствующая ветка и результат.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

В сервисах Spring предпочтительнее @Transactional на методе Java/Groovy-сервиса — единая модель с остальным приложением.


GORM без Grails (Spring Boot)

  1. Подключить org.apache.grails:gorm-spring-boot (версия под ваш стек Grails/GORM).
  2. Настроить dataSource в application.yml.
  3. Поместить доменные классы в пакет, сканируемый GORM.
  4. Вызывать домены из @Service / контроллеров так же, как в Grails.

Детали зависят от версии — сверяйте с официальным guide GORM для Spring Boot.


Альтернатива — groovy.sql.Sql (скрипты)

Для ETL, админ-скриптов и разовых отчётов без ORM:


import groovy.sql.Sql

def sql = Sql.newInstance(
'jdbc:postgresql://localhost:5432/app',
'user', 'pass', 'org.postgresql.Driver'
)

sql.eachRow('SELECT id, title FROM books WHERE price < ?', [50.0]) { row ->
println "${row.id}: ${row.title}"
}
sql.close()

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с import groovy.sql.Sql и задает контекст выполнения.
  • Строки import подключают нужные классы и модули, чтобы дальше вызывать API без полного имени пакета.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Параметризованные запросы снижают риск SQL-инъекций. Для сложной логики в приложении чаще выбирают Spring Data или jOOQ.


Что выбрать

ЗадачаИнструмент
Веб на Grails, быстрый CRUDGORM
Spring + Groovy, знакомый Active RecordGORM или Spring Data JPA
Скрипт миграции / отчётgroovy.sql.Sql
Строгая схема, сложные запросыjOOQ / JPA из Java/Groovy

Типичные ошибки

  1. N+1 запросы при ленивой загрузке коллекций — используйте fetchMode, join в запросе или DTO.
  2. save() без проверки ошибок — тихие сбои валидации.
  3. Автосхема в продакшене — только контролируемые миграции (Flyway/Liquibase).

Связанные материалы


Практические шаблоны запросов

Пакетная обработка

Для больших таблиц данные лучше читать батчами, чтобы снизить нагрузку на память. Общая теория chunk/bulk и checkpoint — Пакетная работа с данными.


import groovy.sql.Sql

def sql = Sql.newInstance(url, user, pass, driver)
sql.eachRow("SELECT id, title FROM books ORDER BY id") { row ->
println "${row.id} -> ${row.title}"
}
sql.close()

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с import groovy.sql.Sql и задает контекст выполнения.
  • Строки import подключают нужные классы и модули, чтобы дальше вызывать API без полного имени пакета.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

При сложных ETL-сценариях добавляйте явный размер батча и промежуточный commit.


Транзакция "все или ничего"

sql.withTransaction {
sql.executeInsert("INSERT INTO books(title) VALUES (?)", ["Groovy handbook"])
sql.executeUpdate("UPDATE stats SET total = total + 1")
}

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с sql.withTransaction { и задает контекст выполнения.
  • Ключевые операторы и выражения (def, литералы, вызовы) формируют данные, с которыми работает остальная часть примера.
  • Вызовы функций или команд выполняют полезное действие: чтение данных, вычисление результата или запуск задачи сборки.
  • Поток выполнения линейный: шаги идут последовательно, поэтому итог зависит от порядка операций в блоке.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Если внутри транзакции возникает исключение, изменения откатываются целиком.


Архитектурные заметки по выбору слоя данных

  • GORM подходит для быстрого CRUD и бизнес-логики вокруг доменных сущностей.
  • groovy.sql.Sql удобен для админки, миграций и отчётов.
  • Spring Data / jOOQ лучше подходят для сложной SQL-модели и явного контроля запросов.

Смежные материалы: Groovy и Java, Jenkins Pipeline для запуска миграций в CI.


Сквозной кейс — сохранение активных книг

После нормализации данных из JSON сохраняем только активные записи:

Код ITЗагрузка примера кода…

Разбор:

  • Фрагмент на groovy показывает рабочий пример: начинается с import groovy.sql.Sql и задает контекст выполнения.
  • Строки import подключают нужные классы и модули, чтобы дальше вызывать API без полного имени пакета.
  • Вызовы методов и объявления функций задают основной шаг логики: входные значения передаются в метод и сразу обрабатываются.
  • Поток выполнения строится на итерации: код последовательно применяет одну и ту же операцию к набору значений.
  • Итог фрагмента — воспроизводимый результат — вывод в консоль, изменение данных или артефакт, который можно сразу проверить.

Шаблон легко переносится в job CI или административный скрипт.

Дальше по кейсу: Spock, Jenkins Pipeline.