Goodbye Grails. Hi Micronaut. Continuation


This is the second article in a series on migrating from Grails to Micronaut. Note: your application must be built in Grails 4.x or later

In total, there are 10 parts in a cycle of publications about migration from Grails to Micronaut:

Multi-module project
Configuration
Static compilation
Datasets
Marshaling
Domain classes
Services
Controllers
Micronaut App
Micronaut Data

In this article, we’ll talk about datasets, marshaling, and domain classes.

The first article (about multi-module projects, configuration and static compilation) can be read here.

Part 4. Datasets

It is worth writing tests for critical components of the application in case the migration does not go according to plan (and this may well happen). And for tests to be effective, you need meaningful data. In this article, we will show you how to create datasets using the Dru framework: it already supports Grails, GORM and Micronaut Data.

Dru does a great job of creating relationships between different entities, but we won’t go into details. If you want to know more about this framework, it is better to consult the documentation.

Let’s say we have a simple domain class in our codebase:

class Vehicle {

    String name 

    String make
    String model

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

To define a dataset for an entity, we usually take a JSON or SQL file. With JSON, you can get data from a test or production environment, and with SQL, you can use lightweight database dumps. To include Dru in the classpath parameter, update the application’s Gradle file using the following dependencies:

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"
}

This snippet shows how to use JSON fixtures to load test data:

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 }
        }
    }

}

If the class HelloDataSet declared inside hello, then the JSON file with test data for our class Vehicle will be here: src/test/resources/hello/HelloDataSet/vehicles.json

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

Our dataset deserves a separate specification, as many other tests will depend on correct data loading:

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'
    }

}

The created datasets will help us in the future in writing tests for controllers, as well as in migrating from GORM to Micronaut Data.

Now let’s move on to decoupling the web tier from the domain tier by passing data transfer objects (DTOs) to controllers.


Part 5. Marshaling

Controllers are responsible for communicating with other layers of the application, including the frontend. You need to make sure that the API does not change and the application will consume and return the same data as before the migration. For this, the Gru test framework, which supports Grails and Micronaut, is perfect. Gru can evaluate responses from controllers.

You can add Gru to your project by specifying the following dependency in the Gradle file of your application subproject:

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

Let’s say we have a simple controller that only renders one entity.

Let’s write a simple test that checks the JSON output of the controller.

class VehicleController {
 
    VehicleDataService vehicleDataService

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

}

Let’s take the dataset created in the previous step to load test data for rendering. The vehicle.json file is generated automatically on first launch. However, we need to re-check it for variable values ​​such as timestamps. V reference documentation describes additional operations such as ignoring timestamps.

We have written tests for the current output. Now it’s time to switch the interior to ObjectMapper

In our case, the class VehicleResponse looks like a simple entity 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'
                }
            }
    }

}

We want to make sure that no Grails related marshaling is triggered under the hood. This will further help us migrate to Micronaut controllers as well as Micronaut Data.

Current tests will fail due to lack of bin ObjectMapper… Fortunately, this is easy to fix with the doWithSpring method: just declare the bean ObjectMapper

In the next step, we will extract domain classes to a separate library.


Part 6. Classes of the subject area

Typically, domain classes are essential components of any Grails application, and are therefore the most difficult to port. First, we need to transfer all calls related to the database in order to use data services in all cases instead of “Magic” methods and properties (including static methods / properties and instance methods / properties).

We can easily create a data service for a domain class Vehiclewith which we started working earlier:

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

@Service(Vehicle)
@CompileStatic
interface VehicleDataService {

    Vehicle findById(Long id)

}

We have already used such a service in the controller in the previous step.

Finding a GORM Method Using Version Control

The hardest part is finding all the use cases for the GORM instance and static API.

First, let’s find all the use cases for the entity. The easiest way is to commit all the work to source control, so your IDE does all the hard work of finding links. Select one of the entities and transfer it to another package. You can, for example, add .legacy to your package name: so class Vehicle from hello will move to hello.legacy

Don’t forget to move your data service!

Let’s take a look at the list of changed files in the version control system: it should contain all the classes related to a specific class of the subject area.

Now let’s replace all calls to static methods and methods of the GORM instance in these files. For example, let’s change Vehicle.get(id) on the vehicleDataService.findById(id)… We can simulate work in tests vehicleDataService or implement real testing with a test datastore. The last procedure is detailed in this article:

Let’s re-list the steps that need to be performed for each class in the domain:

  1. Commit all changes to the version control system.

  2. Move the subject area class to a separate package (for example, original.legacy).

  3. Create a new data service for the domain, or move an existing one into the same package.

  4. Check all changed files in version control system.

  5. Replace GORM methods with data service calls.

  6. Repeat these steps for each domain class until you transfer all of them.

Finding GORM Methods at Compile Time

After completing the steps above, there might be some well-hidden calls to static and GORM instance methods. To find them, let’s use the Groovy Code Check for GORM:

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

It is a strict library: it will throw compilation errors every time it finds a GORM-related method. This is very useful for finding indirect use cases (e.g. user.vehicle.save()) when GORM methods are not called directly from the entity object, but by reference.

Compilation errors can also occur due to changes in the Enterprise Groovy config file 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()}"
)

If we comment out or remove the checking extensions associated with GORM, we get compilation errors wherever the library magic is applied.

Transferring domain classes to the library

Let’s extract the domain classes into a separate subproject so that we can then create a modular structure for the other components of the application. This is described in detail in a separate article:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *