ModelQL
Independent ModelQLClient
ModelQL defines its own HTTP endpoint and provides server/client implementations for it.
The model-server
and the mps-model-server-plugin
already implement this endpoint.
The client can be created like this:
val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: List<String?> = client.query { root ->
root.children("modules").property("name").toList()
}
Integration with LightModelClient
When creating a LightModelClient
you can optionally provide a ModelQLClient
instance,
which allows invoking .query { … }
(see below) on a node returned by the LightModelClient
.
val modelqlClient = ModelQLClient.builder().build()
val client = LightModelClient.builder().modelQLClient(modelqlClient).build()
val result: List<String?> = client.getRootNode()!!.query {
it.children("modules").property("name").toList()
}
Type-safe ModelQL API
You can use the model-api-gen-gradle
plugin to generate type safe extensions from your meta-model.
Specify the modelqlKotlinDir property to enable the generation.
val result: List<StaticMethodDeclaration> = client.query { root ->
root.children("classes").ofConcept(C_ClassConcept)
.member
.ofConcept(C_StaticMethodDeclaration)
.filter { it.visibility.instanceOf(C_PublicVisibility) }
.toList()
}
Run query on an INode
If a query returns a node, you can execute a new query starting from that node.
val cls: ClassConcept = client.query {
it.children("classes").ofConcept(C_ClassConcept).first()
}
val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() }
For convenience, it’s possible to access further data of that node using the INode API, but this is not recommended though, because each access sends a new query to the server.
val cls: ClassConcept = client.query {
it.children("classes").ofConcept(C_ClassConcept).first()
}
val className = cls.name
Complex query results
While returning a list of elements is simple,
the purpose of the query language is to reduce the number of request to a minimum.
This requires combining multiple values into more complex data structures.
The zip
operation provides a simple way of doing that:
val result: List<IZip3Output<Any, Int, String, List<String>>> = query { db ->
db.products.map {
val id = it.id
val title = it.title
val images = it.images.toList()
id.zip(title, images)
}.toList()
}
result.forEach { println("ID: ${it.first}, Title: ${it.second}, Images: ${it.third}") }
This is suitable for combining a small number of values, but because of the missing variable names it can be hard to read for a larger number of values or even multiple zip operations assembled into a hierarchical data structure.
This can be solved by defining custom data classes and using the mapLocal
operation:
data class MyProduct(val id: Int, val title: String, val images: List<MyImage>)
data class MyImage(val url: String)
val result: List<MyProduct> = remoteProductDatabaseQuery { db ->
db.products.map {
val id = it.id
val title = it.title
val images = it.images.mapLocal { MyImage(it) }.toList()
id.zip(title, images).mapLocal {
MyProduct(it.first, it.second, it.third)
}
}.toList()
}
result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") }
The mapLocal
operation is not just useful in combination with the zip
operation,
but in general to create instances of classes only known to the client.
The body of mapLocal
is executed on the client after receiving the result from the server.
That’s why you only have access to the output of the zip
operation
and still have to use first
, second
and third
inside the query.
To make this even more readable there is a buildLocalMapping
operation,
which provides a different syntax for the zip
-mapLocal
chain.
data class MyProduct(val id: Int, val title: String, val images: List<MyImage>)
data class MyImage(val url: String)
val result: List<MyProduct> = query { db ->
db.products.buildLocalMapping {
val id = it.id.request()
val title = it.title.request()
val images = it.images.mapLocal { MyImage(it) }.toList().request()
onSuccess {
MyProduct(id.get(), title.get(), images.get())
}
}.toList()
}
result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") }
At the beginning of the buildLocalMapping
body, you invoke request()
on all the values you need to assemble your object.
This basically adds the operand to the internal zip
operation and returns an object that gives you access to the value
after receiving it from the server.
Inside the onSuccess
block you assemble the local object using the previously requested values.
Kotlin HTML integration
One use case of the query language is to build database applications that generate HTML pages from the data stored in the model server. You can use the Kotlin HTML DSL together with ModelQL to do that.
Use buildHtmlQuery
to request data from the server and render it into an HTML string:
val html = query {
it.map(buildHtmlQuery {
val modules = input.children("modules").requestFragment<_, FlowContent> {
val moduleName = input.property("name").request()
val models = input.children("models").requestFragment<_, FlowContent> {
val modelName = input.property("name").request()
onSuccess {
div {
h2 {
+"Model: ${modelName.get()}"
}
}
}
}
onSuccess {
div {
h1 {
+"Module: ${moduleName.get()}"
}
insertFragment(models)
}
}
}
onSuccess {
body {
insertFragment(modules)
}
}
})
}
buildHtmlQuery
and the requestFragment
operation are similar to the buildLocalMapping
operation,
but inside the onSuccess
block you use the Kotlin HTML DSL.