diff --git a/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/JpaExceptionTranslator.java b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/JpaExceptionTranslator.java new file mode 100644 index 000000000..00429c419 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa-api/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/JpaExceptionTranslator.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.utils; + +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLErrorCodes; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; + +/** + * A single point of exception translators in hawkBit + * in order to be used in Hibernate and EclipseLink implementation + * and unify jpa exception translations behaviour in the project + */ +public class JpaExceptionTranslator { + + private static final SQLErrorCodeSQLExceptionTranslator SQL_EXCEPTION_TRANSLATOR; + + // providing list/set of codes which are not handled from the sql translator properly + private static final String[] DATA_INTEGRITY_VIOLATION_CODES = new String[] { + "1366" + }; + private static final String[] DUPLICATE_KEY_VIOLATION_CODES = new String[] { + "1062" + }; + + static { + SQL_EXCEPTION_TRANSLATOR = new SQLErrorCodeSQLExceptionTranslator(); + SQLErrorCodes codes = new SQLErrorCodes(); + + codes.setDataIntegrityViolationCodes(DATA_INTEGRITY_VIOLATION_CODES); + codes.setDuplicateKeyCodes(DUPLICATE_KEY_VIOLATION_CODES); + SQL_EXCEPTION_TRANSLATOR.setSqlErrorCodes(codes); + // explicitly set old translator as a fallback (uses Subclass translator by default) + SQL_EXCEPTION_TRANSLATOR.setFallbackTranslator(new SQLStateSQLExceptionTranslator()); + } + + private JpaExceptionTranslator() {} + + public static SQLExceptionTranslator getTranslator() { + return SQL_EXCEPTION_TRANSLATOR; + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java index 5336f64bb..0c3996a01 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java +++ b/hawkbit-repository/hawkbit-repository-jpa-eclipselink/src/main/java/org/eclipse/hawkbit/repository/jpa/HawkbitEclipseLinkJpaDialect.java @@ -14,9 +14,8 @@ import java.sql.SQLException; import jakarta.persistence.PersistenceException; +import org.eclipse.hawkbit.repository.jpa.utils.JpaExceptionTranslator; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; -import org.springframework.jdbc.support.SQLErrorCodes; import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; import org.springframework.lang.NonNull; import org.springframework.orm.jpa.JpaSystemException; @@ -56,23 +55,6 @@ class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect { @Serial private static final long serialVersionUID = 1L; - private static final SQLErrorCodeSQLExceptionTranslator SQL_EXCEPTION_TRANSLATOR; - - // providing list/set of codes which are not handled from the sql translator properly - private static final String[] DATA_INTEGRITY_VIOLATION_CODES = new String[] { - "1366" - }; - - static { - SQL_EXCEPTION_TRANSLATOR = new SQLErrorCodeSQLExceptionTranslator(); - SQLErrorCodes codes = new SQLErrorCodes(); - - codes.setDataIntegrityViolationCodes(DATA_INTEGRITY_VIOLATION_CODES); - SQL_EXCEPTION_TRANSLATOR.setSqlErrorCodes(codes); - // explicitly set old translator as a fallback (uses Subclass translator by default) - SQL_EXCEPTION_TRANSLATOR.setFallbackTranslator(new SQLStateSQLExceptionTranslator()); - } - @Override public DataAccessException translateExceptionIfPossible(@NonNull final RuntimeException ex) { final DataAccessException dataAccessException = super.translateExceptionIfPossible(ex); @@ -102,7 +84,7 @@ class HawkbitEclipseLinkJpaDialect extends EclipseLinkJpaDialect { return null; } - return SQL_EXCEPTION_TRANSLATOR.translate("", null, sqlException); + return JpaExceptionTranslator.getTranslator().translate("", null, sqlException); } private static SQLException findSqlException(final RuntimeException jpaSystemException) { diff --git a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java index 002ecb293..499c3d627 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa-hibernate/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaConfiguration.java @@ -10,9 +10,12 @@ package org.eclipse.hawkbit.repository.jpa; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener; +import org.eclipse.hawkbit.repository.jpa.utils.JpaExceptionTranslator; import org.eclipse.hawkbit.tenancy.TenantAware; import org.hibernate.boot.Metadata; import org.hibernate.boot.spi.BootstrapContext; @@ -24,23 +27,33 @@ import org.hibernate.event.spi.EventType; import org.hibernate.integrator.spi.Integrator; import org.hibernate.jpa.boot.spi.IntegratorProvider; import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.jta.JtaTransactionManager; + +import javax.sql.DataSource; /** * General Hibernate configuration for hawkBit's Repository. */ @Configuration -public class JpaConfiguration { +public class JpaConfiguration extends JpaBaseConfiguration { private final TenantIdentifier tenantIdentifier; private final boolean enableLazyLoadNoTrans; - protected JpaConfiguration( + final DataSource dataSource, final JpaProperties properties, + final ObjectProvider jtaTransactionManagerProvider, final TenantAware.TenantResolver tenantResolver, @Value("${hibernate.enable-lazy-load-no-trans:true}") final boolean enableLazyLoadNoTrans) { + super(dataSource, properties, jtaTransactionManagerProvider); tenantIdentifier = new TenantIdentifier(tenantResolver); this.enableLazyLoadNoTrans = enableLazyLoadNoTrans; } @@ -50,33 +63,53 @@ public class JpaConfiguration { return tenantIdentifier; } - @Bean - HibernatePropertiesCustomizer hibernatePropertiesCustomizers() { - return hibernateProperties -> { - // override the default naming strategy - hibernateProperties.put("hibernate.physical_naming_strategy", org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.class.getName()); - hibernateProperties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier); - hibernateProperties.put("hibernate.multiTenancy", "DISCRIMINATOR"); - // LAZY_LOAD - Enable lazy loading of lazy fields when session is closed - N + 1 problem occur. - // So it would be good if in future hawkBit run without that - // Otherwise, if false, call for the lazy field will throw LazyInitializationException - hibernateProperties.put("hibernate.enable_lazy_load_no_trans", enableLazyLoadNoTrans); - hibernateProperties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() { + @Override + protected AbstractJpaVendorAdapter createJpaVendorAdapter() { - @Override - public void integrate( - final Metadata metadata, final BootstrapContext bootstrapContext, - final SessionFactoryImplementor sessionFactory) { - sessionFactory.getServiceRegistry() - .getService(EventListenerRegistry.class) - .appendListeners(EventType.POST_UPDATE, new EntityPropertyChangeListener()); - } + return new HibernateJpaVendorAdapter() { + private final HibernateJpaDialect hibernateJpaDialect = new CustomHibernateJpaDialect(); - @Override - public void disintegrate(final SessionFactoryImplementor sessionFactory, final SessionFactoryServiceRegistry serviceRegistry) { - // do nothing - } - })); + @Override + public HibernateJpaDialect getJpaDialect() { + return hibernateJpaDialect; + } }; } + + @Override + protected Map getVendorProperties(DataSource dataSource) { + Map hibernateProperties = new HashMap<>(); + hibernateProperties.put("hibernate.physical_naming_strategy", org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.class.getName()); + hibernateProperties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier); + hibernateProperties.put("hibernate.multiTenancy", "DISCRIMINATOR"); + // LAZY_LOAD - Enable lazy loading of lazy fields when session is closed - N + 1 problem occur. + // So it would be good if in future hawkBit run without that + // Otherwise, if false, call for the lazy field will throw LazyInitializationException + hibernateProperties.put("hibernate.enable_lazy_load_no_trans", enableLazyLoadNoTrans); + hibernateProperties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() { + + @Override + public void integrate( + final Metadata metadata, final BootstrapContext bootstrapContext, + final SessionFactoryImplementor sessionFactory) { + sessionFactory.getServiceRegistry() + .getService(EventListenerRegistry.class) + .appendListeners(EventType.POST_UPDATE, new EntityPropertyChangeListener()); + } + + @Override + public void disintegrate(final SessionFactoryImplementor sessionFactory, final SessionFactoryServiceRegistry serviceRegistry) { + // do nothing + } + })); + return hibernateProperties; + } + + static class CustomHibernateJpaDialect extends HibernateJpaDialect { + + protected CustomHibernateJpaDialect() { + super(); + this.setJdbcExceptionTranslator(JpaExceptionTranslator.getTranslator()); + } + } } \ No newline at end of file