ModelQL
Independent ModelQLClient
ModelQL defines its own HTTP endpoint and provides server/client implementations for it. A model server already implements this endpoint.
It is recommended to use the ModelQL client integrated in to the model client V2 when working with the model server:
val client = ModelClientV2.builder()
.url("http://localhost:28101/v2")
.build()
val result: List<String?> = client.query { root ->
root.children("modules").property("name").toList()
}
An independent ModelQL client can be created like this:
This is only useful when some custom implementation of a ModelQL server needs to be used. |
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.
Indices/Caching
To search for a node efficiently, .find
can be used. Internally, it creates a map of all the elements and reuses that
in following queries.
val nodeId: String
root.find(
// Provides all nodes to index
{ it.descendants() },
// selects the index key for each node
{ it.nodeReferenceAsString() },
// The key to search for in the current request
nodeId.asMono()
)
It’s also possible to search for multiple nodes:
val name: String
root.findAll({ it.descendants().ofConcept(C_INamedConcept) }, { it.name }, name.asMono())
Internally, they both use the memoize
operation. memoize
stores the result of the query and reuses it without
re-executing the query.
The find
example is equivalent to this:
root.memoize { it.descendants().associateBy { it.nodeReferenceAsString() } }.get(nodeId.asMono()).filterNotNull()