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 Vehicle
with 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:
Commit all changes to the version control system.
Move the subject area class to a separate package (for example,
original.legacy
).Create a new data service for the domain, or move an existing one into the same package.
Check all changed files in version control system.
Replace GORM methods with data service calls.
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: