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

private SessionFactoryScope sessionFactoryScope;

@Test
void insert() {
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()
.append(ID_FIELD_NAME, new BsonObjectId(item.id))
.append("v", new BsonObjectId(item.v)));
}

@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) {
this.sessionFactoryScope = sessionFactoryScope;
}

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

ObjectId v;
}
}
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);
}
}
84 changes: 84 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,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<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,55 @@
/*
* 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<ObjectId> {
@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> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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 <X> ValueBinder<X> getBinder(JavaType<X> javaType) {
if (!javaType.equals(JAVA_TYPE)) {
throw new FeatureNotSupportedException();
}
@SuppressWarnings("unchecked")
var result = (ValueBinder<X>) Binder.INSTANCE;
return result;
}

@Override
public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
if (!javaType.equals(JAVA_TYPE)) {
throw new FeatureNotSupportedException();
}
@SuppressWarnings("unchecked")
var result = (ValueExtractor<X>) Extractor.INSTANCE;
return result;
}

/**
* Thread-safe.
*/
private static final class Binder extends BasicBinder<ObjectId> {
@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();
}
}

/**
* Thread-safe.
*/
private static final class Extractor extends BasicExtractor<ObjectId> {
@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();
}
}
}
Loading