Skip to content

Commit 1eb7193

Browse files
committed
Merge FR-003 Plan 2: binding registry + typed jsonb value-objects + Spring-tx connection
2 parents 26d196d + 1c8b325 commit 1eb7193

14 files changed

Lines changed: 989 additions & 5 deletions

File tree

server/java/core-spring/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@
3939
<version>6.2.11</version>
4040
</dependency>
4141

42+
<!-- Spring JDBC + TX (transaction-aware ObjectConnection bridge) -->
43+
<dependency>
44+
<groupId>org.springframework</groupId>
45+
<artifactId>spring-jdbc</artifactId>
46+
<version>6.2.11</version>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.springframework</groupId>
50+
<artifactId>spring-tx</artifactId>
51+
<version>6.2.11</version>
52+
</dependency>
53+
<!-- OMDB: ObjectConnectionDB base class for SpringObjectConnections -->
54+
<dependency>
55+
<groupId>com.metaobjects</groupId>
56+
<artifactId>metaobjects-omdb</artifactId>
57+
</dependency>
58+
4259
<!-- Test Dependencies -->
4360
<dependency>
4461
<groupId>junit</groupId>
@@ -59,6 +76,13 @@
5976
<type>test-jar</type>
6077
<scope>test</scope>
6178
</dependency>
79+
<!-- Embedded H2 for transaction integration tests -->
80+
<dependency>
81+
<groupId>com.h2database</groupId>
82+
<artifactId>h2</artifactId>
83+
<version>2.2.224</version>
84+
<scope>test</scope>
85+
</dependency>
6286
</dependencies>
6387

6488
<build>
@@ -79,6 +103,7 @@
79103
com.metaobjects.*,
80104
org.springframework.*,
81105
org.slf4j.*,
106+
javax.sql.*,
82107
*
83108
</Import-Package>
84109
</instructions>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.metaobjects.spring;
2+
3+
import com.metaobjects.manager.ObjectConnection;
4+
import com.metaobjects.manager.db.ObjectConnectionDB;
5+
import org.springframework.jdbc.datasource.DataSourceUtils;
6+
7+
import javax.sql.DataSource;
8+
import java.sql.Connection;
9+
10+
/**
11+
* Bridges OMDB to Spring-managed transactions.
12+
*
13+
* <p>{@link #current(DataSource)} returns an {@link ObjectConnection} backed by the SAME
14+
* physical {@link Connection} that Spring has bound to the current {@code @Transactional}
15+
* scope (via {@link DataSourceUtils#getConnection}). The wrapper's {@link ObjectConnection#close()}
16+
* is a no-op: Spring's transaction manager owns the connection lifecycle and will close it
17+
* when the transaction completes.</p>
18+
*
19+
* <p>If no transaction is active, {@link DataSourceUtils#getConnection} obtains a fresh
20+
* connection from the pool; callers are responsible for releasing it via
21+
* {@link DataSourceUtils#releaseConnection} in that case.</p>
22+
*/
23+
public final class SpringObjectConnections {
24+
25+
private SpringObjectConnections() {}
26+
27+
/**
28+
* Returns an {@link ObjectConnection} over the current Spring-bound connection for
29+
* {@code dataSource}. When a transaction is active the returned connection is the
30+
* tx-bound connection; otherwise a fresh connection is obtained from the pool.
31+
*
32+
* @param dataSource the {@link DataSource} whose Spring-bound connection to use
33+
* @return an {@link ObjectConnection} whose {@code close()} is a no-op
34+
*/
35+
public static ObjectConnection current(DataSource dataSource) {
36+
Connection c = DataSourceUtils.getConnection(dataSource);
37+
return new NonClosingObjectConnectionDB(c);
38+
}
39+
40+
/**
41+
* An {@link ObjectConnectionDB} whose {@code close()} is a no-op.
42+
* Spring's transaction manager closes the underlying connection at transaction end.
43+
*/
44+
static final class NonClosingObjectConnectionDB extends ObjectConnectionDB {
45+
46+
NonClosingObjectConnectionDB(Connection c) {
47+
super(c);
48+
}
49+
50+
/**
51+
* No-op — Spring owns the connection lifecycle.
52+
* The underlying connection will be closed by the transaction manager.
53+
*/
54+
@Override
55+
public void close() {
56+
// intentional no-op: Spring closes the connection at transaction end
57+
}
58+
}
59+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.metaobjects.spring;
2+
3+
import com.metaobjects.manager.ObjectConnection;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
11+
import org.springframework.jdbc.datasource.DataSourceUtils;
12+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
13+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
14+
import org.springframework.test.context.ContextConfiguration;
15+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
16+
import org.springframework.transaction.PlatformTransactionManager;
17+
import org.springframework.transaction.support.TransactionTemplate;
18+
19+
import javax.sql.DataSource;
20+
import java.sql.Connection;
21+
import java.sql.PreparedStatement;
22+
import java.sql.ResultSet;
23+
import java.sql.Statement;
24+
25+
import static org.junit.Assert.*;
26+
27+
/**
28+
* Verifies that SpringObjectConnections joins the caller's Spring-managed transaction:
29+
* <ol>
30+
* <li>The ObjectConnection wraps the SAME physical connection Spring bound to the tx.</li>
31+
* <li>close() on the wrapper is a no-op — Spring owns the lifecycle.</li>
32+
* <li>DML executed through the connection participates in Spring rollback.</li>
33+
* </ol>
34+
*/
35+
@RunWith(SpringJUnit4ClassRunner.class)
36+
@ContextConfiguration(classes = SpringObjectConnectionTest.TestConfig.class)
37+
public class SpringObjectConnectionTest {
38+
39+
@Configuration
40+
static class TestConfig {
41+
42+
@Bean
43+
public DataSource dataSource() {
44+
return new EmbeddedDatabaseBuilder()
45+
.setType(EmbeddedDatabaseType.H2)
46+
.build();
47+
}
48+
49+
@Bean
50+
public PlatformTransactionManager transactionManager(DataSource dataSource) {
51+
return new DataSourceTransactionManager(dataSource);
52+
}
53+
54+
@Bean
55+
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
56+
return new TransactionTemplate(transactionManager);
57+
}
58+
}
59+
60+
@Autowired
61+
private DataSource dataSource;
62+
63+
@Autowired
64+
private TransactionTemplate transactionTemplate;
65+
66+
@Before
67+
public void createTable() throws Exception {
68+
try (Connection c = dataSource.getConnection();
69+
Statement st = c.createStatement()) {
70+
st.execute("CREATE TABLE IF NOT EXISTS t (id INT PRIMARY KEY)");
71+
}
72+
}
73+
74+
/**
75+
* Test 1 — same connection + no-close.
76+
*
77+
* Inside a Spring transaction, SpringObjectConnections.current() must return the
78+
* exact same physical Connection that Spring bound to the transaction. After
79+
* calling close() on the wrapper the underlying connection must still be open
80+
* (Spring, not the wrapper, owns the lifecycle).
81+
*/
82+
@Test
83+
public void testJoinsTransactionAndCloseIsNoOp() throws Exception {
84+
transactionTemplate.execute(status -> {
85+
try {
86+
ObjectConnection oc = SpringObjectConnections.current(dataSource);
87+
Connection springConn = DataSourceUtils.getConnection(dataSource);
88+
89+
// The wrapped connection must be the same physical object Spring bound.
90+
Connection wrappedConn = (Connection) oc.getDatastoreConnection();
91+
assertSame(
92+
"SpringObjectConnections must return the tx-bound connection",
93+
springConn, wrappedConn);
94+
95+
// close() must be a no-op — Spring owns the lifecycle.
96+
oc.close();
97+
assertFalse(
98+
"Underlying connection must still be open after wrapper close()",
99+
wrappedConn.isClosed());
100+
101+
// Release the extra reference obtained above (DataSourceUtils contract).
102+
DataSourceUtils.releaseConnection(springConn, dataSource);
103+
} catch (Exception e) {
104+
throw new RuntimeException(e);
105+
}
106+
return null;
107+
});
108+
}
109+
110+
/**
111+
* Test 2 — rollback participation.
112+
*
113+
* DML executed via the connection obtained from SpringObjectConnections.current()
114+
* must be rolled back when the surrounding Spring transaction rolls back.
115+
* After the rollback the row must be absent.
116+
*/
117+
@Test
118+
public void testRollbackRemovesRow() throws Exception {
119+
// Execute inside a transaction that we force to roll back.
120+
try {
121+
transactionTemplate.execute(status -> {
122+
try {
123+
ObjectConnection oc = SpringObjectConnections.current(dataSource);
124+
Connection conn = (Connection) oc.getDatastoreConnection();
125+
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO t VALUES (1)")) {
126+
ps.executeUpdate();
127+
}
128+
} catch (Exception e) {
129+
throw new RuntimeException(e);
130+
}
131+
// Force rollback by throwing an unchecked exception.
132+
throw new RuntimeException("intentional rollback");
133+
});
134+
} catch (RuntimeException ignored) {
135+
// expected — transaction has been rolled back
136+
}
137+
138+
// Verify the row is absent on a fresh, non-transactional connection.
139+
try (Connection c = dataSource.getConnection();
140+
Statement st = c.createStatement();
141+
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM t WHERE id = 1")) {
142+
assertTrue(rs.next());
143+
assertEquals(
144+
"Row must be absent after rollback — OMDB connection participated in the Spring tx",
145+
0, rs.getInt(1));
146+
}
147+
}
148+
}

server/java/metadata/src/main/java/com/metaobjects/database/CoreDBMetaDataProvider.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public class CoreDBMetaDataProvider implements MetaDataTypeProvider {
3939
public static final String DB_PRECISION = "dbPrecision";
4040
public static final String DB_SCALE = "dbScale";
4141
public static final String DB_AUTO_INCREMENT = "dbAutoIncrement";
42+
public static final String DB_TYPE = "dbType";
43+
44+
/** {@code @dbType} value that marks a field as a JSON document column. */
45+
public static final String DB_TYPE_JSONB = "jsonb";
4246

4347
// Identity-specific database attributes
4448
public static final String DB_SEQUENCE_NAME = "dbSequenceName";
@@ -90,7 +94,8 @@ public static void registerDatabaseAttributes(MetaDataRegistry registry) {
9094
.optionalAttribute(DB_UNIQUE, BooleanAttribute.SUBTYPE_BOOLEAN)
9195
.optionalAttribute(DB_LENGTH, IntAttribute.SUBTYPE_INT)
9296
.optionalAttribute(DB_PRECISION, IntAttribute.SUBTYPE_INT)
93-
.optionalAttribute(DB_SCALE, IntAttribute.SUBTYPE_INT);
97+
.optionalAttribute(DB_SCALE, IntAttribute.SUBTYPE_INT)
98+
.optionalAttribute(DB_TYPE, StringAttribute.SUBTYPE_STRING);
9499

95100
// String field specific
96101
registry.findType(MetaField.TYPE_FIELD, StringAttribute.SUBTYPE_STRING)

server/java/metadata/src/main/java/com/metaobjects/object/MetaObject.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,20 +332,36 @@ protected Class<?> getObjectClassFromAttr() throws ClassNotFoundException {
332332
}
333333

334334
/**
335-
* Retrieves the object class of an object, or null if one is not specified
335+
* Retrieves the object class of an object, or null if one is not specified.
336+
*
337+
* <p>Resolution order (ADR-0001):</p>
338+
* <ol>
339+
* <li>{@code @object} attribute</li>
340+
* <li>Process-global {@link com.metaobjects.registry.ObjectClassRegistry} keyed by FQN</li>
341+
* <li>Name-convention: {@code pkg::Name} → {@code pkg.Name}</li>
342+
* </ol>
343+
*
344+
* <p><strong>Caching:</strong> the resolved class is cached per MetaObject instance.
345+
* The binding registry ({@link com.metaobjects.registry.ObjectClassRegistry#global()}) must
346+
* therefore be configured <em>before</em> the first call to this method on any given instance.
347+
* In production this holds naturally — the registry is discovered once at startup.
348+
* Tests that alter the global registry should use fresh MetaObject instances to avoid
349+
* observing a stale cached result.</p>
336350
*/
337351
public Class<?> getObjectClass() throws ClassNotFoundException {
338352

339353
final String CACHE_KEY = "getObjectClass()";
340354
Class<?> c = (Class<?>) getCacheValue(CACHE_KEY );
341355
if ( c == null ) {
342356

343-
c = null;
344-
345357
if (hasObjectAttr()) {
346358
c = getObjectClassFromAttr();
347359
}
348360

361+
if (c == null) {
362+
c = com.metaobjects.registry.ObjectClassRegistry.global().resolve(getName());
363+
}
364+
349365
if (c == null)
350366
c = createClassFromMetaDataName(true);
351367

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.metaobjects.registry;
2+
3+
import java.util.Map;
4+
5+
/** SPI: a domain slice's contribution of FQN -> Java class bindings (ADR-0001).
6+
* Implementations are discovered via ServiceLoader; codegen emits one per package. */
7+
@FunctionalInterface
8+
public interface ObjectClassBindingProvider {
9+
/** Canonical metadata FQN ("pkg::Name") -> the concrete Java class to instantiate. */
10+
Map<String, Class<?>> bindings();
11+
}

0 commit comments

Comments
 (0)