Improve handling of datasource name specified in persistence.xml files (#41194)

Closes #41192

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš 2025-07-17 16:06:00 +02:00 committed by GitHub
parent 7ea7c2dcc4
commit 8d77dfaf72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 271 additions and 6 deletions

View File

@ -53,6 +53,8 @@ import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import jakarta.persistence.Entity;
import jakarta.persistence.PersistenceUnitTransactionType;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.hibernate.cfg.JdbcSettings;
import org.eclipse.microprofile.health.Readiness;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
@ -154,6 +156,7 @@ import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
@ -366,6 +369,47 @@ class KeycloakProcessor {
recorder.setDefaultUserProfileConfiguration(configuration.getDefaultConfig());
}
/**
* Get datasource name obtained from the persistence.xml file based on this order:
* <ol>
* <li> return {@link JdbcSettings#JAKARTA_JTA_DATASOURCE} if specified
* <li> return {@link AvailableSettings#DATASOURCE} property if specified
* <li> return persistence unit name
* </ol>
* Can be removed after removing support for persistence.xml files
*/
static String getDatasourceNameFromPersistenceXml(PersistenceUnitDescriptor descriptor) {
if (descriptor == null) {
throw new IllegalStateException("Descriptor cannot be null");
}
final BiConsumer<String, String> infoAboutUsedSourceForDsName = (source, name) -> logger.debugf(
"Datasource name '%s' is obtained from the '%s' configuration property in persistence.xml file. " +
"Use '%s' name for datasource options like 'db-kind-%s'.", name, source, name, name);
String persistenceUnitName = descriptor.getName();
Properties properties = descriptor.getProperties();
// 1. return Jakarta properties
var jakartaProperty = properties.getProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE);
if (jakartaProperty != null) {
infoAboutUsedSourceForDsName.accept(JdbcSettings.JAKARTA_JTA_DATASOURCE, jakartaProperty);
return jakartaProperty;
}
// 2. return deprecated Hibernate property
var deprecatedHibernateProperty = properties.getProperty(AvailableSettings.DATASOURCE);
if (deprecatedHibernateProperty != null) {
logger.warnf("Property '%s' is deprecated for some time and you should rather use '%s' property for datasource name in persistence.xml file.",
AvailableSettings.DATASOURCE, JdbcSettings.JAKARTA_JTA_DATASOURCE);
infoAboutUsedSourceForDsName.accept(AvailableSettings.DATASOURCE, deprecatedHibernateProperty);
return deprecatedHibernateProperty;
}
// 3. return persistence unit name
infoAboutUsedSourceForDsName.accept("Persistence unit name", persistenceUnitName);
return persistenceUnitName;
}
/**
* <p>Configures the persistence unit for Quarkus.
*
@ -398,10 +442,11 @@ class KeycloakProcessor {
runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", defaultUnitDescriptor.getName())
.setInitListener(recorder.createDefaultUnitListener()));
} else {
Properties properties = descriptor.getProperties();
String datasourceName = getDatasourceNameFromPersistenceXml(descriptor);
configurePersistenceUnitProperties(datasourceName, descriptor);
// register a listener for customizing the unit configuration at runtime
runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", descriptor.getName())
.setInitListener(recorder.createUserDefinedUnitListener(properties.getProperty(AvailableSettings.DATASOURCE))));
.setInitListener(recorder.createUserDefinedUnitListener(datasourceName)));
userManagedEntities.addAll(descriptor.getManagedClassNames());
}
}
@ -427,6 +472,25 @@ class KeycloakProcessor {
producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor));
}
static void configurePersistenceUnitProperties(String datasourceName, ParsedPersistenceXmlDescriptor descriptor) {
Properties unitProperties = descriptor.getProperties();
var isResourceLocalSpecified = PersistenceUnitTransactionType.RESOURCE_LOCAL.equals(descriptor.getPersistenceUnitTransactionType()) ||
Optional.ofNullable(unitProperties.getProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE))
.map(f -> f.equalsIgnoreCase(PersistenceUnitTransactionType.RESOURCE_LOCAL.name()))
.orElse(false);
if (isResourceLocalSpecified) {
throw new IllegalArgumentException("You need to use '%s' transaction type in your persistence.xml file."
.formatted(PersistenceUnitTransactionType.JTA.name()));
}
unitProperties.setProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
descriptor.setTransactionType(PersistenceUnitTransactionType.JTA);
// set datasource name
unitProperties.setProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE,datasourceName);
unitProperties.setProperty(AvailableSettings.DATASOURCE, datasourceName); // for backward compatibility
}
private void configureDefaultPersistenceUnitProperties(ParsedPersistenceXmlDescriptor descriptor, HibernateOrmConfig config,
JdbcDataSourceBuildItem defaultDataSource) {
if (defaultDataSource == null || !defaultDataSource.isDefault()) {
@ -566,7 +630,7 @@ class KeycloakProcessor {
}
/**
* Register the custom {@link org.eclipse.microprofile.config.spi.ConfigSource} implementations.
* Register the custom {@link ConfigSource} implementations.
*
* @param configSources
*/

View File

@ -0,0 +1,200 @@
package org.keycloak.quarkus.deployment;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.JdbcSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.spi.PersistenceXmlParser;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.keycloak.quarkus.deployment.KeycloakProcessor.configurePersistenceUnitProperties;
import static org.keycloak.quarkus.deployment.KeycloakProcessor.getDatasourceNameFromPersistenceXml;
import static org.wildfly.common.Assert.assertNotNull;
public class PersistenceXmlDatasourcesTest {
private static final String PERSISTENCE_XML_BODY = """
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
version="3.0">
%s
</persistence>
""";
private static PersistenceXmlParser parser;
@BeforeAll
public static void setupParser() {
parser = PersistenceXmlParser.create();
}
@Test
public void datasourceNamesOrder() throws IOException {
// use Jakarta property
var content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
</properties>
</persistence-unit>
""";
assertUsedName(content, "user-store");
// use Hibernate property
content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
<properties>
<property name="hibernate.connection.datasource" value="my-store" />
</properties>
</persistence-unit>
""";
assertUsedName(content, "my-store");
// use persistence unit name
content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
</persistence-unit>
""";
assertUsedName(content, "user-store-pu");
// prefer Jakarta property
content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
<property name="hibernate.connection.datasource" value="my-store" />
</properties>
</persistence-unit>
""";
assertUsedName(content, "user-store");
// prefer Hibernate property as not accepting nonJta datasource
content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
<properties>
<property name="jakarta.persistence.nonJtaDataSource" value="user-store" />
<property name="hibernate.connection.datasource" value="my-store" />
</properties>
</persistence-unit>
""";
assertUsedName(content, "my-store");
}
@Test
public void transactionTypes() throws IOException {
// not specified transaction-type -> error
var content = """
<persistence-unit name="user-store-pu">
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
</properties>
</persistence-unit>
""";
assertPersistenceXmlSingleDS(content, descriptor -> {
var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor));
assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file."));
});
// jta data source is specified, so the tx type is JTA by default -> ok
content = """
<persistence-unit name="user-store-pu">
<jta-data-source>JDBC/something</jta-data-source>
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
</properties>
</persistence-unit>
""";
assertPersistenceXmlSingleDS(content, descriptor -> {
assertDoesNotThrow(() -> configurePersistenceUnitProperties("user-store", descriptor));
});
// tx type is set to RESOURCE_LOCAL -> error
content = """
<persistence-unit name="user-store-pu" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
</properties>
</persistence-unit>
""";
assertPersistenceXmlSingleDS(content, descriptor -> {
var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor));
assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file."));
});
// Jakarta TX prop is set to RESOURCE_LOCAL -> error
content = """
<persistence-unit name="user-store-pu">
<jta-data-source>JDBC/something</jta-data-source>
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
<property name="jakarta.persistence.transactionType" value="RESOURCE_LOCAL" />
</properties>
</persistence-unit>
""";
assertPersistenceXmlSingleDS(content, descriptor -> {
var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor));
assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file."));
});
// Everything is correct, we can check if the Jakarta prop is automatically set -> ok
content = """
<persistence-unit name="user-store-pu" transaction-type="JTA">
<properties>
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
</properties>
</persistence-unit>
""";
assertPersistenceXmlSingleDS(content, descriptor -> {
configurePersistenceUnitProperties("user-store", descriptor);
assertThat(descriptor.getProperties().getProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE), is("JTA"));
});
}
private void assertUsedName(String content, String expectedName) throws IOException {
assertPersistenceXmlSingleDS(content, descriptor -> {
var name = getDatasourceNameFromPersistenceXml(descriptor);
assertThat(name, is(expectedName));
configurePersistenceUnitProperties(name, descriptor);
var properties = descriptor.getProperties();
assertNotNull(properties);
assertThat(properties.getProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE), is(expectedName));
assertThat(properties.getProperty(AvailableSettings.DATASOURCE), is(expectedName));
});
}
private void assertPersistenceXmlSingleDS(String content, Consumer<ParsedPersistenceXmlDescriptor> asserts) throws IOException {
assertPersistenceXml(content, descriptors -> {
assertNotNull(descriptors);
assertThat(descriptors.size(), is(1));
var descriptor = descriptors.get(0);
assertNotNull(descriptor);
asserts.accept(descriptor);
});
}
private void assertPersistenceXml(String content, Consumer<List<ParsedPersistenceXmlDescriptor>> asserts) throws IOException {
String finalPersistenceXmlFileContent = PERSISTENCE_XML_BODY.formatted(content);
Path persistenceXmlFile = null;
try {
persistenceXmlFile = Files.createTempFile("persistence", ".xml");
Files.writeString(persistenceXmlFile, finalPersistenceXmlFileContent);
asserts.accept(parser.parse(List.of(persistenceXmlFile.toUri().toURL())).values().stream().map(f -> (ParsedPersistenceXmlDescriptor) f).toList());
} finally {
if (persistenceXmlFile != null) {
Files.deleteIfExists(persistenceXmlFile);
}
}
}
}

View File

@ -25,8 +25,7 @@
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<!-- Sets the name of the datasource to be the same as the datasource name in quarkus.properties-->
<property name="hibernate.connection.datasource" value="user-store" />
<property name="jakarta.persistence.transactionType" value="JTA" />
<property name="jakarta.persistence.jtaDataSource" value="user-store" />
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="false" />
</properties>

View File

@ -34,8 +34,10 @@ public class CustomJpaEntityProviderDistTest {
@Test
@TestProvider(CustomJpaEntityProvider.class)
@Launch({ "start-dev", "--log-level=org.hibernate.jpa.internal.util.LogHelper:debug" })
@Launch({ "start-dev", "--log-level=org.hibernate.jpa.internal.util.LogHelper:debug,org.keycloak.quarkus.deployment.KeycloakProcessor:debug" })
void testUserManagedEntityNotAddedToDefaultPU(CLIResult cliResult) {
cliResult.assertMessage("Multiple datasources are specified: <default>, user-store");
cliResult.assertMessage("Datasource name 'user-store' is obtained from the 'jakarta.persistence.jtaDataSource' configuration property in persistence.xml file. Use 'user-store' name for datasource options like 'db-kind-user-store'.");
cliResult.assertStringCount("name: user-store", 1);
cliResult.assertStringCount("com.acme.provider.legacy.jpa.entity.Realm", 1);
cliResult.assertStartedDevMode();