Skip to content

Commit 2d4fde4

Browse files
committed
Merge: Kotlin OMDB facade (omdb-ktx) — transaction/Spring scopes, CRUD, query DSL, coroutine wrappers over the Java engine
2 parents 10d6bf5 + 15ee871 commit 2d4fde4

17 files changed

Lines changed: 1031 additions & 0 deletions

File tree

server/java/omdb-ktx/README.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# MetaObjects :: OMDB Kotlin Facade (`omdb-ktx`)
2+
3+
A thin Kotlin extension layer over the Java OMDB engine.
4+
No forking, no reimplementation — just idiomatic Kotlin syntax on top of the existing Java API.
5+
6+
## Dependency
7+
8+
```xml
9+
<dependency>
10+
<groupId>com.metaobjects</groupId>
11+
<artifactId>metaobjects-omdb-ktx</artifactId>
12+
<version>${project.version}</version>
13+
</dependency>
14+
```
15+
16+
---
17+
18+
## Before / After
19+
20+
### Transaction management
21+
22+
**Before** — raw Java engine:
23+
24+
```java
25+
ObjectConnection conn = omdb.getConnection();
26+
try {
27+
conn.setAutoCommit(false);
28+
omdb.createObject(conn, widget);
29+
conn.commit();
30+
} catch (Exception e) {
31+
conn.rollback();
32+
throw e;
33+
} finally {
34+
omdb.releaseConnection(conn);
35+
}
36+
```
37+
38+
**After** — Kotlin facade:
39+
40+
```kotlin
41+
omdb.transaction { session ->
42+
session.create(widget)
43+
}
44+
// Commits on normal return, rolls back and rethrows on any exception,
45+
// always releases the connection.
46+
```
47+
48+
The block's return value is propagated, so you can read and return in one call:
49+
50+
```kotlin
51+
val count: Int = omdb.transaction { session ->
52+
session.create(widget)
53+
42 // returned to the caller
54+
}
55+
```
56+
57+
---
58+
59+
### CRUD operations
60+
61+
```kotlin
62+
omdb.transaction { session ->
63+
session.create(widget) // INSERT
64+
session.update(widget) // UPDATE
65+
session.load(widget) // reload fields in-place
66+
session.delete(widget) // DELETE
67+
}
68+
```
69+
70+
---
71+
72+
### Querying with the DSL
73+
74+
```kotlin
75+
val widgetMeta: MetaObject = registry.findMetaObjectByName("acme::Widget")
76+
77+
val results: Collection<*> = omdb.transaction { session ->
78+
session.find(widgetMeta) {
79+
where(field("quantity") gte 10)
80+
orderByAsc("name")
81+
limit(50)
82+
}
83+
}
84+
```
85+
86+
Available infix operators on `field(name)`:
87+
`eq`, `ne`, `gt`, `lt`, `gte`, `lte`
88+
89+
Compound predicates use the `Expression` instance methods, which Kotlin can call as infix:
90+
91+
```kotlin
92+
where((field("quantity") gte 10) and (field("name") ne "archived"))
93+
```
94+
95+
`session.find` runs the query on the **session's own connection**, so writes made earlier
96+
in the same transaction are visible immediately (read-your-writes). This contrasts with
97+
`QueryBuilder.execute()`, which opens a new pool connection and cannot see uncommitted rows.
98+
99+
---
100+
101+
### Resolving an object by string reference
102+
103+
```kotlin
104+
val widget: Widget? = session.findByRef<Widget>("objectref://acme::Widget/42")
105+
```
106+
107+
Returns `null` if the reference does not exist. Name resolution (FQN → `MetaObject`) is
108+
delegated to the engine's global loader registry, which is populated during Spring/OSGi
109+
bootstrap. In a standalone context without active registry bindings this may throw
110+
`MetaDataNotFoundException` rather than return `null`.
111+
112+
---
113+
114+
### Spring-managed connections
115+
116+
When a `@Transactional` Spring method owns the connection lifecycle, use
117+
`withSpringConnection` instead of `transaction`. It does **not** commit, rollback, or
118+
close — the surrounding transaction owns all of that.
119+
120+
```kotlin
121+
@Service
122+
class WidgetService(
123+
private val omdb: ObjectManagerDB,
124+
private val dataSource: DataSource,
125+
private val widgetMeta: MetaObject,
126+
) {
127+
@Transactional
128+
fun createWidget(widget: Widget) {
129+
omdb.withSpringConnection(dataSource) { session ->
130+
session.create(widget)
131+
val others = session.find(widgetMeta) {
132+
where(field("name") eq widget.name)
133+
}
134+
// session uses the Spring-bound connection — sees the uncommitted create above
135+
}
136+
}
137+
}
138+
```
139+
140+
---
141+
142+
### Coroutines
143+
144+
`Coroutines.kt` provides suspend wrappers over the engine's async APIs:
145+
146+
```kotlin
147+
import com.metaobjects.omdb.ktx.awaitGetObjects
148+
import com.metaobjects.omdb.ktx.awaitExecute
149+
import kotlinx.coroutines.runBlocking
150+
151+
// Simple suspend fetch (no filter):
152+
val widgets: Collection<*> = runBlocking {
153+
omdb.awaitGetObjects(widgetMeta)
154+
}
155+
156+
// Filtered suspend query via QueryBuilder:
157+
val filtered: Collection<*> = runBlocking {
158+
omdb.query(widgetMeta)
159+
.where(field("quantity") gte 10)
160+
.awaitExecute()
161+
}
162+
```
163+
164+
Note: `awaitExecute()` calls `QueryBuilder.executeAsync()`, which opens a new connection
165+
from the pool. It does not share a session connection, so it cannot see uncommitted writes.
166+
Use `session.find(...)` inside a `transaction` block when read-your-writes semantics matter.
167+
168+
---
169+
170+
## Design notes
171+
172+
- **Thin wrapper, not a fork.** Every call delegates to the same Java engine methods
173+
(`createObject`, `updateObject`, `getObjects`, etc.). There is no reimplementation of
174+
persistence logic.
175+
- **`OmdbSession` is a value pair.** `class OmdbSession(val manager: ObjectManager, val connection: ObjectConnection)` — it exists solely so extension functions (`create`, `find`, …) can read naturally without requiring two separate parameters at every call site.
176+
- **`find` runs on the session connection.** `QueryBuilder.build()` produces `QueryOptions`; the facade then dispatches through `ObjectManager.getObjects(connection, metaObject, options)` on the session's own connection. This is the one place where the facade does more than forward a call — it selects the right overload to preserve transaction visibility.
177+
- **`findByRef` uses the engine's global registry.** The engine resolves a string reference to a `MetaObject` via whatever `MetaDataLoaderRegistry` bindings are active. In production (Spring/OSGi bootstrap) this works transparently; in a bare unit-test context you must register the relevant `MetaObject` yourself.

server/java/omdb-ktx/pom.xml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.metaobjects</groupId>
9+
<artifactId>metaobjects</artifactId>
10+
<version>7.0.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>metaobjects-omdb-ktx</artifactId>
14+
<name>MetaObjects :: OMDB Kotlin Facade</name>
15+
<description>Idiomatic Kotlin facade over the Java ObjectManagerDB engine.</description>
16+
17+
<properties>
18+
<kotlin.version>2.0.21</kotlin.version>
19+
<maven.compiler.release>21</maven.compiler.release>
20+
</properties>
21+
22+
<dependencies>
23+
<dependency>
24+
<groupId>org.jetbrains.kotlin</groupId>
25+
<artifactId>kotlin-stdlib</artifactId>
26+
<version>${kotlin.version}</version>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.jetbrains.kotlinx</groupId>
30+
<artifactId>kotlinx-coroutines-jdk8</artifactId>
31+
<version>1.9.0</version>
32+
</dependency>
33+
<dependency>
34+
<groupId>com.metaobjects</groupId>
35+
<artifactId>metaobjects-omdb</artifactId>
36+
<version>${project.version}</version>
37+
</dependency>
38+
<dependency>
39+
<groupId>com.metaobjects</groupId>
40+
<artifactId>metaobjects-core-spring</artifactId>
41+
<version>${project.version}</version>
42+
</dependency>
43+
44+
<dependency>
45+
<groupId>org.jetbrains.kotlin</groupId>
46+
<artifactId>kotlin-test-junit5</artifactId>
47+
<version>${kotlin.version}</version>
48+
<scope>test</scope>
49+
</dependency>
50+
<dependency>
51+
<groupId>org.junit.jupiter</groupId>
52+
<artifactId>junit-jupiter</artifactId>
53+
<version>5.10.2</version>
54+
<scope>test</scope>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.apache.derby</groupId>
58+
<artifactId>derby</artifactId>
59+
<version>${derby.version}</version>
60+
<scope>test</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.apache.derby</groupId>
64+
<artifactId>derbyshared</artifactId>
65+
<version>${derby.version}</version>
66+
<scope>test</scope>
67+
</dependency>
68+
<dependency>
69+
<groupId>org.apache.derby</groupId>
70+
<artifactId>derbytools</artifactId>
71+
<version>${derby.version}</version>
72+
<scope>test</scope>
73+
</dependency>
74+
</dependencies>
75+
76+
<build>
77+
<sourceDirectory>src/main/kotlin</sourceDirectory>
78+
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
79+
<plugins>
80+
<plugin>
81+
<groupId>org.jetbrains.kotlin</groupId>
82+
<artifactId>kotlin-maven-plugin</artifactId>
83+
<version>${kotlin.version}</version>
84+
<executions>
85+
<execution>
86+
<id>compile</id>
87+
<phase>compile</phase>
88+
<goals><goal>compile</goal></goals>
89+
</execution>
90+
<execution>
91+
<id>test-compile</id>
92+
<phase>test-compile</phase>
93+
<goals><goal>test-compile</goal></goals>
94+
</execution>
95+
</executions>
96+
<configuration>
97+
<jvmTarget>21</jvmTarget>
98+
</configuration>
99+
</plugin>
100+
<plugin>
101+
<groupId>org.apache.maven.plugins</groupId>
102+
<artifactId>maven-surefire-plugin</artifactId>
103+
<version>${surefire.plugin.version}</version>
104+
</plugin>
105+
</plugins>
106+
</build>
107+
</project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.metaobjects.omdb.ktx
2+
3+
import com.metaobjects.manager.ObjectManager
4+
import com.metaobjects.manager.QueryBuilder
5+
import com.metaobjects.`object`.MetaObject
6+
import kotlinx.coroutines.future.await
7+
8+
/** Suspend wrapper over [ObjectManager.getObjectsAsync]. */
9+
suspend fun ObjectManager.awaitGetObjects(mc: MetaObject): Collection<*> =
10+
getObjectsAsync(mc).await()
11+
12+
/** Suspend wrapper over [QueryBuilder.executeAsync]. */
13+
suspend fun QueryBuilder.awaitExecute(): Collection<*> =
14+
executeAsync().await()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.metaobjects.omdb.ktx
2+
3+
fun OmdbSession.create(obj: Any) = manager.createObject(connection, obj)
4+
fun OmdbSession.update(obj: Any) = manager.updateObject(connection, obj)
5+
fun OmdbSession.delete(obj: Any) = manager.deleteObject(connection, obj)
6+
fun OmdbSession.load(obj: Any) = manager.loadObject(connection, obj)
7+
8+
/**
9+
* Finds an object by its string reference, returning null if it does not exist.
10+
*
11+
* Name resolution (FQN -> MetaObject) is delegated to the engine, which in production relies on
12+
* the global loader registry populated during Spring/OSGi bootstrap. In a bare/standalone context
13+
* (no active ServiceRegistryFactory bindings) this can throw MetaDataNotFoundException rather than
14+
* return null. The cast to [T] is unchecked — callers are responsible for passing the correct type.
15+
*/
16+
@Suppress("UNCHECKED_CAST")
17+
fun <T : Any> OmdbSession.findByRef(refStr: String): T? =
18+
manager.findObjectByRef(connection, refStr).orElse(null) as T?
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.metaobjects.omdb.ktx
2+
3+
import com.metaobjects.manager.QueryBuilder
4+
import com.metaobjects.manager.exp.Expression
5+
import com.metaobjects.`object`.MetaObject
6+
7+
/**
8+
* A thin wrapper around a field name that provides infix operator syntax for building
9+
* [Expression]s without coupling the call site to raw constructor calls.
10+
*/
11+
@JvmInline
12+
value class FieldRef(val name: String)
13+
14+
/** Factory function: `field("quantity")` */
15+
fun field(name: String) = FieldRef(name)
16+
17+
infix fun FieldRef.eq(value: Any?) = Expression(name, value, Expression.EQUAL)
18+
infix fun FieldRef.ne(value: Any?) = Expression(name, value, Expression.NOT_EQUAL)
19+
infix fun FieldRef.gt(value: Any?) = Expression(name, value, Expression.GREATER)
20+
infix fun FieldRef.lt(value: Any?) = Expression(name, value, Expression.LESSER)
21+
infix fun FieldRef.gte(value: Any?) = Expression(name, value, Expression.EQUAL_GREATER)
22+
infix fun FieldRef.lte(value: Any?) = Expression(name, value, Expression.EQUAL_LESSER)
23+
24+
// Note: Expression already has instance methods `and(Expression)` and `or(Expression)`.
25+
// Kotlin treats any single-parameter method as a valid infix call, so callers can write
26+
// `(field("a") eq 1) and (field("b") gt 2)` without any additional declarations here.
27+
28+
/**
29+
* Executes a query on the **session's own connection** using a fluent [QueryBuilder] DSL.
30+
*
31+
* Connection semantics — read-your-writes within a transaction:
32+
* [QueryBuilder.execute] opens a *new* connection from the manager's pool, which means
33+
* it cannot see writes made on the current session connection that have not yet been
34+
* committed. This extension avoids that pitfall by building the [QueryOptions] via
35+
* [QueryBuilder.build] and then dispatching through
36+
* [ObjectManager.getObjects(ObjectConnection, MetaObject, QueryOptions)], which runs on
37+
* `this.connection` — the same connection that already holds the session's in-progress writes.
38+
*
39+
* @param metaObject the metamodel descriptor for the entity type to query
40+
* @param configure optional DSL block applied to the [QueryBuilder] before execution
41+
* @return the collection of matching objects (untyped — cast elements as needed)
42+
*/
43+
fun OmdbSession.find(metaObject: MetaObject, configure: QueryBuilder.() -> Unit = {}): Collection<*> {
44+
val options = manager.query(metaObject).apply(configure).build()
45+
return manager.getObjects(connection, metaObject, options)
46+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.metaobjects.omdb.ktx
2+
3+
import com.metaobjects.manager.ObjectManager
4+
import com.metaobjects.manager.ObjectConnection
5+
6+
/** Bundles the manager + an open connection so extension functions read naturally. */
7+
class OmdbSession(val manager: ObjectManager, val connection: ObjectConnection)

0 commit comments

Comments
 (0)