Работа с базами данных из Groovy
Работа с базами данных из Groovy
На JVM доступ к данным обычно идёт через JDBC или ORM. В экосистеме Groovy чаще всего встречаются три уровня:
- GORM — Active Record поверх Hibernate (Grails, Spring Boot).
groovy.sql.Sql— тонкая обёртка над JDBC для скриптов и миграций.- Чистый 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)
- Подключить
org.apache.grails:gorm-spring-boot(версия под ваш стек Grails/GORM). - Настроить
dataSourceвapplication.yml. - Поместить доменные классы в пакет, сканируемый GORM.
- Вызывать домены из
@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, быстрый CRUD | GORM |
| Spring + Groovy, знакомый Active Record | GORM или Spring Data JPA |
| Скрипт миграции / отчёт | groovy.sql.Sql |
| Строгая схема, сложные запросы | jOOQ / JPA из Java/Groovy |
Типичные ошибки
- N+1 запросы при ленивой загрузке коллекций — используйте
fetchMode, join в запросе или DTO. save()без проверки ошибок — тихие сбои валидации.- Автосхема в продакшене — только контролируемые миграции (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.