Skip to content

Commit 392ac28

Browse files
authored
Design, implement and document how an application can configure the product (#31)
1 parent edc0a09 commit 392ac28

36 files changed

+1066
-358
lines changed

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
# A MongoDB Dialect for the Hibernate ORM
1+
# MongoDB extension of Hibernate ORM
22

3-
This project aims to provide a library to seamlessly integrate MongoDB with Hibernate ORM. Hibernate _ORM_ is a powerful **O**bject-**r**elational **m**apping tool. Due to the SQL and JDBC standards, Hibernate ORM could centralize each SQL vendor's idiosyncrasies in the so-called _Hibernate Dialect_. This project will include a document database member in the Hibernate's Dialect family.
3+
This product enables applications to use databases managed by the [MongoDB](https://www.mongodb.com/) DBMS
4+
via [Hibernate ORM](https://hibernate.org/orm/).
45

56
## Overview
67

7-
MongoDB speaks _MQL_ (**M**ongoDB **Q**uery **L**anguage in JSON format) instead of SQL. This project creates a MongoDB Hibernate Dialect by:
8+
MongoDB speaks MQL (**M**ongoDB **Q**uery **L**anguage) instead of SQL. This product works by:
89

9-
- Creating a JDBC adapter using [MongoDB Java Driver](https://www.mongodb.com/docs/drivers/java-drivers/)
10-
- Translating Hibernate's internal SQL AST into MQL
10+
- Creating a JDBC adapter using [MongoDB Java Driver](https://www.mongodb.com/docs/drivers/java-drivers/),
11+
which has to be plugged into Hibernate ORM via a custom [`ConnectionProvider`](https://docs.jboss.org/hibernate/orm/6.6/javadocs/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.html).
12+
- Translating Hibernate's internal SQL AST into MQL by means of a custom [`Dialect`](https://docs.jboss.org/hibernate/orm/6.6/javadocs/org/hibernate/dialect/Dialect.html),
13+
which has to be plugged into Hibernate ORM.
1114

12-
<img src="mongodb_dialect.png" alt="MongoDB Dialect" />
15+
<img src="mongodb_dialect.png" alt="MongoDB extension" />
1316

1417
## Development
1518

1619
Java 17 is the JDK version for development.
1720

1821
Initially Hibernate ORM v6.6 is the dependency version.
1922

20-
MongoDB v6 is the minimal version this dialect supports.
23+
MongoDB v6.0 is the minimal version this product supports.
2124

2225
> [Standalone instance](https://www.mongodb.com/docs/manual/reference/glossary/#std-term-standalone) is not supported. It is recommended to [convert it to a replica set](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/).
2326

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,13 @@ buildConfig {
173173

174174
dependencies {
175175
testImplementation(libs.junit.jupiter)
176+
testImplementation(libs.assertj)
176177
testImplementation(libs.logback.classic)
177178
testImplementation(libs.mockito.junit.jupiter)
178179
testRuntimeOnly(libs.junit.platform.launcher)
179180

180181
integrationTestImplementation(libs.junit.jupiter)
182+
integrationTestImplementation(libs.assertj)
181183
integrationTestImplementation(libs.logback.classic)
182184

183185
@Suppress("UnstableApiUsage")

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
[versions]
55
junit-jupiter = "5.11.4"
6+
assertj = "3.27.3"
67
spotless = "7.0.2"
78
palantir = "2.50.0"
89
ktfmt = "0.54"
@@ -20,6 +21,7 @@ buildconfig = "5.5.1"
2021
[libraries]
2122
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
2223
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
24+
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
2325
nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" }
2426
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
2527
google-errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "google-errorprone-core" }

src/integrationTest/java/com/mongodb/hibernate/BasicInsertionIntegrationTests.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
package com.mongodb.hibernate;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20-
import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL;
20+
import static org.assertj.core.api.InstanceOfAssertFactories.LIST;
2121

22-
import com.mongodb.ConnectionString;
23-
import com.mongodb.MongoClientSettings;
2422
import com.mongodb.client.MongoClients;
2523
import com.mongodb.client.MongoCollection;
24+
import com.mongodb.hibernate.internal.cfg.MongoConfiguration;
25+
import com.mongodb.hibernate.internal.cfg.MongoConfigurationBuilder;
2626
import jakarta.persistence.Column;
2727
import jakarta.persistence.Embeddable;
2828
import jakarta.persistence.Entity;
@@ -32,7 +32,6 @@
3232
import java.util.List;
3333
import java.util.function.Consumer;
3434
import org.bson.BsonDocument;
35-
import org.hibernate.cfg.Configuration;
3635
import org.hibernate.testing.orm.junit.DomainModel;
3736
import org.hibernate.testing.orm.junit.SessionFactory;
3837
import org.hibernate.testing.orm.junit.SessionFactoryScope;
@@ -46,9 +45,11 @@
4645
BasicInsertionIntegrationTests.BookWithEmbeddedField.class
4746
})
4847
class BasicInsertionIntegrationTests {
48+
private static MongoConfiguration config;
4949

5050
@BeforeEach
51-
void setUp() {
51+
void setUp(SessionFactoryScope scope) {
52+
config = new MongoConfigurationBuilder(scope.getSessionFactory().getProperties()).build();
5253
onMongoCollection(MongoCollection::drop);
5354
}
5455

@@ -119,12 +120,8 @@ void testEntityWithEmbeddedFieldInsertion(SessionFactoryScope scope) {
119120
}
120121

121122
private void onMongoCollection(Consumer<MongoCollection<BsonDocument>> collectionConsumer) {
122-
var connectionString = new ConnectionString(new Configuration().getProperty(JAKARTA_JDBC_URL));
123-
try (var mongoClient = MongoClients.create(MongoClientSettings.builder()
124-
.applyConnectionString(connectionString)
125-
.build())) {
126-
var collection =
127-
mongoClient.getDatabase(connectionString.getDatabase()).getCollection("books", BsonDocument.class);
123+
try (var mongoClient = MongoClients.create(config.mongoClientSettings())) {
124+
var collection = mongoClient.getDatabase(config.databaseName()).getCollection("books", BsonDocument.class);
128125
collectionConsumer.accept(collection);
129126
}
130127
}
@@ -136,7 +133,7 @@ private List<BsonDocument> getCollectionDocuments() {
136133
}
137134

138135
private void assertCollectionContainsOnly(BsonDocument expectedDoc) {
139-
assertThat(getCollectionDocuments()).asList().singleElement().isEqualTo(expectedDoc);
136+
assertThat(getCollectionDocuments()).asInstanceOf(LIST).singleElement().isEqualTo(expectedDoc);
140137
}
141138

142139
@Entity(name = "Book")
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2025-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.hibernate;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
21+
import com.mongodb.client.MongoClient;
22+
import com.mongodb.client.MongoClients;
23+
import com.mongodb.hibernate.internal.cfg.MongoConfiguration;
24+
import com.mongodb.hibernate.internal.cfg.MongoConfigurationBuilder;
25+
import jakarta.persistence.Column;
26+
import jakarta.persistence.Entity;
27+
import jakarta.persistence.EntityManagerFactory;
28+
import jakarta.persistence.Id;
29+
import jakarta.persistence.Persistence;
30+
import jakarta.persistence.Table;
31+
import org.junit.jupiter.api.AfterAll;
32+
import org.junit.jupiter.api.BeforeAll;
33+
import org.junit.jupiter.api.Test;
34+
35+
class JakartaPersistenceBootstrappingIntegrationTests {
36+
private static EntityManagerFactory entityManagerFactory;
37+
private static MongoConfiguration config;
38+
private static MongoClient mongoClient;
39+
40+
@BeforeAll
41+
static void beforeAll() {
42+
entityManagerFactory = Persistence.createEntityManagerFactory("test-persistence-unit");
43+
config = new MongoConfigurationBuilder(entityManagerFactory.getProperties()).build();
44+
mongoClient = MongoClients.create(config.mongoClientSettings());
45+
}
46+
47+
@BeforeAll
48+
static void beforeEach() {
49+
mongoClient.getDatabase(config.databaseName()).drop();
50+
}
51+
52+
@AfterAll
53+
@SuppressWarnings("try")
54+
static void afterAll() {
55+
try (var closed1 = entityManagerFactory;
56+
var closed2 = mongoClient) {}
57+
}
58+
59+
@Test
60+
void smoke() {
61+
try (var entityManager = entityManagerFactory.createEntityManager()) {
62+
var transaction = entityManager.getTransaction();
63+
try {
64+
transaction.begin();
65+
var item = new Item();
66+
item.id = 1;
67+
entityManager.persist(item);
68+
} finally {
69+
transaction.commit();
70+
}
71+
assertEquals(
72+
1,
73+
mongoClient
74+
.getDatabase(config.databaseName())
75+
.getCollection("items")
76+
.countDocuments());
77+
}
78+
}
79+
80+
@Entity(name = "Item")
81+
@Table(name = "items")
82+
static class Item {
83+
@Id
84+
@Column(name = "_id")
85+
int id;
86+
}
87+
}

src/integrationTest/java/com/mongodb/hibernate/SessionFactoryIntegrationTests.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616

1717
package com.mongodb.hibernate;
1818

19-
import static org.hibernate.cfg.JdbcSettings.JAKARTA_JDBC_URL;
19+
import static org.hibernate.cfg.AvailableSettings.JAKARTA_JDBC_URL;
2020
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
21-
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
2221
import static org.junit.jupiter.api.Assertions.assertThrows;
2322

2423
import java.util.Map;
25-
import org.hibernate.HibernateException;
2624
import org.hibernate.SessionFactory;
2725
import org.hibernate.boot.MetadataSources;
2826
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
@@ -38,10 +36,9 @@ void testSuccess() {
3836

3937
@Test
4038
void testInvalidConnectionString() {
41-
var exception = assertThrows(ServiceException.class, () -> buildSessionFactory(
39+
assertThrows(ServiceException.class, () -> buildSessionFactory(
4240
Map.of(JAKARTA_JDBC_URL, "jdbc:postgresql://localhost/test"))
4341
.close());
44-
assertInstanceOf(HibernateException.class, exception.getCause());
4542
}
4643

4744
@Test
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<persistence
2+
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
5+
version="2.1">
6+
<persistence-unit name="test-persistence-unit">
7+
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
8+
<class>com.mongodb.hibernate.JakartaPersistenceBootstrappingIntegrationTests$Item</class>
9+
<properties><!-- rely on `hibernate.properties` --></properties>
10+
</persistence-unit>
11+
</persistence>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
jakarta.persistence.jdbc.url=mongodb://localhost/mongo-hibernate-test?directConnection=false
21
hibernate.dialect=com.mongodb.hibernate.dialect.MongoDialect
32
hibernate.connection.provider_class=com.mongodb.hibernate.jdbc.MongoConnectionProvider
3+
jakarta.persistence.jdbc.url=mongodb://localhost/mongo-hibernate-test?directConnection=false
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2024-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.hibernate.cfg;
18+
19+
import com.mongodb.ConnectionString;
20+
import com.mongodb.MongoClientSettings;
21+
import com.mongodb.hibernate.internal.Sealed;
22+
import com.mongodb.hibernate.service.spi.MongoConfigurationContributor;
23+
import java.util.Map;
24+
import java.util.function.Consumer;
25+
import org.hibernate.cfg.AvailableSettings;
26+
import org.hibernate.service.spi.Configurable;
27+
28+
/**
29+
* The configurator of the MongoDB extension of Hibernate ORM.
30+
*
31+
* <table>
32+
* <caption>Supported configuration properties</caption>
33+
* <thead>
34+
* <tr>
35+
* <th>Method</th>
36+
* <th>Has default</th>
37+
* <th>Related {@linkplain Configurable#configure(Map) configuration property} name</th>
38+
* <th>Supported value types of the configuration property</th>
39+
* <th>Value, unless overridden via {@link MongoConfigurator}</th>
40+
* </tr>
41+
* </thead>
42+
* <tbody>
43+
* <tr>
44+
* <td>{@link #applyToMongoClientSettings(Consumer)}</td>
45+
* <td>✓</td>
46+
* <td>{@value AvailableSettings#JAKARTA_JDBC_URL}</td>
47+
* <td>
48+
* <ul>
49+
* <li>{@link String}</li>
50+
* <li>{@link ConnectionString}</li>
51+
* </ul>
52+
* </td>
53+
* <td>Is {@linkplain MongoClientSettings.Builder#applyConnectionString(ConnectionString) based} on
54+
* the {@link ConnectionString} {@linkplain ConnectionString#ConnectionString(String) constructed} from
55+
* {@value AvailableSettings#JAKARTA_JDBC_URL}, if the latter is configured; otherwise a {@link MongoClientSettings}
56+
* instance with its defaults.</td>
57+
* </tr>
58+
* <tr>
59+
* <td>{@link #databaseName(String)}</td>
60+
* <td>✗</td>
61+
* <td>{@value AvailableSettings#JAKARTA_JDBC_URL}</td>
62+
* <td>
63+
* <ul>
64+
* <li>{@link String}</li>
65+
* <li>{@link ConnectionString}</li>
66+
* </ul>
67+
* </td>
68+
* <td>The MongoDB database name from {@value AvailableSettings#JAKARTA_JDBC_URL},
69+
* if {@linkplain ConnectionString#getDatabase() configured};
70+
* otherwise a value must be configured via {@link MongoConfigurator#databaseName(String)}.</td>
71+
* </tr>
72+
* </tbody>
73+
* </table>
74+
*
75+
* @see MongoConfigurationContributor
76+
*/
77+
@Sealed
78+
public interface MongoConfigurator {
79+
/**
80+
* Configures {@link MongoClientSettings}.
81+
*
82+
* <p>Note that if you {@linkplain MongoClientSettings.Builder#applyConnectionString(ConnectionString) apply} a
83+
* {@link ConnectionString} with the {@linkplain ConnectionString#getDatabase() database name} configured, you still
84+
* must configure the database name via {@link MongoConfigurator#databaseName(String)}, as there is no way for that
85+
* to happen automatically.
86+
*
87+
* @param configurator The {@link Consumer} of the {@link MongoClientSettings.Builder}.
88+
* @return {@code this}.
89+
*/
90+
MongoConfigurator applyToMongoClientSettings(Consumer<MongoClientSettings.Builder> configurator);
91+
92+
/**
93+
* Sets the name of a MongoDB database to use.
94+
*
95+
* @param databaseName The name of a MongoDB database to use.
96+
* @return {@code this}.
97+
*/
98+
MongoConfigurator databaseName(String databaseName);
99+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2024-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** Program elements related to configuring the MongoDB extension of Hibernate ORM. */
18+
@NullMarked
19+
package com.mongodb.hibernate.cfg;
20+
21+
import org.jspecify.annotations.NullMarked;

0 commit comments

Comments
 (0)