Статьи

Настройка Grails для игнорирования DDL на определенных доменах

Зачастую создание приложения Grails, использующего все классные кусочки GORM с существующей схемой базы данных (например, предоставляемой сторонним продуктом), может быть довольно простым, но в зависимости от сложности схемы оно также может быть очень уродливым. Недавно я закончил приложение, которое должно было (а) отобразить пользовательские объекты приложения в новые таблицы и (б) объединить существующие данные из таблиц вендоров, не изменяя и не компрометируя их схему.

Рассматриваемая схема поставщика является частью ERP с тысячами таблиц и одинаково большим количеством индексов, ограничений, триггеров и так далее. Если вы опытный разработчик Grails, вы можете подумать «просто сопоставьте объекты с существующими таблицами и покончите с этим». Хорошо, но что произойдет, если, как и я, вы случайно оставите свойство dbCreate в своем источнике данных равным «create-drop», и во время тестирования Grails удалит критическую таблицу с миллионами записей, которые нельзя потерять? К счастью, это произошло в среде разработки, где вполне допустимы такие аварии, но в производственной среде эти аварии неприемлемы. Это событие заставило меня задуматься:  как ответственный способ использовать Grails с существующими таблицами и гарантировать, что он не будет пытаться изменить эти объекты схемы? Если бы только в отображении Grails был простой способ отключить DDL на определенных доменах, но при этом сохранялось множество методов DRY, таких как list (…), именованные запросы и даже вставка / обновление записей с помощью save (…).

Я представляю вам то, что я считаю достаточно элегантным решением этой проблемы, объединяющим чтение многих сообщений на форуме и поиск крошечных полезных фрагментов информации в других блогах. Хитрость заключается в том, чтобы настроить источник данных с помощью нового настроенного класса конфигурации Hibernate, который расширяет поведение Grails по умолчанию, но затем переопределяет создание оператора DDL нашими собственными действиями, чтобы игнорировать конкретные таблицы.

Это конкретное решение использует тот факт, что базовая версия Grails класса конфигурации Hibernate создается с доступом к экземпляру приложения Grails. Поэтому мы можем привязать Config.groovy, и не нужно жестко кодировать имена таблиц.

package edu.fhda.grails.hibernate

import groovy.util.logging.Log4j
import org.codehaus.groovy.grails.commons.GrailsApplication
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration
import org.hibernate.HibernateException
import org.hibernate.dialect.Dialect
import org.hibernate.tool.hbm2ddl.DatabaseMetadata

/**
 * Customized Hibernate configurator that extends the default GrailsAnnotationConfiguration to provide
 * a method for specifying a configurable list of tables to ignore during Grails startup. Very helpful
 * if you want to have domain objects that should be GORM managed alongside domain objects that
 * map to existing Banner database tables. This will prevent Grails from trying to create, update, or drop
 * Banner baseline schema objects.
 * @author Matt Rapczynski, rapczynskimatthew@fhda.edu
 */
@Log4j
class BannerHibernateConfiguration extends GrailsAnnotationConfiguration {

    private GrailsApplication grailsApplication

    @Override
    String[] generateDropSchemaScript(Dialect dialect) throws HibernateException {
        return checkAndRemoveIgnoredTables(super.generateDropSchemaScript(dialect));
    }

    @Override
    String[] generateSchemaCreationScript(Dialect dialect) throws HibernateException {
        return checkAndRemoveIgnoredTables(super.generateSchemaCreationScript(dialect));
    }

    @Override
    String[] generateSchemaUpdateScript(Dialect dialect, DatabaseMetadata databaseMetadata) throws HibernateException {
        return checkAndRemoveIgnoredTables(super.generateSchemaUpdateScript(dialect, databaseMetadata));
    }

    private String[] checkAndRemoveIgnoredTables(String[] sqlStatements) {
        // Create a collection of SQL statements for non-ignored tables
        ArrayList<String> validSqlStatements = new ArrayList<>()

        // Get the list of table names to ignored from the application config
        def ignoredTables = grailsApplication.config.edu.fhda.grails.hibernate.banner.ignoreTables as List<String>
        if(ignoredTables?.size() > 0) {
            log.debug "Using ignored tables configuration: ${ignoredTables}"

            // Iterate each SQL DML statement generated by GORM
            sqlStatements?.each { String sqlStatement ->
                boolean safeToAdd = true

                // Check to see if the statement references any ignored tables
                ignoredTables?.each { String tableName ->
                    if(sqlStatement.toLowerCase().contains("table ${tableName.toLowerCase()}")) {
                        /* NOTE: Log reference using this keyword prevents NullPointerExceptions */
                        this.log.debug "Bypassing ${tableName} for Grails domain mapping"
                        safeToAdd = false
                    }
                }

                // Did the statement pass the test?
                if(safeToAdd) {
                    validSqlStatements.add(sqlStatement)
                }
            }

            // Return a String array of validated statements
            return validSqlStatements.toArray() as String[]
        }
        return sqlStatements
    }

    @Override
    void setGrailsApplication(GrailsApplication application) {
        // Run superclass method
        super.setGrailsApplication(application)

        // Inject Grails application reference
        this.grailsApplication = application
    }

}

Что происходит?

  • Hibernate генерирует операторы DDL на основе описания объектов и компилирует эти операторы в массив строк. Мы можем переопределить этот метод генерации, заставить суперкласс выполнить свою обычную процедуру, но затем изменить операторы, прежде чем возвращать их для выполнения в базе данных.
  • Закрытый метод checkAndRemoveIgnoredTables предоставляет логику для проверки операторов SQL в массиве String. Используя список таблиц в Config.groovy с пространством имен, мы можем искать шаблоны, в которых мы находим текст «table_name таблицы» в выражении DDL, а затем помечаем его как игнорируемый. Это прекрасно подходит для поиска операторов, таких как create table, drop table, alter table create index для таблицы и т. Д. И т. Д.
  • После того, как мы определили набор операторов SQL, которые нормально работать , например , как для объектов предметной области , что мы  действительно  хотим Grails управлять, то этот список возвращается в качестве String [] и Hibernate продолжает свою работу.
  • Протестировано с Grails 2.2.1
  • Проверено на СУБД Oracle 11gR2

Как выглядит файл Config.groovy, когда вы хотите указать конкретные таблицы?

// Hibernate Configuration for Banner (one or more tables Grails should never try to modify)
edu.fhda.grails.hibernate.banner.ignoreTables = [
    "STVTERM"
]

И конфигурация DataSource.grooy; все, что вам нужно сделать, это добавить (или настроить) одну строку, указав новое имя класса для конфигурации Hibernate:

// Example data source configuration
dataSource {
    pooled = true
    driverClassName = "oracle.jdbc.OracleDriver"
    configClass = edu.fhda.grails.hibernate.BannerHibernateConfiguration
}

И это все! Пройдите процесс добавления этого в приложение, которое нуждается в нем впервые, и вы обнаружите, что это довольно простое решение для работы. Я использовал его для применения в производстве сегодня с большим успехом. Единственный способ сделать это проще, если команда Grails добавит эту функциональность в будущий выпуск Grails, где мы можем включить или выключить DDL с помощью значения true или false в отображении таблицы.

Во введении я сосредоточился на проблеме смешивания пользовательских таблиц с отображением существующих таблиц. Сценарий, который я пропустил, заключается в том, что вы хотите сопоставить домен представлению SQL, а не напрямую с таблицей. Это решение работает очень хорошо и для представлений — и я должен знать, потому что это одна из причин, почему я исследовал эту проблему в первую очередь. Почему Grails должен делать DDL для существующего представления? Это не нужно, и вы можете использовать этот подход, чтобы пропустить представления в вашей базе данных.

Во время моего первоначального исследования я прочитал некоторые комментарии о переполнении стека от другого разработчика, который считал, что сопоставление доменов Grails с представлениями было «излишним», но я думаю, что это должно зависеть от того, что вы пытаетесь сделать с представлением. В моем конкретном случае я работаю над созданием приложений для системы ERP, где нередко пишутся запросы с 6, 10 и даже 12 объединениями таблиц. Превращение этих объединений в отдельные домены, а затем навигация по каменистым водам составных внешних ключей — сложная задача (возможно, даже невозможная!). Это просто название игры, когда вы работаете с гигантской системой, разработанной в течение периода, превышающего 15 лет.

Я использую представления, чтобы маскировать эту сложность управляемым способом, и при этом сохраняю преимущества GORM для чистой бизнес-логики. Используя приведенное выше решение, я могу убедиться, что мои представления не затронуты и что Grails не пытается создавать индексы или ограничения для вещей, которые им не нужны. Дополнительное предложение:  я не часто работал с реляционными базами данных, кроме Oracle, но в экосистеме Oracle я использую триггеры «INSTEAD OF» для сохранения возможности вставлять и обновлять через представления из доменов Grails.

В любом случае, это так. Я разместил фрагменты кода выше как GitHub Gist. Если у вас есть хорошие моды, пожалуйста, раскройте суть, чтобы сообщество могло извлечь выгоду из любых улучшений. Наслаждайтесь, и я надеюсь, что это послужит вам так же хорошо, как и для меня.

Смотрите этот пример как GitHub Gist