From 8ac53b294e45a99bf2f224f4c71e3b11f20cbf48 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 17 Mar 2025 08:31:21 -0600 Subject: [PATCH 01/12] Add support for `ObjectId` --- .../id/ObjectIdFieldTypeIntegrationTests.java | 71 ++++++++++ .../hibernate/dialect/MongoDialect.java | 11 ++ .../hibernate/internal/type/MqlType.java | 84 ++++++++++++ .../internal/type/ObjectIdJavaType.java | 52 ++++++++ .../internal/type/ObjectIdJdbcType.java | 125 ++++++++++++++++++ .../hibernate/internal/type/package-info.java | 21 +++ .../jdbc/MongoPreparedStatement.java | 16 ++- .../hibernate/jdbc/MongoResultSet.java | 1 + 8 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/type/MqlType.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/type/package-info.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java new file mode 100644 index 00000000..4b206e61 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.id; + +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; +import static org.assertj.core.api.Assertions.assertThat; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.bson.BsonDocument; +import org.bson.BsonObjectId; +import org.bson.types.ObjectId; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel(annotatedClasses = {ObjectIdFieldTypeIntegrationTests.Item.class}) +@ExtendWith(MongoExtension.class) +class ObjectIdFieldTypeIntegrationTests implements SessionFactoryScopeAware { + @InjectMongoCollection("items") + private static MongoCollection mongoCollection; + + private SessionFactoryScope sessionFactoryScope; + + @Test + void insert() { + var id = new ObjectId(1, 0); + sessionFactoryScope.inTransaction(session -> { + var item = new Item(); + item.id = id; + session.persist(item); + }); + assertThat(mongoCollection.find()).containsExactly(new BsonDocument(ID_FIELD_NAME, new BsonObjectId(id))); + } + + // VAKOTODO add read tests + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Entity + @Table(name = "items") + static class Item { + @Id + ObjectId id; + } +} diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index c10ceb31..27e7845f 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -17,10 +17,14 @@ package com.mongodb.hibernate.dialect; import com.mongodb.hibernate.internal.translate.MongoTranslatorFactory; +import com.mongodb.hibernate.internal.type.ObjectIdJavaType; +import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import com.mongodb.hibernate.jdbc.MongoConnectionProvider; +import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslatorFactory; /** @@ -48,4 +52,11 @@ protected DatabaseVersion getMinimumSupportedVersion() { public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { return new MongoTranslatorFactory(); } + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contribute(typeContributions, serviceRegistry); + typeContributions.contributeJavaType(ObjectIdJavaType.INSTANCE); + typeContributions.contributeJdbcType(ObjectIdJdbcType.INSTANCE); + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java new file mode 100644 index 00000000..7ab6029a --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; + +import com.mongodb.hibernate.internal.MongoAssertions; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.SQLType; +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; +import org.hibernate.type.SqlTypes; + +public enum MqlType implements SQLType { + OBJECT_ID(11_000); + + static { + assertTrue(maxHibernateSqlType() < minType()); + } + + MqlType(int type) { + this.type = type; + } + + private final int type; + + @Override + public String getName() { + return name(); + } + + @Override + public String getVendor() { + return "MongoDB"; + } + + @Override + public Integer getVendorTypeNumber() { + return type; + } + + private static int minType() { + return Arrays.stream(MqlType.values()) + .mapToInt(MqlType::getVendorTypeNumber) + .min() + .orElseThrow(MongoAssertions::fail); + } + + private static int maxHibernateSqlType() { + Predicate publicStaticFinal = field -> { + var modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); + }; + ToIntFunction valueExtractor = field -> { + try { + return field.getInt(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + return Arrays.stream(SqlTypes.class.getDeclaredFields()) + .filter(field -> field.getType().equals(int.class)) + .filter(publicStaticFinal) + .mapToInt(valueExtractor) + .max() + .orElseThrow(MongoAssertions::fail); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java new file mode 100644 index 00000000..879bdfe9 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import java.io.Serial; +import org.bson.types.ObjectId; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +public final class ObjectIdJavaType extends AbstractClassJavaType { + @Serial + private static final long serialVersionUID = 1L; + + public static final ObjectIdJavaType INSTANCE = new ObjectIdJavaType(); + + private ObjectIdJavaType() { + super(ObjectId.class, ImmutableMutabilityPlan.instance()); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return ObjectIdJdbcType.INSTANCE; + } + + @Override + public X unwrap(ObjectId value, Class type, WrapperOptions options) { + throw new FeatureNotSupportedException(); + } + + @Override + public ObjectId wrap(X value, WrapperOptions options) { + throw new FeatureNotSupportedException(); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java new file mode 100644 index 00000000..1b3c66c6 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import java.io.Serial; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import org.bson.types.ObjectId; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +public final class ObjectIdJdbcType implements JdbcType { + @Serial + private static final long serialVersionUID = 1L; + + private static final MqlType MQL_TYPE = MqlType.OBJECT_ID; + private static final ObjectIdJavaType JAVA_TYPE = ObjectIdJavaType.INSTANCE; + public static final ObjectIdJdbcType INSTANCE = new ObjectIdJdbcType(); + + private ObjectIdJdbcType() {} + + @Override + public int getJdbcTypeCode() { + return MQL_TYPE.getVendorTypeNumber(); + } + + @Override + public String getFriendlyName() { + return MQL_TYPE.getName(); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if (!javaType.equals(JAVA_TYPE)) { + throw new FeatureNotSupportedException(); + } + @SuppressWarnings("unchecked") + var result = (ValueBinder) Binder.INSTANCE; + return result; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if (!javaType.equals(JAVA_TYPE)) { + throw new FeatureNotSupportedException(); + } + @SuppressWarnings("unchecked") + var result = (ValueExtractor) Extractor.INSTANCE; + return result; + } + + private static final class Binder extends BasicBinder { + @Serial + private static final long serialVersionUID = 1L; + + static Binder INSTANCE = new Binder(); + + private Binder() { + super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); + } + + @Override + protected void doBind(PreparedStatement st, ObjectId value, int index, WrapperOptions options) + throws SQLException { + st.setObject(index, value, getJdbcType().getJdbcTypeCode()); + } + + @Override + protected void doBind(CallableStatement st, ObjectId value, String name, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + } + + private static final class Extractor extends BasicExtractor { + @Serial + private static final long serialVersionUID = 1L; + + static Extractor INSTANCE = new Extractor(); + + private Extractor() { + super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); + } + + @Override + protected ObjectId doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return rs.getObject(paramIndex, getJavaType().getJavaTypeClass()); + } + + @Override + protected ObjectId doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + protected ObjectId doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/package-info.java b/src/main/java/com/mongodb/hibernate/internal/type/package-info.java new file mode 100644 index 00000000..51519547 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** The program elements within this package are not part of the public API and may be removed or changed at any time */ +@NullMarked +package com.mongodb.hibernate.internal.type; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index ccc3c789..33ce9065 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -21,6 +21,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; +import com.mongodb.hibernate.internal.type.MqlType; import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; @@ -46,10 +47,12 @@ import org.bson.BsonInt32; import org.bson.BsonInt64; import org.bson.BsonNull; +import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.BsonType; import org.bson.BsonValue; import org.bson.types.Decimal128; +import org.bson.types.ObjectId; final class MongoPreparedStatement extends MongoStatement implements PreparedStatementAdapter { @@ -186,9 +189,20 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + // VAKOTODO add tests checkClosed(); checkParameterIndex(parameterIndex); - throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + BsonValue value; + if (targetSqlType == MqlType.OBJECT_ID.getVendorTypeNumber()) { + if (x instanceof ObjectId v) { + value = new BsonObjectId(v); + } else { + throw fail(); + } + } else { + throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + } + setParameter(parameterIndex, value); } @Override diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index f0d0ee7e..0fdcfec2 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -202,6 +202,7 @@ public double getDouble(int columnIndex) throws SQLException { @Override public @Nullable T getObject(int columnIndex, Class type) throws SQLException { + // VAKOTODO implement reading and add MongoResultSet tests checkClosed(); checkColumnIndex(columnIndex); throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); From bff3df118230ed27856cd8212a5b237d6760e040 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 26 Mar 2025 18:54:00 -0600 Subject: [PATCH 02/12] Add `ObjectIdFieldTypeIntegrationTests.getById` --- .../id/ObjectIdFieldTypeIntegrationTests.java | 30 ++++++++++++++----- .../internal/type/ObjectIdJavaType.java | 3 ++ .../hibernate/jdbc/MongoResultSet.java | 11 +++++-- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java index 4b206e61..3ece45de 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java @@ -46,16 +46,28 @@ class ObjectIdFieldTypeIntegrationTests implements SessionFactoryScopeAware { @Test void insert() { - var id = new ObjectId(1, 0); - sessionFactoryScope.inTransaction(session -> { - var item = new Item(); - item.id = id; - session.persist(item); - }); - assertThat(mongoCollection.find()).containsExactly(new BsonDocument(ID_FIELD_NAME, new BsonObjectId(id))); + var item = new Item(); + item.id = new ObjectId(1, 0); + item.v = new ObjectId(2, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertThat(mongoCollection.find()) + .containsExactly(new BsonDocument(ID_FIELD_NAME, new BsonObjectId(item.id)) + .append("v", new BsonObjectId(item.v))); } - // VAKOTODO add read tests + @Test + void getById() { + var item = new Item(); + item.id = new ObjectId(1, 0); + item.v = new ObjectId(2, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); + assertThat(loadedItem) + .isNotNull() + .usingRecursiveComparison() + .withStrictTypeChecking() + .isEqualTo(item); + } @Override public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { @@ -67,5 +79,7 @@ public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { static class Item { @Id ObjectId id; + + ObjectId v; } } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java index 879bdfe9..a65e763e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -47,6 +47,9 @@ public X unwrap(ObjectId value, Class type, WrapperOptions options) { @Override public ObjectId wrap(X value, WrapperOptions options) { + if (value instanceof ObjectId wrapped) { + return wrapped; + } throw new FeatureNotSupportedException(); } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 0fdcfec2..4084e35b 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -51,6 +51,7 @@ import java.util.function.Function; import org.bson.BsonDocument; import org.bson.BsonValue; +import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; final class MongoResultSet implements ResultSetAdapter { @@ -202,10 +203,16 @@ public double getDouble(int columnIndex) throws SQLException { @Override public @Nullable T getObject(int columnIndex, Class type) throws SQLException { - // VAKOTODO implement reading and add MongoResultSet tests + // VAKOTODO add MongoResultSet tests checkClosed(); checkColumnIndex(columnIndex); - throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + Object value; + if (type.equals(ObjectId.class)) { + value = getValue(columnIndex, bsonValue -> bsonValue.asObjectId().getValue()); + } else { + throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + } + return type.cast(value); } @Override From fdefbd9aa9f17dd2707df681ec37219ffcc9f394 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 26 Mar 2025 20:21:39 -0600 Subject: [PATCH 03/12] Test `MongoPreparedStatement.setObject` --- .../id/ObjectIdFieldTypeIntegrationTests.java | 3 +- .../jdbc/MongoPreparedStatement.java | 1 - .../jdbc/MongoPreparedStatementTests.java | 68 +++++++++++-------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java index 3ece45de..70805974 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java @@ -51,7 +51,8 @@ void insert() { item.v = new ObjectId(2, 0); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertThat(mongoCollection.find()) - .containsExactly(new BsonDocument(ID_FIELD_NAME, new BsonObjectId(item.id)) + .containsExactly(new BsonDocument() + .append(ID_FIELD_NAME, new BsonObjectId(item.id)) .append("v", new BsonObjectId(item.v))); } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 33ce9065..200f9303 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -189,7 +189,6 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { - // VAKOTODO add tests checkClosed(); checkParameterIndex(parameterIndex); BsonValue value; diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 34c268ea..c62eeda7 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -35,6 +35,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; +import com.mongodb.hibernate.internal.type.MqlType; import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; @@ -45,9 +46,16 @@ import java.sql.Timestamp; import java.sql.Types; import java.util.Calendar; +import java.util.List; import java.util.function.Consumer; +import org.bson.BsonArray; +import org.bson.BsonBoolean; import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonObjectId; +import org.bson.BsonString; import org.bson.Document; +import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -79,16 +87,18 @@ private MongoPreparedStatement createMongoPreparedStatement(String mql) throws S private static final String EXAMPLE_MQL = """ { - insert: "books", + insert: "items", documents: [ { - title: { $undefined: true }, - author: { $undefined: true }, - publishYear: { $undefined: true }, - outOfStock: { $undefined: true }, - tags: [ + string1: { $undefined: true }, + string2: { $undefined: true }, + int32: { $undefined: true }, + boolean: { $undefined: true }, + stringAndObjectId: [ + { $undefined: true }, { $undefined: true } ] + objectId: { $undefined: true } } ] } @@ -110,33 +120,33 @@ void testSuccess() throws SQLException { try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { - preparedStatement.setString(1, "War and Peace"); - preparedStatement.setString(2, "Leo Tolstoy"); - preparedStatement.setInt(3, 1869); - preparedStatement.setBoolean(4, false); - preparedStatement.setString(5, "classic"); + preparedStatement.setString(1, "s1"); + preparedStatement.setString(2, "s2"); + preparedStatement.setInt(3, 1); + preparedStatement.setBoolean(4, true); + preparedStatement.setString(5, "array element"); + preparedStatement.setObject(6, new ObjectId(1, 2), MqlType.OBJECT_ID.getVendorTypeNumber()); + preparedStatement.setObject(7, new ObjectId(2, 0), MqlType.OBJECT_ID.getVendorTypeNumber()); preparedStatement.executeUpdate(); verify(mongoDatabase).runCommand(eq(clientSession), commandCaptor.capture()); var command = commandCaptor.getValue(); - var expectedDoc = BsonDocument.parse( - """ - { - insert: "books", - documents: [ - { - title: "War and Peace", - author: "Leo Tolstoy", - publishYear: 1869, - outOfStock: false, - tags: [ - "classic" - ] - } - ] - } - """); + var expectedDoc = new BsonDocument() + .append("insert", new BsonString("items")) + .append( + "documents", + new BsonArray(List.of(new BsonDocument() + .append("string1", new BsonString("s1")) + .append("string2", new BsonString("s2")) + .append("int32", new BsonInt32(1)) + .append("boolean", BsonBoolean.TRUE) + .append( + "stringAndObjectId", + new BsonArray(List.of( + new BsonString("array element"), + new BsonObjectId(new ObjectId(1, 2))))) + .append("objectId", new BsonObjectId(new ObjectId(2, 0)))))); assertEquals(expectedDoc, command); } } @@ -151,7 +161,7 @@ void testParameterIndexUnderflow() throws SQLSyntaxErrorException { @Test void testParameterIndexOverflow() throws SQLSyntaxErrorException { var mongoPreparedStatement = createMongoPreparedStatement(EXAMPLE_MQL); - checkSetterMethods(mongoPreparedStatement, 6, MongoPreparedStatementTests::assertThrowsOutOfRangeException); + checkSetterMethods(mongoPreparedStatement, 8, MongoPreparedStatementTests::assertThrowsOutOfRangeException); } @Nested From b9346b8c1f6651c09b66f79294dd203205bfeb4f Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 26 Mar 2025 22:20:25 -0600 Subject: [PATCH 04/12] Test `MongoResultSet.getObject` --- .../hibernate/jdbc/MongoResultSet.java | 1 - .../hibernate/jdbc/MongoResultSetTests.java | 58 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 4084e35b..96ddc440 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -203,7 +203,6 @@ public double getDouble(int columnIndex) throws SQLException { @Override public @Nullable T getObject(int columnIndex, Class type) throws SQLException { - // VAKOTODO add MongoResultSet tests checkClosed(); checkColumnIndex(columnIndex); Object value; diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java index a23369cd..d02406ff 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java @@ -41,9 +41,11 @@ import org.bson.BsonInt32; import org.bson.BsonInt64; import org.bson.BsonNull; +import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.types.Decimal128; +import org.bson.types.ObjectId; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -106,7 +108,10 @@ void testGettersForNull() throws SQLException { () -> assertEquals(0, mongoResultSet.getInt(1)), () -> assertEquals(0L, mongoResultSet.getLong(1)), () -> assertEquals(0D, mongoResultSet.getDouble(1)), - () -> assertNull(mongoResultSet.getBigDecimal(1))); + () -> assertNull(mongoResultSet.getBytes(1)), + () -> assertNull(mongoResultSet.getBigDecimal(1)), + () -> assertNull(mongoResultSet.getObject(1, ObjectId.class)), + () -> assertTrue(mongoResultSet.wasNull())); } @Test @@ -118,7 +123,10 @@ void testGettersForBoolean() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -130,7 +138,10 @@ void testGettersForDouble() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertEquals(3.1415, mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -142,7 +153,10 @@ void testGettersForInt() throws SQLException { () -> assertEquals(120, mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -154,7 +168,10 @@ void testGettersForLong() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertEquals(12345678L, mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -166,7 +183,10 @@ void testGettersForString() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -180,7 +200,10 @@ void testGettersForBigDecimal() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertEquals(bigDecimalValue, mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertEquals(bigDecimalValue, mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -194,8 +217,27 @@ void testGettersForBinary() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), + () -> assertEquals(bytes, mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); + } + + @Test + void testGettersForObject() throws SQLException { + var objectId = new ObjectId(1, 0); + var value = new BsonObjectId(objectId); + createResultSetWith(value); + assertAll( + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getString(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBoolean(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), - () -> assertEquals(bytes, mongoResultSet.getBytes(1))); + () -> assertEquals(objectId, mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } } From effeb17738f23ab45e657583d0f169b8d2801c7a Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 26 Mar 2025 22:34:15 -0600 Subject: [PATCH 05/12] Add internal docs --- .../mongodb/hibernate/internal/type/ObjectIdJdbcType.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java index 1b3c66c6..106c756d 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -72,6 +72,9 @@ public ValueExtractor getExtractor(JavaType javaType) { return result; } + /** + * Thread-safe. + */ private static final class Binder extends BasicBinder { @Serial private static final long serialVersionUID = 1L; @@ -95,6 +98,9 @@ protected void doBind(CallableStatement st, ObjectId value, String name, Wrapper } } + /** + * Thread-safe. + */ private static final class Extractor extends BasicExtractor { @Serial private static final long serialVersionUID = 1L; From 33d95e365939a884a444e5255e4d07c7b9799356 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 26 Mar 2025 23:25:07 -0600 Subject: [PATCH 06/12] Do minor improvements --- .../mongodb/hibernate/internal/type/ObjectIdJdbcType.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java index 106c756d..10f03fb5 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -36,9 +36,9 @@ public final class ObjectIdJdbcType implements JdbcType { @Serial private static final long serialVersionUID = 1L; + public static final ObjectIdJdbcType INSTANCE = new ObjectIdJdbcType(); private static final MqlType MQL_TYPE = MqlType.OBJECT_ID; private static final ObjectIdJavaType JAVA_TYPE = ObjectIdJavaType.INSTANCE; - public static final ObjectIdJdbcType INSTANCE = new ObjectIdJdbcType(); private ObjectIdJdbcType() {} @@ -79,7 +79,7 @@ private static final class Binder extends BasicBinder { @Serial private static final long serialVersionUID = 1L; - static Binder INSTANCE = new Binder(); + static final Binder INSTANCE = new Binder(); private Binder() { super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); @@ -105,7 +105,7 @@ private static final class Extractor extends BasicExtractor { @Serial private static final long serialVersionUID = 1L; - static Extractor INSTANCE = new Extractor(); + static final Extractor INSTANCE = new Extractor(); private Extractor() { super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); From 4dbfcaaf44f601b843f2b84abf181f66583d4bbc Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Sun, 30 Mar 2025 13:53:57 -0600 Subject: [PATCH 07/12] Move the `"MongoDB"` literal from `MqlType` to `MongoConstants` --- .../java/com/mongodb/hibernate/internal/MongoConstants.java | 1 + src/main/java/com/mongodb/hibernate/internal/type/MqlType.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index 27068bc6..73bbb921 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -20,5 +20,6 @@ public final class MongoConstants { private MongoConstants() {} + public static final String MONGO_DBMS_NAME = "MongoDB"; public static final String ID_FIELD_NAME = "_id"; } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java index 7ab6029a..76ccc615 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java @@ -17,6 +17,7 @@ package com.mongodb.hibernate.internal.type; import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; +import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; import com.mongodb.hibernate.internal.MongoAssertions; import java.lang.reflect.Field; @@ -47,7 +48,7 @@ public String getName() { @Override public String getVendor() { - return "MongoDB"; + return MONGO_DBMS_NAME; } @Override From d0fd67266328e8530d2a0c8b979e0cfa1ed3ef8f Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Sun, 30 Mar 2025 14:34:21 -0600 Subject: [PATCH 08/12] Rename `type` -> `code` --- .../mongodb/hibernate/internal/type/MqlType.java | 14 +++++++------- .../hibernate/internal/type/ObjectIdJdbcType.java | 8 ++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java index 76ccc615..c3486c78 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java @@ -32,14 +32,14 @@ public enum MqlType implements SQLType { OBJECT_ID(11_000); static { - assertTrue(maxHibernateSqlType() < minType()); + assertTrue(maxHibernateSqlTypeCode() < minMqlTypeCode()); } - MqlType(int type) { - this.type = type; + MqlType(int code) { + this.code = code; } - private final int type; + private final int code; @Override public String getName() { @@ -53,17 +53,17 @@ public String getVendor() { @Override public Integer getVendorTypeNumber() { - return type; + return code; } - private static int minType() { + private static int minMqlTypeCode() { return Arrays.stream(MqlType.values()) .mapToInt(MqlType::getVendorTypeNumber) .min() .orElseThrow(MongoAssertions::fail); } - private static int maxHibernateSqlType() { + private static int maxHibernateSqlTypeCode() { Predicate publicStaticFinal = field -> { var modifiers = field.getModifiers(); return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java index 10f03fb5..ac1a13da 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -72,9 +72,7 @@ public ValueExtractor getExtractor(JavaType javaType) { return result; } - /** - * Thread-safe. - */ + /** Thread-safe. */ private static final class Binder extends BasicBinder { @Serial private static final long serialVersionUID = 1L; @@ -98,9 +96,7 @@ protected void doBind(CallableStatement st, ObjectId value, String name, Wrapper } } - /** - * Thread-safe. - */ + /** Thread-safe. */ private static final class Extractor extends BasicExtractor { @Serial private static final long serialVersionUID = 1L; From d11dbcffda0eb8f56a5f18d7138db9e6361b7bd6 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 31 Mar 2025 06:47:27 -0600 Subject: [PATCH 09/12] Rely on the default `MutabilityPlan` --- .../com/mongodb/hibernate/internal/type/ObjectIdJavaType.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java index a65e763e..4b7084f8 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -21,7 +21,6 @@ import org.bson.types.ObjectId; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.AbstractClassJavaType; -import org.hibernate.type.descriptor.java.ImmutableMutabilityPlan; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -32,7 +31,7 @@ public final class ObjectIdJavaType extends AbstractClassJavaType { public static final ObjectIdJavaType INSTANCE = new ObjectIdJavaType(); private ObjectIdJavaType() { - super(ObjectId.class, ImmutableMutabilityPlan.instance()); + super(ObjectId.class); } @Override From f810311b747227480721d73d179089c7c2d401b0 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 31 Mar 2025 07:12:09 -0600 Subject: [PATCH 10/12] Separate integration tests --- .../id/ObjectIdAsIdIntegrationTests.java | 77 +++++++++++++++++++ .../ObjectIdIntegrationTests.java} | 16 ++-- 2 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java rename src/integrationTest/java/com/mongodb/hibernate/{id/ObjectIdFieldTypeIntegrationTests.java => type/ObjectIdIntegrationTests.java} (87%) diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java new file mode 100644 index 00000000..871602ab --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.id; + +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.bson.BsonDocument; +import org.bson.BsonObjectId; +import org.bson.types.ObjectId; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel(annotatedClasses = {ObjectIdAsIdIntegrationTests.Item.class}) +@ExtendWith(MongoExtension.class) +class ObjectIdAsIdIntegrationTests implements SessionFactoryScopeAware { + @InjectMongoCollection("items") + private static MongoCollection mongoCollection; + + private SessionFactoryScope sessionFactoryScope; + + @Test + void insert() { + var item = new Item(); + item.id = new ObjectId(1, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertThat(mongoCollection.find()) + .containsExactly(new BsonDocument().append(ID_FIELD_NAME, new BsonObjectId(item.id))); + } + + @Test + void getById() { + var item = new Item(); + item.id = new ObjectId(1, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); + assertEquals(item.id, loadedItem.id); + } + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Entity + @Table(name = "items") + static class Item { + @Id + ObjectId id; + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java similarity index 87% rename from src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java rename to src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java index 70805974..cc463f71 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdFieldTypeIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb.hibernate.id; +package com.mongodb.hibernate.type; import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static org.assertj.core.api.Assertions.assertThat; @@ -26,6 +26,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import org.bson.BsonDocument; +import org.bson.BsonInt32; import org.bson.BsonObjectId; import org.bson.types.ObjectId; import org.hibernate.testing.orm.junit.DomainModel; @@ -36,9 +37,9 @@ import org.junit.jupiter.api.extension.ExtendWith; @SessionFactory(exportSchema = false) -@DomainModel(annotatedClasses = {ObjectIdFieldTypeIntegrationTests.Item.class}) +@DomainModel(annotatedClasses = {ObjectIdIntegrationTests.Item.class}) @ExtendWith(MongoExtension.class) -class ObjectIdFieldTypeIntegrationTests implements SessionFactoryScopeAware { +class ObjectIdIntegrationTests implements SessionFactoryScopeAware { @InjectMongoCollection("items") private static MongoCollection mongoCollection; @@ -47,24 +48,23 @@ class ObjectIdFieldTypeIntegrationTests implements SessionFactoryScopeAware { @Test void insert() { var item = new Item(); - item.id = new ObjectId(1, 0); + item.id = 1; item.v = new ObjectId(2, 0); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertThat(mongoCollection.find()) .containsExactly(new BsonDocument() - .append(ID_FIELD_NAME, new BsonObjectId(item.id)) + .append(ID_FIELD_NAME, new BsonInt32(1)) .append("v", new BsonObjectId(item.v))); } @Test void getById() { var item = new Item(); - item.id = new ObjectId(1, 0); + item.id = 1; item.v = new ObjectId(2, 0); sessionFactoryScope.inTransaction(session -> session.persist(item)); var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); assertThat(loadedItem) - .isNotNull() .usingRecursiveComparison() .withStrictTypeChecking() .isEqualTo(item); @@ -79,7 +79,7 @@ public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { @Table(name = "items") static class Item { @Id - ObjectId id; + int id; ObjectId v; } From 2f15c79f317826c6b773120fe7e3a56c42c529ae Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 31 Mar 2025 08:44:04 -0600 Subject: [PATCH 11/12] Test `null` as a value of `ObjectId` --- .../type/ObjectIdIntegrationTests.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java index cc463f71..0a85e5a8 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java @@ -18,6 +18,7 @@ import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.mongodb.client.MongoCollection; import com.mongodb.hibernate.junit.InjectMongoCollection; @@ -25,8 +26,10 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.util.Objects; import org.bson.BsonDocument; import org.bson.BsonInt32; +import org.bson.BsonNull; import org.bson.BsonObjectId; import org.bson.types.ObjectId; import org.hibernate.testing.orm.junit.DomainModel; @@ -54,7 +57,8 @@ void insert() { assertThat(mongoCollection.find()) .containsExactly(new BsonDocument() .append(ID_FIELD_NAME, new BsonInt32(1)) - .append("v", new BsonObjectId(item.v))); + .append("v", new BsonObjectId(item.v)) + .append("vNull", BsonNull.VALUE)); } @Test @@ -64,10 +68,7 @@ void getById() { item.v = new ObjectId(2, 0); sessionFactoryScope.inTransaction(session -> session.persist(item)); var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); - assertThat(loadedItem) - .usingRecursiveComparison() - .withStrictTypeChecking() - .isEqualTo(item); + assertEquals(item, loadedItem); } @Override @@ -82,5 +83,20 @@ static class Item { int id; ObjectId v; + ObjectId vNull; + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Item item = (Item) o; + return id == item.id && Objects.equals(v, item.v) && vNull == item.vNull; + } + + @Override + public int hashCode() { + return Objects.hash(id, v, vNull); + } } } From 60c73455928af323f6e42d3e34261cfa8c65e905 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 31 Mar 2025 09:20:15 -0600 Subject: [PATCH 12/12] Take into account that Hibernate ORM may instantiate `ObjectIdJavaType` itself --- .../type/ObjectIdIntegrationTests.java | 23 +++++++++++++++---- .../internal/type/ObjectIdJavaType.java | 13 +++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java index 0a85e5a8..76fde3e6 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java @@ -21,6 +21,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.internal.type.ObjectIdJavaType; +import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; import jakarta.persistence.Entity; @@ -32,6 +34,8 @@ import org.bson.BsonNull; import org.bson.BsonObjectId; import org.bson.types.ObjectId; +import org.hibernate.annotations.JavaType; +import org.hibernate.annotations.JdbcType; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -52,13 +56,17 @@ class ObjectIdIntegrationTests implements SessionFactoryScopeAware { void insert() { var item = new Item(); item.id = 1; - item.v = new ObjectId(2, 0); + item.v = new ObjectId(1, 0); + item.vExplicitlyAnnotatedNotForThePublic = new ObjectId(2, 3); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertThat(mongoCollection.find()) .containsExactly(new BsonDocument() .append(ID_FIELD_NAME, new BsonInt32(1)) .append("v", new BsonObjectId(item.v)) - .append("vNull", BsonNull.VALUE)); + .append("vNull", BsonNull.VALUE) + .append( + "vExplicitlyAnnotatedNotForThePublic", + new BsonObjectId(item.vExplicitlyAnnotatedNotForThePublic))); } @Test @@ -85,18 +93,25 @@ static class Item { ObjectId v; ObjectId vNull; + @JavaType(ObjectIdJavaType.class) + @JdbcType(ObjectIdJdbcType.class) + ObjectId vExplicitlyAnnotatedNotForThePublic; + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } Item item = (Item) o; - return id == item.id && Objects.equals(v, item.v) && vNull == item.vNull; + return id == item.id + && Objects.equals(v, item.v) + && Objects.equals(vNull, item.vNull) + && Objects.equals(vExplicitlyAnnotatedNotForThePublic, item.vExplicitlyAnnotatedNotForThePublic); } @Override public int hashCode() { - return Objects.hash(id, v, vNull); + return Objects.hash(id, v, vNull, vExplicitlyAnnotatedNotForThePublic); } } } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java index 4b7084f8..c19db47f 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -18,6 +18,7 @@ import com.mongodb.hibernate.internal.FeatureNotSupportedException; import java.io.Serial; +import java.util.concurrent.ThreadLocalRandom; import org.bson.types.ObjectId; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.AbstractClassJavaType; @@ -28,6 +29,8 @@ public final class ObjectIdJavaType extends AbstractClassJavaType { @Serial private static final long serialVersionUID = 1L; + private static final int hashCode = ThreadLocalRandom.current().nextInt(); + public static final ObjectIdJavaType INSTANCE = new ObjectIdJavaType(); private ObjectIdJavaType() { @@ -51,4 +54,14 @@ public ObjectId wrap(X value, WrapperOptions options) { } throw new FeatureNotSupportedException(); } + + @Override + public boolean equals(Object o) { + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return hashCode; + } }