Skip to content
Original file line number Diff line number Diff line change
@@ -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<BsonDocument> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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.type;

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.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;
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.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;
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 = {ObjectIdIntegrationTests.Item.class})
@ExtendWith(MongoExtension.class)
class ObjectIdIntegrationTests implements SessionFactoryScopeAware {
@InjectMongoCollection("items")
private static MongoCollection<BsonDocument> mongoCollection;

private SessionFactoryScope sessionFactoryScope;

@Test
void insert() {
var item = new Item();
item.id = 1;
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(
"vExplicitlyAnnotatedNotForThePublic",
new BsonObjectId(item.vExplicitlyAnnotatedNotForThePublic)));
}

@Test
void getById() {
var item = new Item();
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));
assertEquals(item, loadedItem);
}

@Override
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) {
this.sessionFactoryScope = sessionFactoryScope;
}

@Entity
@Table(name = "items")
static class Item {
@Id
int id;

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)
&& Objects.equals(vNull, item.vNull)
&& Objects.equals(vExplicitlyAnnotatedNotForThePublic, item.vExplicitlyAnnotatedNotForThePublic);
}

@Override
public int hashCode() {
return Objects.hash(id, v, vNull, vExplicitlyAnnotatedNotForThePublic);
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
85 changes: 85 additions & 0 deletions src/main/java/com/mongodb/hibernate/internal/type/MqlType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME;

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(maxHibernateSqlTypeCode() < minMqlTypeCode());
}

MqlType(int code) {
this.code = code;
}

private final int code;

@Override
public String getName() {
return name();
}

@Override
public String getVendor() {
return MONGO_DBMS_NAME;
}

@Override
public Integer getVendorTypeNumber() {
return code;
}

private static int minMqlTypeCode() {
return Arrays.stream(MqlType.values())
.mapToInt(MqlType::getVendorTypeNumber)
.min()
.orElseThrow(MongoAssertions::fail);
}

private static int maxHibernateSqlTypeCode() {
Predicate<Field> publicStaticFinal = field -> {
var modifiers = field.getModifiers();
return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
};
ToIntFunction<Field> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.util.concurrent.ThreadLocalRandom;
import org.bson.types.ObjectId;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractClassJavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;

public final class ObjectIdJavaType extends AbstractClassJavaType<ObjectId> {
@Serial
private static final long serialVersionUID = 1L;

private static final int hashCode = ThreadLocalRandom.current().nextInt();

public static final ObjectIdJavaType INSTANCE = new ObjectIdJavaType();

private ObjectIdJavaType() {
super(ObjectId.class);
}

@Override
public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) {
return ObjectIdJdbcType.INSTANCE;
}

@Override
public <X> X unwrap(ObjectId value, Class<X> type, WrapperOptions options) {
throw new FeatureNotSupportedException();
}

@Override
public <X> ObjectId wrap(X value, WrapperOptions options) {
if (value instanceof ObjectId wrapped) {
return wrapped;
}
throw new FeatureNotSupportedException();
}

@Override
public boolean equals(Object o) {
return o != null && getClass() == o.getClass();
}

@Override
public int hashCode() {
return hashCode;
}
}
Loading