Skip to content
10 changes: 7 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ tasks.withType<Javadoc> {
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/persistence/3.1/apidocs/",
"https://docs.hibernate.org/orm/6.6/javadocs/",
"https://mongodb.github.io/mongo-java-driver/5.3/apidocs/bson/",
"https://mongodb.github.io/mongo-java-driver/5.3/apidocs/mongodb-driver-core/",
"https://mongodb.github.io/mongo-java-driver/5.3/apidocs/mongodb-driver-sync/",
"https://mongodb.github.io/mongo-java-driver/5.6/apidocs/bson/",
"https://mongodb.github.io/mongo-java-driver/5.6/apidocs/driver-core/",
"https://mongodb.github.io/mongo-java-driver/5.6/apidocs/driver-sync/",
"https://javadoc.io/doc/org.jspecify/jspecify/1.0.0/")
// specify the custom `@mongoCme` `javadoc` block tag
tags("mongoCme:TM:Concurrency, Mutability, Execution\\:")
Copy link
Member Author

@stIncMale stIncMale Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose using a javadoc tag, which allows us to express behavior related to thread-safety, mutability, and execution overall in a free form, as opposed to the @ThreadSafe uniform approach used in the driver. This is because the reality is often more subtle than @ThreadSafe/@NotThreadSafe, and it is useful to be able to express it while having the javadoc formatting at hand.

The name of the tag is currently @mongoCme, but I am open to other suggestions.

}
}

Expand Down Expand Up @@ -146,6 +148,8 @@ tasks.withType<JavaCompile>().configureEach {
tasks.compileJava.get() ->
options.errorprone {
disableWarningsInGeneratedCode = true
// Error Prone complains about the `javadoc` tags registered via the `-tag`/`-taglet` options
disable("InvalidBlockTag")
option("NullAway:AnnotatedPackages", "com.mongodb.hibernate")
error("NullAway")
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ junit-jupiter = "5.13.4"
assertj = "3.27.3"
google-errorprone-core = "2.36.0"
nullaway = "0.12.4"
jspecify = "1.0.0"
jspecify = "1.0.0" # Remember to update javadoc links
hibernate-orm = "6.6.34.Final" # Remember to update javadoc links
mongo-java-driver-sync = "5.6.1" # Remember to update javadoc links
findbugs-jsr = "3.0.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@
* <p>For the documentation on the supported <a
* href="https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#hql-exp-functions">HQL
* functions</a> see {@link #initializeFunctionRegistry(FunctionContributions)}.
*
* @mongoCme Must be immutable, as per the documentation of {@link Dialect}. It is unclear whether it should be
* shallowly or deeply immutable; most likely—shallowly.
*/
@Sealed
public class MongoDialect extends Dialect {
Expand Down Expand Up @@ -196,7 +199,7 @@ protected void checkVersion() {

@Override
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
return new MongoTranslatorFactory();
return MongoTranslatorFactory.INSTANCE;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way we work around the issue fixed in hibernate/hibernate-orm#9900, because we are not getting that fix in Hibernate ORM 6.x.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your raising the issue and it is a quick fix. On the other hand, it seems trivial to me for we only avoid one unnecessary duplicated object creation (which usually is not a big deal for the creation is not costly).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other id generator defect is more important and hard to fix from Hibernate side. I created a PR at hibernate/hibernate-orm#9965. Let us see.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR @NathanQingyangXu mentioned was superseded with hibernate/hibernate-orm#9969, and the fix was released in 6.6.14.

}

@Override
Expand Down Expand Up @@ -350,6 +353,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionRegistry.register("array_includes_nullable", new MongoArrayIncludesFunction(true, typeConfiguration));
}

/** @mongoCme The {@link MutationOperation} returned from this method does not have to be thread-safe. */
@Override
public MutationOperation createOptionalTableUpdateOperation(
EntityMutationTarget mutationTarget,
Expand All @@ -372,6 +376,7 @@ public void appendDatetimeFormat(SqlAppender appender, String format) {
throw new FeatureNotSupportedException("TODO-HIBERNATE-88 https://jira.mongodb.org/browse/HIBERNATE-88");
}

/** @mongoCme The {@link SQLExceptionConversionDelegate} returned from this method must be thread-safe. */
@Override
public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() {
return (sqlException, exceptionMessage, mql) -> new JDBCException(exceptionMessage, sqlException, mql);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.hibernate.mapping.AggregateColumn;
import org.hibernate.mapping.Column;

/** @mongoCme It is unclear whether this class must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class MongoAggregateSupport extends AggregateSupportImpl {
public static final MongoAggregateSupport INSTANCE = new MongoAggregateSupport();
public static final String UNSUPPORTED_MESSAGE_PREFIX =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
* href="https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#hql-array-constructor-functions">
* {@code array}, {@code array_list}</a>.
*
* <p>Thread-safe.
* @mongoCme Must be thread-safe.
*/
public final class MongoArrayConstructorFunction extends ArrayConstructorFunction {
static final Set<String> NAMES = Set.of("array", "array_list");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
*
* <p>See <a href="https://www.mongodb.com/docs/manual/tutorial/query-arrays/">Query an Array</a>.
*
* <p>Thread-safe.
* @mongoCme Must be thread-safe.
*/
public final class MongoArrayContainsFunction extends AbstractArrayContainsFunction {
public MongoArrayContainsFunction(boolean nullable, TypeConfiguration typeConfiguration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
*
* <p>See <a href="https://www.mongodb.com/docs/manual/tutorial/query-arrays/">Query an Array</a>.
*
* <p>Thread-safe.
* @mongoCme Must be thread-safe.
*/
public final class MongoArrayIncludesFunction extends AbstractArrayIncludesFunction {
public MongoArrayIncludesFunction(boolean nullable, TypeConfiguration typeConfiguration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
import java.util.Set;
import java.util.StringJoiner;
import org.hibernate.annotations.Struct;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.ResourceStreamLocator;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.spi.AdditionalMappingContributions;
import org.hibernate.boot.spi.AdditionalMappingContributor;
import org.hibernate.boot.spi.InFlightMetadataCollector;
Expand All @@ -50,6 +53,12 @@
import org.hibernate.type.BasicPluralType;
import org.hibernate.type.ComponentType;

/**
* @mongoCme The instance methods of {@link AdditionalMappingContributor} are called multiple times if multiple
* {@link Metadata} instances are {@linkplain MetadataSources#buildMetadata() built} using the same
* {@link BootstrapServiceRegistry}.
*/
@SuppressWarnings("MissingSummary")
public final class MongoAdditionalMappingContributor implements AdditionalMappingContributor {
/**
* We do not support these characters because BSON fields with names containing them must be handled specially as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@
import java.util.Map;
import java.util.Set;
import org.hibernate.HibernateException;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.registry.StandardServiceInitiator;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.service.Service;
import org.hibernate.service.UnknownServiceException;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.jspecify.annotations.Nullable;

/** @mongoCme Thread-safe. */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most other places I said "Must be thread-safe", but here it is just "Thread-safe". The difference is that here the behavior is something we provide based on what we need. In other places we must provide thread-safety because of the Hibernate ORM API parts we implement/extend.

@SuppressWarnings("MissingSummary")
public final class StandardServiceRegistryScopedState implements Service {
@Serial
private static final long serialVersionUID = 1L;
Expand All @@ -61,17 +65,30 @@ private void writeObject(ObjectOutputStream out) throws IOException {
"This class is not designed to be serialized despite it having to implement `Serializable`");
}

/**
* @mongoCme The instance methods of {@link org.hibernate.service.spi.ServiceContributor} are called multiple times
* if multiple {@link StandardServiceRegistry} instances are {@linkplain StandardServiceRegistryBuilder#build()
* built} using the same {@link BootstrapServiceRegistry}.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, I failed to see why multiple StandardServiceRegistry need to be build from one BootstrapServiceRegistry. Could we move the confusing doc out of the code base?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that matters. What does matter is that doing so is possible with the API of Hibernate ORM, and it is not forbidden by the API documentation, so we must take that into account when we are extending Hibernate ORM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is why I got confused. Yeah, it is not forbidden, but why we need to cover it? I never know of such scenario that we need to create multiple StandardServiceRegistry, which seems to invite trouble.

Copy link
Contributor

@NathanQingyangXu NathanQingyangXu Apr 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So from my understanding, Hibernate's SessionFactory is pretty heavy-weighted (it is super slow for it tries to prepare everything so runtime perf could be as fast as possible; one example is it tries to finish static SQL translations as much as possible so they could be reused). For that reason, for typical usage, it is seldom created multiple times (in typical Spring JPA usage, one single SessionFactory is bound to the global Spring Bean Context), unless there are good reasons.

If multiple SessionFactory is justified (maybe each one uses a subset of the META-INF/services so they could share the same jar library without conflict; then it requires customized BootstrapServiceRegistry, one of whose components is the ClassLoaderService which could be customized), different BootstrapServiceRegistry is expected to be created for each of them. Different BootstrapServiceRegistry leads to different StandardServiceRegistry (as explained above, maybe due to different class loaders), otherwise why create different SessionFactory in the first place?

During runtime, one SessionFactory has one single ServiceRegistry and its default implementation is StandardServiceRegistry as its API below (in org.hibernate.engine.spi.SessionFactoryImplementor):

ServiceRegistryImplementor getServiceRegistry();

From my understanding, one and only one StandardServiceRegistry is needed, and StandardServiceRegistryBuilder is the de-factor one-time stuff. When it has finished building, it seems only in theory that it could be invoked again (why? one StandardServiceRegistry is not enough?). That is what puzzles me. Yeah, it is possible but why emphasize such fact in Javadoc (arguably not internal doc for it is likely that community contributor needs to read it to figure out something).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above comment also explains why I got puzzled by your previous PR; but you modified it to make it aligned with reality so I approved that.

public static final class ServiceContributor implements org.hibernate.service.spi.ServiceContributor {
public ServiceContributor() {}

@Override
public void contribute(StandardServiceRegistryBuilder serviceRegistryBuilder) {
serviceRegistryBuilder.addInitiator(new StandardServiceInitiator<StandardServiceRegistryScopedState>() {
/**
* @mongoCme This method may be called multiple times when
* {@linkplain StandardServiceRegistryBuilder#build() building} a single
* {@link StandardServiceRegistry} instance.
*/
@Override
public Class<StandardServiceRegistryScopedState> getServiceInitiated() {
return StandardServiceRegistryScopedState.class;
}

/**
* @mongoCme This method is called not more than once per instance of {@link StandardServiceInitiator}.
*/
@Override
public StandardServiceRegistryScopedState initiateService(
Map<String, Object> configurationValues, ServiceRegistryImplementor serviceRegistry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,33 @@
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.generator.EventType;
import org.hibernate.generator.Generator;
import org.hibernate.generator.GeneratorCreationContext;
import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext;
import org.jspecify.annotations.Nullable;

/**
* Tread-safe.
*
* @see com.mongodb.hibernate.annotations.ObjectIdGenerator
* @mongoCme Must be thread-safe.
*/
@SuppressWarnings("MissingSummary")
public final class ObjectIdGenerator implements BeforeExecutionGenerator {
@Serial
private static final long serialVersionUID = 1L;

private static final org.bson.codecs.ObjectIdGenerator GENERATOR = new org.bson.codecs.ObjectIdGenerator();

private final boolean forIdentifier;

/** @see Generator */
public ObjectIdGenerator(
com.mongodb.hibernate.annotations.ObjectIdGenerator config,
Member annotatedMember,
CustomIdGeneratorCreationContext context) {
this(true);
}
CustomIdGeneratorCreationContext context) {}

/** @see Generator */
public ObjectIdGenerator(
com.mongodb.hibernate.annotations.ObjectIdGenerator config,
Member annotatedMember,
GeneratorCreationContext context) {
this(false);
}

private ObjectIdGenerator(boolean forIdentifier) {
this.forIdentifier = forIdentifier;
}
GeneratorCreationContext context) {}

@Override
public Object generate(
Expand All @@ -67,13 +60,6 @@ public Object generate(
EventType eventType) {
if (currentValue != null) {
return currentValue;
} else if (forIdentifier) {
// Hibernate ORM provides `null` as `currentValue` when generating an entity identifier value.
// To work around that behavior we have to read the value explicitly.
var currentId = session.getEntityPersister(null, owner).getIdentifier(owner, session);
if (currentId != null) {
return currentId;
}
Comment on lines -70 to -76
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this workaround since https://hibernate.atlassian.net/browse/HHH-19320 was fixed in 6.6.14.

The fix indeed works:

  1. You may see ObjectIdAsIdIntegrationTests.Generated.assignedValue passing despite the workaround having been removed.
  2. I verified that the test fails on 6.6.13.Final without our hack, but succeeds with it. Note that I had to use junit-jupiter = "5.10.3" to run integration tests with that version of Hibernate ORM, which required removing a few unrelated tests that need a newer junit-jupiter version. I don't know why the older Hibernate ORM requires an older junit-jupiter, and Gradle fails without saying anything meaningful with junit-jupiter = "5.13.4".

@NathanQingyangXu, thank you for reporting the aforementioned bug to Hibernate ORM.

}
return GENERATOR.generate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@
import org.hibernate.type.BasicType;
import org.jspecify.annotations.Nullable;

/** @mongoCme This class and its subclasses do not have to be thread-safe. */
@SuppressWarnings("MissingSummary")
public abstract class AbstractMqlTranslator<T extends JdbcOperation> implements SqlAstTranslator<T> {

private final SessionFactoryImplementor sessionFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.hibernate.sql.model.jdbc.JdbcMutationOperation;
import org.jspecify.annotations.Nullable;

/** @mongoCme Does not have to be thread-safe. */
final class ModelMutationMqlTranslator<O extends JdbcMutationOperation> extends AbstractMqlTranslator<O> {

private final TableMutation<O> tableMutation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
import org.hibernate.sql.model.ast.TableMutation;
import org.hibernate.sql.model.jdbc.JdbcMutationOperation;

/** @mongoCme Must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class MongoTranslatorFactory implements SqlAstTranslatorFactory {
public static MongoTranslatorFactory INSTANCE = new MongoTranslatorFactory();

private MongoTranslatorFactory() {}

@Override
public SqlAstTranslator<JdbcOperationQuerySelect> buildSelectTranslator(
SessionFactoryImplementor sessionFactoryImplementor, SelectStatement selectStatement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.jspecify.annotations.Nullable;

/** @mongoCme Does not have to be thread-safe. */
final class MutationMqlTranslator extends AbstractMqlTranslator<JdbcOperationQueryMutation> {

private final MutationStatement mutationStatement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider;
import org.jspecify.annotations.Nullable;

/** @mongoCme Does not have to be thread-safe. */
final class SelectMqlTranslator extends AbstractMqlTranslator<JdbcOperationQuerySelect> {

private final SelectStatement selectStatement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
import org.hibernate.type.spi.TypeConfiguration;
import org.jspecify.annotations.Nullable;

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class MongoArrayJdbcType extends ArrayJdbcType {
@Serial
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
import org.hibernate.type.descriptor.jdbc.StructJdbcType;
import org.jspecify.annotations.Nullable;

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class MongoStructJdbcType implements StructJdbcType {
@Serial
private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -217,7 +218,7 @@ private void writeObject(ObjectOutputStream out) throws IOException {
"This class is not designed to be serialized despite it having to implement `Serializable`");
}

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
private final class Binder<X> extends BasicBinder<X> {
@Serial
private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -247,7 +248,7 @@ protected void doBind(CallableStatement st, X value, String name, WrapperOptions
}
}

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
private final class Extractor<X> extends BasicExtractor<X> {
@Serial
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.jspecify.annotations.Nullable;

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class ObjectIdJavaType extends AbstractClassJavaType<ObjectId> {
@Serial
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.jspecify.annotations.Nullable;

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
@SuppressWarnings("MissingSummary")
public final class ObjectIdJdbcType implements JdbcType {
@Serial
private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -75,7 +76,7 @@ public <X> ValueExtractor<X> getExtractor(JavaType<X> javaType) {
return result;
}

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
private final class Binder extends BasicBinder<ObjectId> {
@Serial
private static final long serialVersionUID = 1L;
Expand All @@ -97,7 +98,7 @@ protected void doBind(CallableStatement st, ObjectId value, String name, Wrapper
}
}

/** Thread-safe. */
/** @mongoCme Must be thread-safe. */
private final class Extractor extends BasicExtractor<ObjectId> {
@Serial
private static final long serialVersionUID = 1L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
* <p>This {@link ConnectionProvider} does not respect the {@value org.hibernate.cfg.AvailableSettings#AUTOCOMMIT}
* configuration property, and {@linkplain MongoConnectionProvider#getConnection() provides} {@link Connection}s with
* {@linkplain Connection#getAutoCommit() auto-commit} enabled.
*
* @mongoCme The methods {@link #getConnection()}/{@link #closeConnection(Connection)} must be thread-safe. It is
* unclear about the other methods.
*/
public final class MongoConnectionProvider implements ConnectionProvider, Stoppable {
@Serial
Expand All @@ -58,6 +61,7 @@ public final class MongoConnectionProvider implements ConnectionProvider, Stoppa
private @Nullable StandardServiceRegistryScopedState standardServiceRegistryScopedState;
private transient @Nullable MongoClient mongoClient;

/** @mongoCme Must be thread-safe. */
@Override
public Connection getConnection() throws SQLException {
try {
Expand All @@ -72,6 +76,7 @@ public Connection getConnection() throws SQLException {
}
}

/** @mongoCme Must be thread-safe. */
@Override
public void closeConnection(Connection connection) throws SQLException {
connection.close();
Expand Down
Loading