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:

val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build()
val result: List<String?> = client.query { root ->
    root.children("modules").property("name").toList()
}

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()