create-app --lang groovy --inplace --profile service --features hibernate-gorm,groovy,spock
09 June 2019
In Grails, there is a nice extension for GORM that enable us to use json types in the Postgres database. At my work in Cardlay, we had a situation where we would like to store JSON but still using the RDBMS features, and this would be nice for one of our microservices, but Micronaut is preferred for real micro-services.
|
Tip
|
It turned out to be rather easy to use in Micronaut! |
Here, I’ll share the steps needed. First a micronaut app is created, with Groovy as the language and hibernate-gorm as a feature. Thus in the micronaut console do:
create-app --lang groovy --inplace --profile service --features hibernate-gorm,groovy,spock
In build.gradle include in the dependencies block:
compile 'org.grails.plugins:postgresql-extensions:6.1.0'
The dialect of Hibernate must know the extensions, so in src/main/resources/application.yml add
hibernate: dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect
We can now add a domain class that make use of the extensions:
package postgresql.extensions.micronaut
import grails.gorm.annotation.Entity
import net.kaleidos.hibernate.usertype.JsonbMapType
import net.kaleidos.hibernate.usertype.ArrayType
import net.kaleidos.hibernate.usertype.HstoreMapType
@Entity
class GormStorageObject {
String job
Map history
Integer[] favoriteNumbers = []
Map testAttributes
static mapping = {
history type: JsonbMapType
favoriteNumbers type:ArrayType, params: [type: Integer]
testAttributes type: HstoreMapType
}
}
To create a database for the application, we can use docker-compose.
version: "2"
services:
database:
image: postgres:9.5-alpine
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- ./postgresdata/seed.sql:/docker-entrypoint-initdb.d/seed.sql
Where the `seed.sql file adds the required hstore extension for the database:
CREATE EXTENSION hstore;
Running
docker-compose up
And starting the application
./gradlew run
And peeking into the database we see the table is created with the types as expected
postgres=# \d gorm_storage_object
Table "public.gorm_storage_object"
Column | Type | Collation | Nullable | Default
------------------+------------------------+-----------+----------+---------
id | bigint | | not null |
version | bigint | | not null |
job | character varying(255) | | not null |
test_attributes | hstore | | not null |
history | jsonb | | not null |
favorite_numbers | integer[] | | not null |
Indexes:
"gorm_storage_object_pkey" PRIMARY KEY, btree (id)
As the extensions is a Grails plugin, the usage is originally setup from the Grails plugin desctiptor. This does not happen in Micronaut, but we can wire this togeather easily at startup, listening for an event of type ServerStartupEvent. Add a class like this
package postgresql.extensions.micronaut import groovy.util.logging.Slf4j import io.micronaut.context.event.ApplicationEventListener import io.micronaut.runtime.server.event.ServerStartupEvent import net.kaleidos.hibernate.postgresql.criteria.ArrayCriterias import net.kaleidos.hibernate.postgresql.criteria.HstoreCriterias import net.kaleidos.hibernate.postgresql.criteria.JsonCriterias import javax.inject.Singleton @Slf4j @Singleton class GormExtensionInitializer implements ApplicationEventListener{ @Override void onApplicationEvent(ServerStartupEvent event) { new ArrayCriterias() new HstoreCriterias() new JsonCriterias() } }
To be able to store objects, lets first create a service to save and a controller that can use it:
package postgresql.extensions.micronaut
import grails.gorm.services.Service
import javax.inject.Singleton
@Singleton
@Service(GormStorageObject)
interface GormStorageService {
List findAll()
GormStorageObject save(GormStorageObject gormStorageObject)
}
And a controller
package postgresql.extensions.micronaut
import groovy.util.logging.Slf4j
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
@Slf4j
@Controller("/storage")
class StorageController {
GormStorageService gormStorageService
StorageController(GormStorageService gormStorageService) {
this.gormStorageService = gormStorageService
}
@Get("/")
List index() {
log.info("List all")
gormStorageService.findAll()
}
@Post("/")
GormStorageObject save(GormStorageObject gormStorageObject) {
log.info("Save: ${gormStorageObject}")
gormStorageService.save(gormStorageObject)
}
}
We can now do a first manual test of the application
curl localhost:8080/storage -X POST -H 'content-type: application/json' -d '{"job":"Job 1", "history": {"date":"2019-05-26", "activity":"Fun stuff", "price": 12}, "favoriteNumbers": [2, 3, 5, 7], "testAttributes": {"foo":"bar","baz":"rab"} }'
curl localhost:8080/storage
Peeking into the database, we can see all data is correct stored:
postgres=# select * from gorm_storage_object ;
id | version | job | test_attributes | history | favorite_numbers
----+---------+-------+----------------------------+--------------------------------------------------------------+------------------
1 | 0 | Job 1 | "baz"=>"rab", "foo"=>"bar" | {"date": "2019-05-26", "price": 12, "activity": "Fun stuff"} | {2,3,5,7}
(1 row)
Service to use the filtering function:
package postgresql.extensions.micronaut
import grails.gorm.transactions.Transactional
import javax.inject.Singleton
@Singleton
@Transactional
class GormExtensionsService {
List filterByHasFavoriteNumbers(Integer[] numbers) {
GormStorageObject.withCriteria {
pgArrayContains 'favoriteNumbers', numbers
}
}
}
And adding the controller method:
@Get("/numbers")
List filterByNumbers(Integer[] numbers) {
log.info("filterByNumbers: ${numbers}")
gormExtensionsService.filterByHasFavoriteNumbers(numbers)
}
We can now run a simple curl test:
curl "localhost:8080/storage/numbers?numbers=2&numbers=5"
[{"job":"Job 1","history":{"date":"2019-05-26","price":12.0,"activity":"Fun stuff"},"favoriteNumbers":[2,3,5,7],"testAttributes":{"foo":"bar","baz":"rab"},"id":1}]
curl "localhost:8080/storage/numbers?numbers=2&numbers=4"
[]
Service method
ListfilterByAttributes(String attribute) { GormStorageObject.withCriteria { pgHstoreContainsKey "testAttributes", attribute } }
Controller method
@Get("/attribute")
List filterByTestAttributes(String attribute) {
log.info("filterByTestAttributes: ${attribute}")
gormExtensionsService.filterByAttributes(attribute)
}
Test
curl "localhost:8080/storage/attribute?attribute=foo"
[{"job":"Job 1","history":{"date":"2019-05-26","price":12.0,"activity":"Fun stuff"},"favoriteNumbers":[2,3,5,7],"testAttributes":{"foo":"bar","baz":"rab"},"id":1}]
curl "localhost:8080/storage/attribute?attribute=not"
[]
Service method
ListfilterByHistory(String date) { GormStorageObject.withCriteria { pgJson 'history', '->>', "date", '=', date } }
Controller method
@Get("/history")
List filterByHistory(String date) {
log.info("filterByHistory: ${date}")
gormExtensionsService.filterByHistory(date)
}
Test
curl "localhost:8080/storage/history?date=2019-05-26"
[{"job":"Job 1","history":{"date":"2019-05-26","price":12.0,"activity":"Fun stuff"},"favoriteNumbers":[2,3,5,7],"testAttributes":{"foo":"bar","baz":"rab"},"id":1}]
curl "localhost:8080/storage/history?date=2019-05-27"
[]
We should of course still write a test for our service and controller. As this is very Postgres specific, we should use the postgres database. Testcontainers make this easy for us.
In build.gradle include in the dependencies
testCompile 'org.testcontainers:testcontainers:1.11.3' testCompile 'org.testcontainers:spock:1.11.3' testCompile 'org.testcontainers:postgresql:1.11.3'
And we will add an src/main/resources/application-test.yml file, to let the test environment also use the correct hibernate dialect and driver.
hibernate: dialect: net.kaleidos.hibernate.PostgresqlExtensionsDialect dataSource: pooled: true jmxExport: true driverClassName: org.postgresql.Driver
One caveat, if we are using hstore, we cannot just use the build in PostgreSQLContainer, as it does not have the hstore extension. We can solve this by using the already created docker-compose file.
Then we can test the services using the following type of test:
package postgresql.extensions.micronaut
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.spock.Testcontainers
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
@Testcontainers
class GormExtensionServiceSpec extends Specification {
@Shared
DockerComposeContainer dockerComposeContainer =
new DockerComposeContainer(new File("docker-compose.yml"))
.withExposedService("database", 5432)
@Shared @AutoCleanup
EmbeddedServer embeddedServer
@Shared
GormStorageService gormStorageService
@Shared
GormExtensionsService gormExtensionsService
void setupSpec() {
String host = dockerComposeContainer.getServiceHost("database", 5432)
Integer port = dockerComposeContainer.getServicePort("database", 5432)
embeddedServer = ApplicationContext
.build()
.properties(
'dataSource.url':"jdbc:postgresql://${host}:${port}/postgres",
'dataSource.username':"postgres",
'dataSource.password':"postgres",
)
.packages(
'postgresql.extensions.micronaut'
)
.run(EmbeddedServer)
gormExtensionsService = embeddedServer.applicationContext.getBean(GormExtensionsService)
gormStorageService = embeddedServer.applicationContext.getBean(GormStorageService)
}
void "test save and filteringByAttributes"() {
setup:
gormStorageService.save(new GormStorageObject(
job: 'irrelevant',
history: [date: '2019-05-26', activity:'Speakers dinner'],
favoriteNumbers: [1,2,3],
testAttributes: [happy:'path',funny:'yes']
))
when:
List result = gormExtensionsService.filterByAttributes('happy')
then:
result.size() == 1
result.first().testAttributes.happy == 'path'
}
}
And the controller and full application using a low level client:
package postgresql.extensions.micronaut
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.spock.Testcontainers
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
@Testcontainers
class StorageControllerSpec extends Specification {
@AutoCleanup
@Shared
RxHttpClient client
@Shared
DockerComposeContainer dockerComposeContainer =
new DockerComposeContainer(new File("docker-compose.yml"))
.withExposedService("database", 5432)
@Shared @AutoCleanup
EmbeddedServer embeddedServer
void setupSpec() {
String host = dockerComposeContainer.getServiceHost("database", 5432)
Integer port = dockerComposeContainer.getServicePort("database", 5432)
embeddedServer = ApplicationContext
.build()
.properties(
'dataSource.url':"jdbc:postgresql://${host}:${port}/postgres",
'dataSource.username':"postgres",
'dataSource.password':"postgres",
)
.packages(
'postgresql.extensions.micronaut'
)
.run(EmbeddedServer)
client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())
GormStorageObject object = new GormStorageObject(
job: 'irrelevant',
history: [date: '2019-05-26', activity:'Speakers dinner'],
favoriteNumbers: [1,2,3],
testAttributes: [happy:'path',funny:'yes']
)
HttpRequest request = HttpRequest.POST("/storage", object)
HttpResponse response = client.toBlocking().exchange(request, Map)
}
void "test index"() {
given:
HttpResponse response = client.toBlocking().exchange("/storage")
expect:
response.status == HttpStatus.OK
}
void "test filterByNumbers"() {
when:
def result = client.toBlocking().retrieve(HttpRequest.GET('/storage/numbers?numbers=2&numbers=3'), List)
println result
then:
result
result.size() == 1
result.first().favoriteNumbers == [1,2,3]
}
}
It is quite easy to integrate the postgresql extensions for GORM from the Grails plugin. Both if you would like the array extension, the hstore or the JSON extension.
As an added bonus, we have tested the service using testcontainers.
The version of Micronaut used is 1.1.2, and the code can be found in this github repo: https://github.com/JacobAae/postgresql-extensions-micronaut^
Thanks goes to Iván López for hints on issues that could arrise, and to Mathias Strange Hansen for doing the initial work at Cardlay A/S