|
| 1 | += ModelQL |
| 2 | + |
| 3 | +== Independent ModelQLClient |
| 4 | + |
| 5 | +ModelQL defines its own HTTP endpoint and provides server/client implementations for it. |
| 6 | +The `model-server` and the `mps-model-server-plugin` already implement this endpoint. |
| 7 | +The client can be created like this: |
| 8 | + |
| 9 | +[source,kotlin] |
| 10 | +-- |
| 11 | +val client = ModelQLClient.builder().url("http://localhost/query").httpClient(httpClient).build() |
| 12 | +val result: List<String?> = client.query { root -> |
| 13 | + root.children("modules").property("name").toList() |
| 14 | +} |
| 15 | +-- |
| 16 | + |
| 17 | +== Integration with LightModelClient |
| 18 | + |
| 19 | +When creating a `LightModelClient` you can optionally provide a `ModelQLClient` instance, |
| 20 | +which allows invoking `.query { ... }` (see below) on a node returned by the `LightModelClient`. |
| 21 | + |
| 22 | +[source,kotlin] |
| 23 | +-- |
| 24 | +val modelqlClient = ModelQLClient.builder().build() |
| 25 | +val client = LightModelClient.builder().modelQLClient(modelqlClient).build() |
| 26 | +val result: List<String?> = client.getRootNode()!!.query { |
| 27 | + it.children("modules").property("name").toList() |
| 28 | +} |
| 29 | +-- |
| 30 | + |
| 31 | +== Type-safe ModelQL API |
| 32 | + |
| 33 | +You can use the `model-api-gen-gradle` plugin to generate type safe extensions from your meta-model. |
| 34 | +Specify the link:../reference/component-model-api-gen-gradle.adoc#model-api-gen-gradle_attributes_modelqlKotlinDir[modelqlKotlinDir] property to enable the generation. |
| 35 | + |
| 36 | +[source,kotlin] |
| 37 | +-- |
| 38 | +val result: List<StaticMethodDeclaration> = client.query { root -> |
| 39 | + root.children("classes").ofConcept(C_ClassConcept) |
| 40 | + .member |
| 41 | + .ofConcept(C_StaticMethodDeclaration) |
| 42 | + .filter { it.visibility.instanceOf(C_PublicVisibility) } |
| 43 | + .toList() |
| 44 | +} |
| 45 | +-- |
| 46 | + |
| 47 | +== Run query on an INode |
| 48 | + |
| 49 | +If a query returns a node, you can execute a new query starting from that node. |
| 50 | + |
| 51 | +[source,kotlin] |
| 52 | +-- |
| 53 | +val cls: ClassConcept = client.query { |
| 54 | + it.children("classes").ofConcept(C_ClassConcept).first() |
| 55 | +} |
| 56 | +val names = cls.query { it.member.ofConcept(C_StaticMethodDeclaration).name.toList() } |
| 57 | +-- |
| 58 | + |
| 59 | +For convenience, it's possible to access further data of that node using the https://api.modelix.org/3.6.0/model-api/org.modelix.model.api/-i-node/index.html?query=interface%20INode[INode] API, |
| 60 | +but this is not recommended though, because each access sends a new query to the server. |
| 61 | + |
| 62 | +[source,kotlin] |
| 63 | +-- |
| 64 | +val cls: ClassConcept = client.query { |
| 65 | + it.children("classes").ofConcept(C_ClassConcept).first() |
| 66 | +} |
| 67 | +val className = cls.name |
| 68 | +-- |
| 69 | + |
| 70 | +== Complex query results |
| 71 | + |
| 72 | +While returning a list of elements is simple, |
| 73 | +the purpose of the query language is to reduce the number of request to a minimum. |
| 74 | +This requires combining multiple values into more complex data structures. |
| 75 | +The `zip` operation provides a simple way of doing that: |
| 76 | + |
| 77 | +[source,kotlin] |
| 78 | +-- |
| 79 | +val result: List<IZip3Output<Any, Int, String, List<String>>> = query { db -> |
| 80 | + db.products.map { |
| 81 | + val id = it.id |
| 82 | + val title = it.title |
| 83 | + val images = it.images.toList() |
| 84 | + id.zip(title, images) |
| 85 | + }.toList() |
| 86 | +} |
| 87 | +result.forEach { println("ID: ${it.first}, Title: ${it.second}, Images: ${it.third}") } |
| 88 | +-- |
| 89 | + |
| 90 | +This is suitable for combining a small number of values, |
| 91 | +but because of the missing variable names it can be hard to read for a larger number of values |
| 92 | +or even multiple zip operations assembled into a hierarchical data structure. |
| 93 | + |
| 94 | +This can be solved by defining custom data classes and using the `mapLocal` operation: |
| 95 | + |
| 96 | +[source,kotlin] |
| 97 | +-- |
| 98 | +data class MyProduct(val id: Int, val title: String, val images: List<MyImage>) |
| 99 | +data class MyImage(val url: String) |
| 100 | + |
| 101 | +val result: List<MyProduct> = remoteProductDatabaseQuery { db -> |
| 102 | + db.products.map { |
| 103 | + val id = it.id |
| 104 | + val title = it.title |
| 105 | + val images = it.images.mapLocal { MyImage(it) }.toList() |
| 106 | + id.zip(title, images).mapLocal { |
| 107 | + MyProduct(it.first, it.second, it.third) |
| 108 | + } |
| 109 | + }.toList() |
| 110 | +} |
| 111 | +result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } |
| 112 | +-- |
| 113 | + |
| 114 | +The `mapLocal` operation is not just useful in combination with the `zip` operation, |
| 115 | +but in general to create instances of classes only known to the client. |
| 116 | + |
| 117 | +The body of `mapLocal` is executed on the client after receiving the result from the server. |
| 118 | +That's why you only have access to the output of the `zip` operation |
| 119 | +and still have to use `first`, `second` and `third` inside the query. |
| 120 | + |
| 121 | +To make this even more readable there is a `buildLocalMapping` operation, |
| 122 | +which provides a different syntax for the `zip`-`mapLocal` chain. |
| 123 | + |
| 124 | +[source,kotlin] |
| 125 | +-- |
| 126 | +data class MyProduct(val id: Int, val title: String, val images: List<MyImage>) |
| 127 | +data class MyImage(val url: String) |
| 128 | + |
| 129 | +val result: List<MyProduct> = query { db -> |
| 130 | + db.products.buildLocalMapping { |
| 131 | + val id = it.id.request() |
| 132 | + val title = it.title.request() |
| 133 | + val images = it.images.mapLocal { MyImage(it) }.toList().request() |
| 134 | + onSuccess { |
| 135 | + MyProduct(id.get(), title.get(), images.get()) |
| 136 | + } |
| 137 | + }.toList() |
| 138 | +} |
| 139 | +result.forEach { println("ID: ${it.id}, Title: ${it.title}, Images: ${it.images}") } |
| 140 | +-- |
| 141 | + |
| 142 | +At the beginning of the `buildLocalMapping` body, you invoke `request()` on all the values you need to assemble your object. |
| 143 | +This basically adds the operand to the internal `zip` operation and returns an object that gives you access to the value |
| 144 | +after receiving it from the server. |
| 145 | +Inside the `onSuccess` block you assemble the local object using the previously requested values. |
| 146 | + |
| 147 | +== Kotlin HTML integration |
| 148 | + |
| 149 | +One use case of the query language is to build database applications |
| 150 | +that generate HTML pages from the data stored in the model server. |
| 151 | +You can use the https://kotlinlang.org/docs/typesafe-html-dsl.html[Kotlin HTML DSL] together with ModelQL to do that. |
| 152 | + |
| 153 | +Use `buildHtmlQuery` to request data from the server and render it into an HTML string: |
| 154 | + |
| 155 | +[source,kotlin] |
| 156 | +-- |
| 157 | +val html = query { |
| 158 | + it.map(buildHtmlQuery { |
| 159 | + val modules = input.children("modules").requestFragment<_, FlowContent> { |
| 160 | + val moduleName = input.property("name").request() |
| 161 | + val models = input.children("models").requestFragment<_, FlowContent> { |
| 162 | + val modelName = input.property("name").request() |
| 163 | + onSuccess { |
| 164 | + div { |
| 165 | + h2 { |
| 166 | + +"Model: ${modelName.get()}" |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + onSuccess { |
| 172 | + div { |
| 173 | + h1 { |
| 174 | + +"Module: ${moduleName.get()}" |
| 175 | + } |
| 176 | + insertFragment(models) |
| 177 | + } |
| 178 | + } |
| 179 | + } |
| 180 | + onSuccess { |
| 181 | + body { |
| 182 | + insertFragment(modules) |
| 183 | + } |
| 184 | + } |
| 185 | + }) |
| 186 | +} |
| 187 | +-- |
| 188 | + |
| 189 | +`buildHtmlQuery` and the `requestFragment` operation are similar to the `buildLocalMapping` operation, |
| 190 | +but inside the `onSuccess` block you use the Kotlin HTML DSL. |
0 commit comments