Это вторая статья из цикла о миграции из Grails в Micronaut. Обратите внимание: ваше приложение должно быть создано в Grails 4.x или более поздней версии.

Всего в цикле публикаций о миграции из Grails в Micronaut 10 частей:

Многомодульный проект
Конфигурация
Статическая компиляция
Датасеты
Маршалинг
Классы предметной области
Сервисы
Контроллеры
Приложение Micronaut
Micronaut Data

В этой статье поговорим о датасетах, маршалинге и классах предметной области.

Первую статью (о многомодульных проектах, конфигурации и статической компиляции) можно прочитать здесь.

Часть 4. Датасеты

Для критически важных компонентов приложения стоит написать тесты, на случай если миграция пойдет не по плану (а такое вполне может произойти). А чтобы тесты были эффективными, нужны имеющие смысл данные. В этой статье мы расскажем, как создать датасеты с помощью фреймфорка Dru: он уже поддерживает Grails, GORM и к тому же Micronaut Data.

Dru отлично справляется с созданием отношений между различными сущностями, но мы не будем вдаваться в подробности. Если вы хотите узнать больше об этом фреймворке, лучше обратитесь к документации.

Допустим, в нашей кодовой базе есть простой класс предметной области:

class Vehicle {

    String name 

    String make
    String model

    static constraints = { 
        name maxSize: 255
        make inList: ['Ford', 'Chevrolet', 'Nissan', 'Citroen']
        model nullable: true
    }
}

Чтобы определить набор данных для сущности, мы обычно берем файл в формате JSON или SQL. Посредством JSON можно получать данные из тестовой или рабочей среды, а с помощью SQL можно использовать упрощенные дампы базы данных. Чтобы включить Dru в параметр classpath, обновите Gradle-файл приложения, используя следующие зависимости:

dependencies {
    // other dependencies

    testCompile "com.agorapulse:dru:0.8.1"
    testCompile "com.agorapulse:dru-client-gorm:0.8.1"
    testCompile "com.agorapulse:dru-parser-json:0.8.1"
}

В этом фрагменте показано, как применять JSON-фикстуры для загрузки тестовых данных:

import com.agorapulse.dru.Dru
import com.agorapulse.dru.PreparedDataSet
import groovy.transform.CompileStatic

@CompileStatic
class HelloDataSets {

    public static final PreparedDataSet VEHICLES = Dru.prepare {
        from 'vehicles.json', {
            map { to Vehicle }
        }
    }

}

Если класс HelloDataSet объявлен внутри hello, то JSON-файл с тестовыми данными для нашего класса Vehicle будет здесь: src/test/resources/hello/HelloDataSet/vehicles.json.

[
  {
    "name": "The Box",
    "make": "Citroen",
    "model": "Berlingo"
  }
]

Наш датасет заслуживает отдельной спецификации, поскольку многие другие тесты будут зависеть от правильной загрузки данных:

import com.agorapulse.dru.Dru
import grails.testing.gorm.DataTest
import spock.lang.AutoCleanup
import spock.lang.Specification

class HelloDataSetsSpec extends Specification implements DataTest {

    @AutoCleanup Dru dru = Dru.create(this)

    void 'vehicles are loaded'() {
        given:
            dru.load(HelloDataSets.VEHICLES)
        when:
            Vehicle box = Vehicle.findByName('The Box')
        then:
            box
            box.name == 'The Box'
            box.make == 'Citroen'
            box.model == 'Berlingo'
    }

}

Созданные датасеты в будущем помогут нам в написании тестов для контроллеров, а также в миграции из GORM в Micronaut Data.

Теперь давайте перейдем к отделению веб-уровня от уровня предметной области путем передачи объектов переноса данных (DTO) в контроллеры.


Часть 5. Маршалинг

Контроллеры ответственны за связь с другими уровнями приложения, включая фронтенд. Нужно убедиться, что API не изменится и приложение будет потреблять и выдавать те же данные, что и до миграции. Для этого отлично подойдет тестовый фреймворк Gru, поддерживающий Grails и Micronaut. Gru может оценивать ответы от контроллеров.

Вы можете добавить Gru в свой проект, указав следующую зависимость в файле Gradle подпроекта вашего приложения:

testCompile 'com.agorapulse:gru-grails:0.9.2'

Допустим, у нас есть простейший контроллер, который рендерит только одну сущность.

Напишем простой тест, который будет проверять JSON-вывод контроллера.

class VehicleController {
 
    VehicleDataService vehicleDataService

    Object show(Long id) {
        Vehicle vehicle = vehicleDataService.findById(id)
        if (!vehicle) {
            render status: 404
            return
        }
        render vehicle as JSON
    }

}

Возьмем датасет, созданный на предыдущем этапе, чтобы загрузить тестовые данные для рендеринга. Файл vehicle.json создается автоматически при первом запуске. Однако нам нужно повторно проверить его на предмет таких значений переменных, как временные метки. В справочной документации описаны дополнительные операции, такие как игнорирование меток времени.

Мы написали тесты для текущего вывода. Теперь пришло время переключить внутреннюю часть на ObjectMapper.

В нашем случае класс VehicleResponse выглядит как простая сущность Vehicle:

import com.agorapulse.dru.Dru
import com.agorapulse.gru.Gru
import com.agorapulse.gru.grails.Grails
import grails.testing.gorm.DataTest
import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.AutoCleanup
import spock.lang.Specification

class VehicleControllerSpec extends Specification implements ControllerUnitTest<VehicleController>, DataTest {

    @AutoCleanup Dru dru = Dru.create {
        include HelloDataSets.VEHICLES
    }

    @AutoCleanup Gru gru = Gru.create(Grails.create(this)).prepare {
        include UrlMappings
    }

    void 'render with gru'() {
        given:
            dru.load()

            controller.vehicleDataService = Mock(VehicleDataService) {
                findById(1) >> dru.findByType(Vehicle)
            }

        expect:
            gru.test {
                get '/vehicle/1'
                expect {
                    json 'vehicle.json'
                }
            }
    }

}

Мы хотим убедиться, что под капотом не запускается маршалинг, связанный с Grails. В дальнейшем это поможет нам перейти на контроллеры Micronaut, а также на Micronaut Data.

Текущие тесты не сработают из-за отсутствия бина ObjectMapper. К счастью, это легко исправить с помощью метода doWithSpring: просто объявите бин ObjectMapper.

На следующем этапе мы извлечем классы предметной области в отдельную библиотеку.


Часть 6. Классы предметной области

Как правило, классы предметной области выступают важными компонентами любого приложения Grails, поэтому их сложнее всего переносить. Для начала нам нужно перенести все вызовы, связанные с базой данных, чтобы во всех случаях использовать сервисы данных вместо «магических» методов и свойств (включая статические методы/свойства и методы/свойства экземпляра).

Мы можем с легкостью создать сервис данных для класса предметной области Vehicle, с которым мы начали работать ранее:

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@Service(Vehicle)
@CompileStatic
interface VehicleDataService {

    Vehicle findById(Long id)

}

Мы уже пользовались таким сервисом в контроллере на предыдущем этапе.

Поиск метода GORM с помощью системы контроля версий

Самая сложная задача — найти все случаи использования экземпляра GORM и статического API.

Для начала найдем все случаи использования сущности. Самый простой способ — зафиксировать всю работу в системе контроля версий: так ваша IDE выполнит всю сложную работу по поиску ссылок. Выберите одну из сущностей и перенесите ее в другой пакет. Можно, например, добавить .legacy к имени вашего пакета: так класс Vehicle из hello переместится в hello.legacy.

Не забудьте переместить сервис данных!

Взглянем на список измененных файлов в системе контроля версий: он должен содержать все классы, относящиеся к определенному классу предметной области.

Теперь заменим все вызовы статических методов и методов экземпляра GORM в этих файлах. Например, поменяем Vehicle.get(id) на vehicleDataService.findById(id). Мы можем имитировать в тестах работу vehicleDataService или реализовать настоящее тестирование с тестовым хранилищем данных. Последняя процедура подробно описана в этой статье:

Давайте еще раз перечислим действия, которые нужно выполнить для каждого класса предметной области:

  1. Зафиксировать все изменения в системе контроля версий.

  2. Переместить класс предметной области в отдельный пакет (например, original.legacy).

  3. Создать новый сервис данных для предметной области или переместить существующий в тот же пакет.

  4. Проверить все измененные файлы в системе контроля версий.

  5. Заменить методы GORM вызовами сервиса данных.

  6. Повторяйте эти шаги для каждого класса предметной области, пока не перенесете их все.

Поиск методов GORM во время компиляции

После выполнения описанных выше шагов могут остаться некоторые хорошо скрытые вызовы статических методов и методов экземпляра GORM. Чтобы найти их, воспользуемся проверкой кода Groovy для GORM (Groovy Code Check for GORM):

compileOnly 'com.agorapulse:groovy-code-checks-gorm:0.9.0'

Это строгая библиотека: она будет выдавать ошибки компиляции каждый раз, когда находит метод, связанный с GORM. Это очень полезно для поиска случаев непрямого использования (например, user.vehicle.save()), когда методы GORM вызываются не напрямую из объекта сущности, а по ссылке.

Ошибки компиляции также могут возникнуть из-за изменения конфигурационного файла Enterprise Groovy convention.groovy.

Map conventions = [
    disable                     : false,
    whiteListScripts            : true,
    disableDynamicCompile       : false,  
    dynamicCompileWhiteList     : [
                'UrlMappings',
                'Application',
                'BootStrap',
                'resources', 
                'org.grails.cli'
    ],
    limitCompileStaticExtensions: false,
    defAllowed                  : false,    // For controllers you can use Object in place of def, and in Domains add Closure to constraints/mappings closure fields.
    skipDefaultPackage          : true,     // For GSP files
    compileStaticExtensions     : [
      'org.grails.compiler.ValidateableTypeCheckingExtension',
      'org.grails.compiler.NamedQueryTypeCheckingExtension',
      'org.grails.compiler.HttpServletRequestTypeCheckingExtension',
//      'org.grails.compiler.WhereQueryTypeCheckingExtension',
//      'org.grails.compiler.DynamicFinderTypeCheckingExtension',
//      'org.grails.compiler.DomainMappingTypeCheckingExtension',
//      'org.grails.compiler.RelationshipManagementMethodTypeCheckingExtension'
    ],
]
System.setProperty(
    'enterprise.groovy.conventions', 
    "conventions=${conventions.inspect()}"
)

Если мы закомментируем или удалим проверяющие расширения, связанные с GORM, то получим ошибки компиляции везде, где применяется «магия» библиотеки.

Перенос классов предметной области в библиотеку

Давайте извлечем классы предметной области в отдельный подпроект, чтобы затем создать модульную структуру для других компонентов приложения. Об этом подробно написано в отдельной статье:

Если вы применяли структуру Kordamp, просто создайте новую папку в разделе libs (например, hello-data), где будет файл сборки hello-data.gradle.

sourceSets {
    main {
        groovy {
            // the source folder for the GORM domain classes
            srcDir 'grails-app/domain'
            // if you also want to include some services
            srcDir 'grails-app/services'
        }
    }
}

dependencies {
    // GORM
    compile "org.grails:grails-datastore-gorm-hibernate5:${project['gorm.hibernate.version']}"

    // required for Grails Plugin generation
    compileOnly "org.grails:grails-core:$grailsVersion"

    // required for Micronaut service generation, if present
    compileOnly "io.micronaut:micronaut-inject-groovy:$micronautVersion"

    // required for jackson ignores generation for Micronaut
    compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.8.11.3'
}

Также стоит объявить основной репозиторий Grails (Grails Central) в корневом файле build.gradle для всех проектов:

allprojects {
    repositories {
        mavenCentral()
        maven { url 'https://repo.grails.org/grails/core/' }
    }
}

Добавляем два новых свойства в файл gradle.properties:

gorm.hibernate.version = 7.0.5
micronautVersion = 1.3.7

Нам также понадобится поддельный дескриптор плагина Grails в папке src/main/groovy:

@CompileStatic
class HelloDataGrailsPlugin {

    String grailsVersion = '3.3.0 > *'

    String title = 'GORM Hello Data'
    String author = 'Vladimir Orany'
    String authorEmail = 'vlad@agorapulse.com'
    String description = 'Mimicking Grails Plugin'

}

Далее создаем папки grails-app/domains и grails-app/services в новой библиотеке данных.

Теперь добавляем новую библиотеку в качестве зависимости в приложение Grails в файле hello.gradle:

implementation project(':hello-data')

В IntelliJ IDEA можно легко переместить пакеты с сущностями предметной области в новую библиотеку. Выбираем исходный пакет, затем в меню сверху нажимаем Refactor -> Move Package or Directory… (горячая клавиша по умолчанию — F6). 

Выбираем Move directory… to another source root.

Выбираем папку grails-app/domain в качестве целевого расположения:

Проверим все предметные области в новой библиотеке данных и добавим аннотацию grails.gorm.annotation.Entity.

@Entity
class Vehicle {

    String name 

    String make
    String model

    static constraints = { 
        name maxSize: 255
        make inList: ['Ford', 'Chevrolet', 'Nissan']
        model nullable: true
    }
}

Наконец переносим связанные сервисы данных GORM в папку grails-app/services: теперь весь код, относящийся к классам предметной области, должен быть в отдельной библиотеке.

Перенос тестовых данных в библиотеку

Переходим к следующему шагу: создадим еще одну библиотеку, где будут тестовые данные, чтобы в будущем обращаться к ним откуда угодно.

Создадим новую папку hello-data-test-data с новым файлом сборки hello-data-test-data.gradle.

dependencies {
    api project(':hello-data')

    api 'com.agorapulse:dru-client-gorm:0.8.1'
    api 'com.agorapulse:dru-parser-json:0.8.1'

    testImplementation("org.spockframework:spock-core") {
        exclude group: "org.codehaus.groovy", module: "groovy-all"
    }

}

Переносим классы тестовых данных вроде HelloDataSets в папку src/main/groovy.

 

Переносим тестовые данные вроде vehicle.json в папку src/main/resources.

Переносим тесты для датасетов вроде HelloDataSetsSpec в папку src/test/groovy, чтобы данные проходили необходимые проверки.

Не забудьте добавить зависимость для новой библиотеки тестовых данных в файл сборки приложения — hello.gradle.

testCompile project(':hello-data-test-data')

После извлечения классов предметной области мы также можем извлечь сервисы в отдельную библиотеку — об этом читайте в следующей статье.

Оригиналы публикаций: часть 4, часть 5, часть 6.


Материал подготовлен в рамках курса «Groovy Developer».

Groovy имеет множество замечательных функций для написания DSL.
Они позволяют создавать удобные библиотеки для разработки, декларативные конфигурации в виде кода, а также инструменты, которые позволяют описывать сценарии без навыков программирования.

Всех желающих приглашаем на бесплатное demo-занятие «Groovy DSL. Создание инструментов для разработки». На занятии рассмотрим такие функции, как: Chains, Script Engine, Operator Overloading, Categories, Closures, AST-трансформации, и другие.
>> РЕГИСТРАЦИЯ НА ЗАНЯТИЕ

Комментарии (0)