Skip to content

Conversation

@quaff
Copy link
Contributor

@quaff quaff commented Jul 24, 2024

[Please describe here what your change is about]


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license
and can be relicensed under the terms of the LGPL v2.1 license in the future at the maintainers' discretion.
For more information on licensing, please check here.


https://hibernate.atlassian.net/browse/HHH-15422

@quaff
Copy link
Contributor Author

quaff commented Aug 30, 2024

Could you review this also? @gavinking

@gavinking
Copy link
Member

So we have not up until this point allowed ConnectionProviders to be beans, and so:

  1. I wonder why it makes sense to do this for the special case of MultiTenantConnectionProvider if we don't do it in general. What's special about MultiTenantConnectionProvider? I assume the reason is that you want to @Inject into it. But inject what precisely? Tell me a story here. Given me an example.
  2. We have been having troubles with chicken/egg problems with respect to the CDI integration we already have, and so I'm sorta disinclined to add new functionality to that until we feel like we have this stuff licked.

I dunno, WDYT @sebersole @scottmarlow @yrodiere?

@yrodiere
Copy link
Member

yrodiere commented Sep 3, 2024

I wonder why it makes sense to do this for the special case of MultiTenantConnectionProvider if we don't do it in general. What's special about MultiTenantConnectionProvider? I assume the reason is that you want to @Inject into it. But inject what precisely? Tell me a story here. Given me an example.

To others reading this: the conversation continued on the Jira, see https://hibernate.atlassian.net/browse/HHH-15422?focusedCommentId=116437

But IMO the main point here is that a MultiTenantConnectionProvider needs access to datasources, which are typically created by the framework, and thus are more easily retrieved through injection.

We have been having troubles with chicken/egg problems with respect to the CDI integration we already have

That's a good point, too much CDI reliance can be problematic in Jakarta application servers, or at least in WildFly.

I would add that relying on CDI in very early phases of bootstrap is problematic for Quarkus as well, if we want to move more of the bootstrap to build time. Though this specific PR seems fine in that regard.

and so I'm sorta disinclined to add new functionality to that until we feel like we have this stuff licked.

I kind of agree, but I don't think that's a healthy approach unless we have a clearly identified Jira issue and at least the beginning of a plan to solve that chicken/egg problem.
If we don't intend to solve that chicken/egg problem, then we probably need to record the criteria for further CDI integration somewhere (must be lazy, or must happen after a certain point in bootstrap, or ...).

Copy link
Member

@gavinking gavinking left a comment

Choose a reason for hiding this comment

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

Please redo this to use the mechanism described here:

https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#entity-listeners

and also, redundantly, here:

https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a2999

These passages of the JPA spec describe how to enable injection into a persistence-managed object without having to make that object into a CDI bean.

This is actually what we should always be doing in this sort of situation.

@gavinking
Copy link
Member

gavinking commented Oct 4, 2024

To be clear, because there's apparently a lot of historical confusion on this point.

There's three strong reasons why something might need to be a bean:

  1. it's going to be injected into other things,
  2. it's contextual, i.e., it has a scope, or
  3. it needs to have interceptors to handle transaction management or whatever.

None of those motivations apply to the objects we're talking about here, and so they do not need to be beans.

@beikov
Copy link
Member

beikov commented Oct 4, 2024

If you think about a MultiTenantConnectionProvider which can delegate it's work to multiple datasource, it is highly desirable to make it a bean to inject datasources into it.
I just don't see a reason why we wouldn't want to make these things beans if we clearly know they could be.

@gavinking
Copy link
Member

gavinking commented Oct 4, 2024

If you think about a MultiTenantConnectionProvider which can delegate it's work to multiple datasource, it is highly desirable to make it a bean to inject datasources into it.

I'm not sure if you've simply not read my comments, or if you've not understood them. But the whole point is that:

Things do not need to be beans in order to have beans injected into them.

Please go and read those sections of the JPA spec which explain how to do it. (We even already have an implementation of that in Hibernate, by the way.)

I just don't see a reason why we wouldn't want to make these things beans if we clearly know they could be.

Because that ties the lifecycle of Hibernate with the lifecycle of CDI and creates problems.

@gavinking
Copy link
Member

gavinking commented Oct 4, 2024

So it turns out that my objections to this pull request actually boil down to only the following, which occurs in two places:

@Override
public boolean useJpaCompliantCreation() {
	return false;
}

If we change those to return true;, I think my objections are addressed, and I'm fine with doing this.

(Note that this is consistent with every other implementation of BeanContainer.LifecycleOptions internal to Hibernate.)

@quaff
Copy link
Contributor Author

quaff commented Oct 8, 2024

So it turns out that my objections to this pull request actually boil down to only the following, which occurs in two places:

@Override
public boolean useJpaCompliantCreation() {
	return false;
}

If we change those to return true;, I think my objections are addressed, and I'm fine with doing this.

(Note that this is consistent with every other implementation of BeanContainer.LifecycleOptions internal to Hibernate.)

Quoted from JpaCompliantLifecycleStrategy.java

 * In particular, {@code @Singleton}-scoped or {@code @ApplicationScoped} beans are instantiated
 * directly by this strategy, even if there is already an instance in the CDI context.
 * This means singletons are not really singletons, but this seems to be the behavior required by
 * the JPA 2.2 spec.

It means bean in context are not reused, it will create two instances, I don't like this behavior.

@gavinking
Copy link
Member

bean in context are not reused

Because they would not be beans. That's the point.

it will create two instances, it will create two instances

It will create one instance per persistence unit, which is completely fine.

@quaff
Copy link
Contributor Author

quaff commented Oct 8, 2024

it will create two instances

It will create one instance per persistence unit, which is completely fine.

Take spring for example, I mean one instance for spring container, one instance for Hibernate, I need it be real singleton.

@gavinking
Copy link
Member

it will create two instances

You need what to be a singleton?

@quaff
Copy link
Contributor Author

quaff commented Oct 8, 2024

it will create two instances

You need what to be a singleton?

The proposed CurrentTenantIdentifierResolver and MultiTenantConnectionProvider.

@yrodiere
Copy link
Member

yrodiere commented Oct 8, 2024

it will create two instances

You need what to be a singleton?

The proposed CurrentTenantIdentifierResolver and MultiTenantConnectionProvider.

This is what we were discussing yesterday, Gavin. Most people will just expect a CDI integration to involve actual CDI beans and to comply with scopes, and frankly I agree with them: it's just KISS. I know you disagree, at least in general, and I know there are technical reasons to not do that for e.g. ID generators and connection providers.

That being said, putting aside other component types, I thought you agreed it was reasonable to do that for the tenant identifier resolver?

EDIT: That being said, @quaff, regarding MultiTenantConnectionProvider I'm not sure we can reasonably reuse a singleton bean, as that class can implement Configurable and get injected with PU-specific config.

@gavinking
Copy link
Member

So as I was saying to Yoann on Zulip, I can see that CurrentTenantIdentifierResolver might legitimately be a contextual bean.

I absolutely struggle to see how this could ever possibly be legit for any sort of connection provider. Connection providers have persistence-unit-specific configuration.

And any contextual state which they might need can be injected into them.

@quaff
Copy link
Contributor Author

quaff commented Oct 8, 2024

as that class can implement Configurable and get injected with PU-specific config.

OK, I get the point, I changed useJpaCompliantCreation() to return true, please review.

@gavinking
Copy link
Member

gavinking commented Oct 8, 2024

Most people will just expect a CDI integration to involve actual CDI beans and to comply with scopes

I mean, just I don't see that.

  • Servlets support CDI injection. Do you expect to be able to inject a servlet into something else?
  • Interceptors support CDI injection. Do you expect to be able to inject an interceptor into something else?
  • etc.

I don't think you do expect that, so why would you expect it here?

From where would the assumption that one can inject a MultiTenantConnectionProvider into an arbitrary CDI bean arise? It's defined as a Hibernate Service, not as a CDI bean!

I thought you agreed it was reasonable to do that for the tenant identifier resolver?

Right, correct, I think there's a reasonable argument that CurrentTenantIdentifierResolver is a special case here, since:

  • it's much more likely to be dealing with contextual state (associated with a request or session),
  • it's not a Hibernate Service,
  • it's not likely to depend on any Hibernate Services or other PU-specific state, and
  • it doesn't have any defined instantiation/configuration semantics, and so Hibernate doesn't need to be involved in instantiating it.

Therefore, by making CurrentTenantIdentifierResolver we're providing some convenience, without tying together the lifecycle of the bean container with the instantiation of Hibernate.

I'm much more concerned about MultiTenantConnectionProvider here.

@gavinking
Copy link
Member

as that class can implement Configurable and get injected with PU-specific config.

OK, I get the point, I changed useJpaCompliantCreation() to return true, please review.

Yeah, that looks fine now.

@yrodiere yrodiere requested review from beikov and gavinking October 8, 2024 09:52
@gavinking
Copy link
Member

I think there's a reasonable argument that CurrentTenantIdentifierResolver is a special case here

Indeed, you could choose to view the CurrentTenantIdentifierResolver as precisely a kind of "bridge" between the world where contextual information about the request/session is available, and the world of PU-specific state held by Services.

And that explains why it's a special case.

@yrodiere
Copy link
Member

yrodiere commented Oct 8, 2024

I mean, just I don't see that.

  • Servlets support CDI injection. Do you expect to be able to inject a servlet into something else?

  • Interceptors support CDI injection. Do you expect to be able to inject an interceptor into something else?

  • etc.

I don't think you do expect that, so why would you expect it here?

I do not expect to need to inject servlets, interceptors, or most pluggable Hibernate components into my application -- with the exception of tenant identifier resolvers and some others.

I do however expect that if servlets, interceptors, or pluggable Hibernate components are involved in CDI, then:

  1. Trying to inject them into my application will either work, using a CDI scope as intuitive as possible, or somehow notify me (exception?) that such component just cannot be a CDI bean.
  2. Putting @ApplicationScoped or @RequestScoped on them, however nonsensical, will either be taken into account, or somehow notify me (exception?) that it's invalid.

These expectations may not match current implementations, or even the specs, but I think they are reasonable from an UX point of view, and they do comply with the principle of least surprise.
They may not be achievable completely (since part of the behavior would have to be implemented in the CDI container), but I think we should at least try where possible.

@quaff quaff marked this pull request as draft October 9, 2024 01:04
@quaff quaff marked this pull request as ready for review October 9, 2024 02:21
Copy link
Member

@gavinking gavinking left a comment

Choose a reason for hiding this comment

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

@quaff, thanks, but please see my further comments.

Comment on lines +381 to +410
final BeanContainer beanContainer = Helper.allowExtensionsInCdi( serviceRegistry ) ? serviceRegistry.requireService( ManagedBeanRegistry.class ).getBeanContainer() : null;
if (beanContainer != null) {
this.currentTenantIdentifierResolver = beanContainer.getBean(
CurrentTenantIdentifierResolver.class,
new BeanContainer.LifecycleOptions() {
@Override
public boolean canUseCachedReferences() {
return true;
}

@Override
public boolean useJpaCompliantCreation() {
return false;
}
},
new BeanInstanceProducer() {

@Override
public <B> B produceBeanInstance(Class<B> beanType) {
return null;
}

@Override
public <B> B produceBeanInstance(String name, Class<B> beanType) {
return null;
}

}
).getBeanInstance();
}
Copy link
Member

Choose a reason for hiding this comment

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

Please extract out a function for this, the constructor is already waaaaaay too long.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gavinking I suggest to introduce new util methods for org.hibernate.resource.beans.internal.Helper and refactor all BeanContainer access to use those methods in another PR, WDYT?

	@SuppressWarnings( "unchecked" )
	public static <T> T getBean(ServiceRegistry serviceRegistry, Class<?> beanType, boolean canUseCachedReferences, boolean useJpaCompliantCreation, T fallback) {
		final BeanContainer beanContainer = allowExtensionsInCdi( serviceRegistry ) ? serviceRegistry.requireService( ManagedBeanRegistry.class ).getBeanContainer() : null;
		if ( beanContainer == null ) {
			return null;
		}
		return (T) beanContainer.getBean(
				beanType,
				new BeanContainer.LifecycleOptions() {
					@Override
					public boolean canUseCachedReferences() {
						return canUseCachedReferences;
					}

					@Override
					public boolean useJpaCompliantCreation() {
						return useJpaCompliantCreation;
					}
				},
				new BeanInstanceProducer() {

					@Override
					public <B> B produceBeanInstance(Class<B> beanType) {
						return (B) fallback;
					}

					@Override
					public <B> B produceBeanInstance(String name, Class<B> beanType) {
						throw new UnsupportedOperationException("The method shouldn't be called");
					}

				}
		).getBeanInstance();
	}

Copy link
Member

Choose a reason for hiding this comment

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

I suggest to introduce new util methods for org.hibernate.resource.beans.internal.Helper and refactor all BeanContainer access to use those methods

That sounds fine to me, but please do it now, so that it doesn't get forgotten. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

OK, great, thanks 🙏

Comment on lines +188 to 220
return beanContainer.getBean(
ConnectionProvider.class,
new BeanContainer.LifecycleOptions() {
@Override
public boolean canUseCachedReferences() {
return true;
}

@Override
public boolean useJpaCompliantCreation() {
return true;
}
},
new BeanInstanceProducer() {

@Override
public <B> B produceBeanInstance(Class<B> beanType) {
return (B) noAppropriateConnectionProvider();
}

@Override
public <B> B produceBeanInstance(String name, Class<B> beanType) {
return (B) noAppropriateConnectionProvider();
}

}
).getBeanInstance();
}
else {
return noAppropriateConnectionProvider();
}

}
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, please extract a function.

Comment on lines +46 to +75
final BeanContainer beanContainer = Helper.allowExtensionsInCdi( registry ) ? registry.requireService( ManagedBeanRegistry.class ).getBeanContainer() : null;
if (beanContainer != null) {
return beanContainer.getBean(
MultiTenantConnectionProvider.class,
new BeanContainer.LifecycleOptions() {
@Override
public boolean canUseCachedReferences() {
return true;
}

@Override
public boolean useJpaCompliantCreation() {
return true;
}
},
new BeanInstanceProducer() {

@Override
public <B> B produceBeanInstance(Class<B> beanType) {
return null;
}

@Override
public <B> B produceBeanInstance(String name, Class<B> beanType) {
return null;
}

}
).getBeanInstance();
}
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, extract a function here, please.

@beikov beikov merged commit 8b5bc44 into hibernate:main Oct 17, 2024
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants