Skip to content

Commit 5c2d1e1

Browse files
committed
[#31974] Add HibernateProcessorUtil class
The goal is to have the common configuration for Hibernate ORM and Hibernate Reactive so that initialization is consistent between the two
1 parent 820127f commit 5c2d1e1

File tree

1 file changed

+366
-0
lines changed

1 file changed

+366
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
package io.quarkus.hibernate.orm.deployment.util;
2+
3+
import static io.quarkus.hibernate.orm.deployment.HibernateConfigUtil.firstPresent;
4+
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.Locale;
10+
import java.util.Map;
11+
import java.util.Optional;
12+
import java.util.OptionalInt;
13+
import java.util.Properties;
14+
import java.util.Set;
15+
import java.util.function.BiConsumer;
16+
import java.util.stream.Collectors;
17+
18+
import jakarta.persistence.SharedCacheMode;
19+
import jakarta.persistence.ValidationMode;
20+
21+
import org.hibernate.cfg.AvailableSettings;
22+
import org.hibernate.id.SequenceMismatchStrategy;
23+
import org.hibernate.jpa.boot.spi.JpaSettings;
24+
import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor;
25+
import org.hibernate.loader.BatchFetchStyle;
26+
import org.jboss.logging.Logger;
27+
28+
import io.quarkus.datasource.common.runtime.DatabaseKind;
29+
import io.quarkus.deployment.Capabilities;
30+
import io.quarkus.deployment.Capability;
31+
import io.quarkus.deployment.annotations.BuildProducer;
32+
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
33+
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
34+
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
35+
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
36+
import io.quarkus.hibernate.orm.deployment.HibernateConfigUtil;
37+
import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig;
38+
import io.quarkus.hibernate.orm.deployment.HibernateOrmConfigPersistenceUnit;
39+
import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem;
40+
import io.quarkus.hibernate.orm.deployment.spi.DatabaseKindDialectBuildItem;
41+
import io.quarkus.hibernate.orm.runtime.HibernateOrmRuntimeConfig;
42+
import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDescriptor;
43+
import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind;
44+
import io.quarkus.runtime.LaunchMode;
45+
import io.quarkus.runtime.configuration.ConfigurationException;
46+
47+
/**
48+
* A set of utilities method to collect the common operations needed to configure the
49+
* Hibernate ORM and Hibernate Reactive extensions.
50+
*/
51+
public final class HibernateProcessorUtil {
52+
private static final Logger LOG = Logger.getLogger(HibernateProcessorUtil.class);
53+
public static final String NO_SQL_LOAD_SCRIPT_FILE = "no-file";
54+
55+
private HibernateProcessorUtil() {
56+
}
57+
58+
public static boolean hasEntities(JpaModelBuildItem jpaModel) {
59+
return !jpaModel.getEntityClassNames().isEmpty();
60+
}
61+
62+
public static Optional<FormatMapperKind> jsonMapperKind(Capabilities capabilities) {
63+
if (capabilities.isPresent(Capability.JACKSON)) {
64+
return Optional.of(FormatMapperKind.JACKSON);
65+
} else if (capabilities.isPresent(Capability.JSONB)) {
66+
return Optional.of(FormatMapperKind.JSONB);
67+
} else {
68+
return Optional.empty();
69+
}
70+
}
71+
72+
public static Optional<FormatMapperKind> xmlMapperKind(Capabilities capabilities) {
73+
return capabilities.isPresent(Capability.JAXB)
74+
? Optional.of(FormatMapperKind.JAXB)
75+
: Optional.empty();
76+
}
77+
78+
public static boolean isHibernateValidatorPresent(Capabilities capabilities) {
79+
return capabilities.isPresent(Capability.HIBERNATE_VALIDATOR);
80+
}
81+
82+
public static void setDialectAndStorageEngine(
83+
String persistenceUnitName,
84+
Optional<String> dbKind,
85+
Optional<String> explicitDialect,
86+
Optional<String> explicitDbMinVersion,
87+
List<DatabaseKindDialectBuildItem> dbKindDialectBuildItems,
88+
Optional<String> storageEngine,
89+
BuildProducer<SystemPropertyBuildItem> systemProperties,
90+
BiConsumer<String, String> puPropertiesCollector,
91+
Set<String> storageEngineCollector) {
92+
Optional<String> dialect = explicitDialect;
93+
Optional<String> dbProductName = Optional.empty();
94+
Optional<String> dbProductVersion = explicitDbMinVersion;
95+
96+
if (dbKind.isPresent() || explicitDialect.isPresent()) {
97+
for (DatabaseKindDialectBuildItem item : dbKindDialectBuildItems) {
98+
if (dbKind.isPresent() && DatabaseKind.is(dbKind.get(), item.getDbKind())
99+
|| explicitDialect.isPresent() && item.getMatchingDialects().contains(explicitDialect.get())) {
100+
dbProductName = item.getDatabaseProductName();
101+
if (dbProductName.isEmpty() && explicitDialect.isEmpty()) {
102+
dialect = item.getDialectOptional();
103+
}
104+
if (explicitDbMinVersion.isEmpty()) {
105+
dbProductVersion = item.getDefaultDatabaseProductVersion();
106+
}
107+
break;
108+
}
109+
}
110+
if (dialect.isEmpty() && dbProductName.isEmpty()) {
111+
throw new ConfigurationException(
112+
"Could not guess the dialect from the database kind '"
113+
+ dbKind.get()
114+
+ "'. Add an explicit '"
115+
+ HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "dialect")
116+
+ "' property.");
117+
}
118+
}
119+
120+
if (dialect.isPresent()) {
121+
puPropertiesCollector.accept(AvailableSettings.DIALECT, dialect.get());
122+
} else if (dbProductName.isPresent()) {
123+
puPropertiesCollector.accept(AvailableSettings.JAKARTA_HBM2DDL_DB_NAME, dbProductName.get());
124+
}
125+
126+
if (storageEngine.isPresent()) {
127+
if (isMySQLOrMariaDB(dbKind, dialect)) {
128+
// The storage engine has to be set as a system property.
129+
// We record it so that we can later run checks (because we can only set a single value)
130+
storageEngineCollector.add(storageEngine.get());
131+
systemProperties.produce(new SystemPropertyBuildItem(AvailableSettings.STORAGE_ENGINE, storageEngine.get()));
132+
} else {
133+
LOG.warnf(
134+
"The storage engine configuration is being ignored because the database is neither MySQL nor MariaDB.");
135+
}
136+
}
137+
138+
if (dbProductVersion.isPresent()) {
139+
puPropertiesCollector.accept(AvailableSettings.JAKARTA_HBM2DDL_DB_VERSION, dbProductVersion.get());
140+
}
141+
}
142+
143+
public static void configureProperties(QuarkusPersistenceUnitDescriptor desc, HibernateOrmConfigPersistenceUnit config,
144+
HibernateOrmConfig hibernateOrmConfig, boolean reactive) {
145+
// Quoting strategy
146+
configureQuoting(desc, config);
147+
148+
// Physical Naming Strategy
149+
config.physicalNamingStrategy().ifPresent(namingStrategy -> desc.getProperties()
150+
.setProperty(AvailableSettings.PHYSICAL_NAMING_STRATEGY, namingStrategy));
151+
152+
// Implicit Naming Strategy
153+
config.implicitNamingStrategy().ifPresent(namingStrategy -> desc.getProperties()
154+
.setProperty(AvailableSettings.IMPLICIT_NAMING_STRATEGY, namingStrategy));
155+
156+
// Metadata builder contributor
157+
config.metadataBuilderContributor().ifPresent(className -> desc.getProperties()
158+
.setProperty(JpaSettings.METADATA_BUILDER_CONTRIBUTOR, className));
159+
160+
// Mapping
161+
if (config.mapping().timezone().timeZoneDefaultStorage().isPresent()) {
162+
desc.getProperties().setProperty(AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
163+
config.mapping().timezone().timeZoneDefaultStorage().get().name());
164+
}
165+
desc.getProperties().setProperty(AvailableSettings.PREFERRED_POOLED_OPTIMIZER,
166+
config.mapping().id().optimizer().idOptimizerDefault()
167+
.orElse(HibernateOrmConfigPersistenceUnit.IdOptimizerType.POOLED_LO).configName);
168+
169+
//charset
170+
desc.getProperties()
171+
.setProperty(AvailableSettings.HBM2DDL_CHARSET_NAME, config.database().charset().name());
172+
173+
// Query
174+
int batchSize = firstPresent(config.fetch().batchSize(), config.batchFetchSize()).orElse(defaultBatchSize(reactive));
175+
if (batchSize > 0) {
176+
desc.getProperties().setProperty(AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, Integer.toString(batchSize));
177+
desc.getProperties().setProperty(AvailableSettings.BATCH_FETCH_STYLE, BatchFetchStyle.PADDED.toString());
178+
}
179+
180+
// Fetch
181+
if (config.fetch().maxDepth().isPresent()) {
182+
setMaxFetchDepth(desc, config.fetch().maxDepth());
183+
} else if (config.maxFetchDepth().isPresent()) {
184+
setMaxFetchDepth(desc, config.maxFetchDepth());
185+
}
186+
187+
desc.getProperties().setProperty(AvailableSettings.QUERY_PLAN_CACHE_MAX_SIZE, Integer.toString(
188+
config.query().queryPlanCacheMaxSize()));
189+
190+
desc.getProperties().setProperty(AvailableSettings.DEFAULT_NULL_ORDERING,
191+
config.query().defaultNullOrdering().name().toLowerCase(Locale.ROOT));
192+
193+
desc.getProperties().setProperty(AvailableSettings.IN_CLAUSE_PARAMETER_PADDING,
194+
String.valueOf(config.query().inClauseParameterPadding()));
195+
196+
// Disable sequence validations: they are reportedly slow, and people already get the same validation from normal schema validation
197+
desc.getProperties().put(AvailableSettings.SEQUENCE_INCREMENT_SIZE_MISMATCH_STRATEGY,
198+
SequenceMismatchStrategy.NONE);
199+
200+
// JDBC
201+
config.jdbc().timezone().ifPresent(
202+
timezone -> desc.getProperties().setProperty(AvailableSettings.JDBC_TIME_ZONE, timezone));
203+
204+
config.jdbc().statementFetchSize().ifPresent(
205+
fetchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_FETCH_SIZE,
206+
String.valueOf(fetchSize)));
207+
208+
config.jdbc().statementBatchSize().ifPresent(
209+
fetchSize -> desc.getProperties().setProperty(AvailableSettings.STATEMENT_BATCH_SIZE,
210+
String.valueOf(fetchSize)));
211+
212+
// Statistics
213+
if (hibernateOrmConfig.metrics().enabled()
214+
|| (hibernateOrmConfig.statistics().isPresent() && hibernateOrmConfig.statistics().get())) {
215+
desc.getProperties().setProperty(AvailableSettings.GENERATE_STATISTICS, "true");
216+
//When statistics are enabled, the default in Hibernate ORM is to also log them after each
217+
// session; turn that off by default as it's very noisy:
218+
desc.getProperties().setProperty(AvailableSettings.LOG_SESSION_METRICS,
219+
String.valueOf(hibernateOrmConfig.logSessionMetrics().orElse(false)));
220+
}
221+
222+
// Caching
223+
configureCaching(desc, config);
224+
225+
// Validation
226+
configureValidation(desc, config);
227+
228+
// Discriminator Column
229+
desc.getProperties().setProperty(AvailableSettings.IGNORE_EXPLICIT_DISCRIMINATOR_COLUMNS_FOR_JOINED_SUBCLASS,
230+
String.valueOf(config.discriminator().ignoreExplicitForJoined()));
231+
}
232+
233+
// TODO ideally we should align on ORM and use 16 as a default, but that would break applications
234+
// because of https://github.com/hibernate/hibernate-reactive/issues/742
235+
private static int defaultBatchSize(boolean reactive) {
236+
return reactive ? -1 : 16;
237+
}
238+
239+
private static void setMaxFetchDepth(PersistenceUnitDescriptor descriptor, OptionalInt maxFetchDepth) {
240+
descriptor.getProperties().setProperty(AvailableSettings.MAX_FETCH_DEPTH, String.valueOf(maxFetchDepth.getAsInt()));
241+
}
242+
243+
private static List<String> getSqlLoadScript(Optional<List<String>> sqlLoadScript, LaunchMode launchMode) {
244+
if (sqlLoadScript.isPresent()) {
245+
return sqlLoadScript.get().stream()
246+
.filter(s -> !NO_SQL_LOAD_SCRIPT_FILE.equalsIgnoreCase(s))
247+
.collect(Collectors.toList());
248+
}
249+
if (launchMode == LaunchMode.NORMAL) {
250+
return Collections.emptyList();
251+
}
252+
return List.of("import.sql");
253+
}
254+
255+
private static boolean isMySQLOrMariaDB(Optional<String> dbKind, Optional<String> dialect) {
256+
if (dbKind.isPresent() && (DatabaseKind.isMySQL(dbKind.get()) || DatabaseKind.isMariaDB(dbKind.get()))) {
257+
return true;
258+
}
259+
if (dialect.isPresent()) {
260+
String lowercaseDialect = dialect.get().toLowerCase(Locale.ROOT);
261+
return lowercaseDialect.contains("mysql") || lowercaseDialect.contains("mariadb");
262+
}
263+
return false;
264+
}
265+
266+
private static void configureCaching(QuarkusPersistenceUnitDescriptor descriptor,
267+
HibernateOrmConfigPersistenceUnit config) {
268+
if (config.secondLevelCachingEnabled()) {
269+
Properties p = descriptor.getProperties();
270+
p.putIfAbsent(AvailableSettings.USE_DIRECT_REFERENCE_CACHE_ENTRIES, Boolean.TRUE);
271+
p.putIfAbsent(AvailableSettings.USE_SECOND_LEVEL_CACHE, Boolean.TRUE);
272+
p.putIfAbsent(AvailableSettings.USE_QUERY_CACHE, Boolean.TRUE);
273+
p.putIfAbsent(AvailableSettings.JAKARTA_SHARED_CACHE_MODE, SharedCacheMode.ENABLE_SELECTIVE);
274+
Map<String, String> cacheConfigEntries = HibernateConfigUtil.getCacheConfigEntries(config);
275+
for (Map.Entry<String, String> entry : cacheConfigEntries.entrySet()) {
276+
descriptor.getProperties().setProperty(entry.getKey(), entry.getValue());
277+
}
278+
} else {
279+
Properties p = descriptor.getProperties();
280+
p.put(AvailableSettings.USE_DIRECT_REFERENCE_CACHE_ENTRIES, Boolean.FALSE);
281+
p.put(AvailableSettings.USE_SECOND_LEVEL_CACHE, Boolean.FALSE);
282+
p.put(AvailableSettings.USE_QUERY_CACHE, Boolean.FALSE);
283+
p.put(AvailableSettings.JAKARTA_SHARED_CACHE_MODE, SharedCacheMode.NONE);
284+
}
285+
}
286+
287+
private static void configureValidation(QuarkusPersistenceUnitDescriptor descriptor,
288+
HibernateOrmConfigPersistenceUnit config) {
289+
if (!config.validation().enabled()) {
290+
descriptor.getProperties().setProperty(AvailableSettings.JAKARTA_VALIDATION_MODE, ValidationMode.NONE.name());
291+
} else {
292+
descriptor.getProperties().setProperty(
293+
AvailableSettings.JAKARTA_VALIDATION_MODE,
294+
config.validation().mode()
295+
.stream()
296+
.map(Enum::name)
297+
.collect(Collectors.joining(",")));
298+
}
299+
}
300+
301+
private static void configureQuoting(QuarkusPersistenceUnitDescriptor desc,
302+
HibernateOrmConfigPersistenceUnit persistenceUnitConfig) {
303+
if (persistenceUnitConfig.quoteIdentifiers()
304+
.strategy() == HibernateOrmConfigPersistenceUnit.IdentifierQuotingStrategy.ALL
305+
|| persistenceUnitConfig.quoteIdentifiers()
306+
.strategy() == HibernateOrmConfigPersistenceUnit.IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS
307+
|| persistenceUnitConfig.database().globallyQuotedIdentifiers()) {
308+
desc.getProperties().setProperty(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, "true");
309+
}
310+
if (persistenceUnitConfig.quoteIdentifiers()
311+
.strategy() == HibernateOrmConfigPersistenceUnit.IdentifierQuotingStrategy.ALL_EXCEPT_COLUMN_DEFINITIONS) {
312+
desc.getProperties().setProperty(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS_SKIP_COLUMN_DEFINITIONS, "true");
313+
} else if (persistenceUnitConfig.quoteIdentifiers()
314+
.strategy() == HibernateOrmConfigPersistenceUnit.IdentifierQuotingStrategy.ONLY_KEYWORDS) {
315+
desc.getProperties().setProperty(AvailableSettings.KEYWORD_AUTO_QUOTING_ENABLED, "true");
316+
}
317+
}
318+
319+
public static void configureSqlLoadScript(String persistenceUnitName,
320+
HibernateOrmConfigPersistenceUnit persistenceUnitConfig,
321+
ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchMode launchMode,
322+
BuildProducer<NativeImageResourceBuildItem> nativeImageResources,
323+
BuildProducer<HotDeploymentWatchedFileBuildItem> hotDeploymentWatchedFiles,
324+
QuarkusPersistenceUnitDescriptor descriptor) {
325+
// sql-load-scripts
326+
List<String> importFiles = getSqlLoadScript(persistenceUnitConfig.sqlLoadScript(), launchMode);
327+
if (!importFiles.isEmpty()) {
328+
for (String importFile : importFiles) {
329+
Path loadScriptPath;
330+
try {
331+
loadScriptPath = applicationArchivesBuildItem.getRootArchive().getChildPath(importFile);
332+
} catch (RuntimeException e) {
333+
throw new ConfigurationException(
334+
"Unable to interpret path referenced in '"
335+
+ HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "sql-load-script") + "="
336+
+ String.join(",", persistenceUnitConfig.sqlLoadScript().get())
337+
+ "': " + e.getMessage());
338+
}
339+
340+
if (loadScriptPath != null && !Files.isDirectory(loadScriptPath)) {
341+
// enlist resource if present
342+
nativeImageResources.produce(new NativeImageResourceBuildItem(importFile));
343+
} else if (persistenceUnitConfig.sqlLoadScript().isPresent()) {
344+
//raise exception if explicit file is not present (i.e. not the default)
345+
throw new ConfigurationException(
346+
"Unable to find file referenced in '"
347+
+ HibernateOrmRuntimeConfig.puPropertyKey(persistenceUnitName, "sql-load-script") + "="
348+
+ String.join(",", persistenceUnitConfig.sqlLoadScript().get())
349+
+ "'. Remove property or add file to your path.");
350+
}
351+
// in dev mode we want to make sure that we watch for changes to file even if it doesn't currently exist
352+
// as a user could still add it after performing the initial configuration
353+
hotDeploymentWatchedFiles.produce(new HotDeploymentWatchedFileBuildItem(importFile));
354+
}
355+
356+
// only set the found import files if configured
357+
if (persistenceUnitConfig.sqlLoadScript().isPresent()) {
358+
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_IMPORT_FILES, String.join(",", importFiles));
359+
}
360+
} else {
361+
//Disable implicit loading of the default import script (import.sql)
362+
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_IMPORT_FILES, "");
363+
descriptor.getProperties().setProperty(AvailableSettings.HBM2DDL_SKIP_DEFAULT_IMPORT_FILE, "true");
364+
}
365+
}
366+
}

0 commit comments

Comments
 (0)