Hibernate support (#2147)

* Hibernate support

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-12-16 16:08:07 +02:00
committed by GitHub
parent af50e8c938
commit db3ac7f2dd
51 changed files with 1397 additions and 704 deletions

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2024 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;
import java.util.List;
import jakarta.persistence.Query;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Slf4j
public class Jpa {
public enum JpaVendor {
ECLIPSELINK,
HIBERNATE
}
public static final JpaVendor JPA_VENDOR = JpaVendor.HIBERNATE;
static {
log.info("JPA vendor: {}", JPA_VENDOR);
}
public static final char NATIVE_QUERY_PARAMETER_PREFIX = ':';
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
return ":" + name;
}
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
deleteQuery.setParameter(name, list);
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* 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;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.cfg.MultiTenancySettings;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
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.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;
/**
* General Hibernate configuration for hawkBit's Repository.
*/
@Configuration
public class JpaConfiguration extends JpaBaseConfiguration {
private final TenantIdentifier tenantIdentifier;
protected JpaConfiguration(
final DataSource dataSource, final JpaProperties properties,
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider,
final TenantAware tenantAware) {
super(dataSource, properties, jtaTransactionManagerProvider);
tenantIdentifier = new TenantIdentifier(tenantAware);
}
@Bean
CurrentTenantIdentifierResolver<String> currentTenantIdentifierResolver() {
return tenantIdentifier;
}
@Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
return new HibernateJpaVendorAdapter() {
private final HibernateJpaDialect jpaDialect = new HibernateJpaDialect();
@Override
public HibernateJpaDialect getJpaDialect() {
return jpaDialect;
}
};
}
@Override
protected Map<String, Object> getVendorProperties() {
final Map<String, Object> properties = new HashMap<>(4);
properties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier);
properties.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
properties.put("hibernate.enable_lazy_load_no_trans", "true");
properties.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 properties;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2024 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;
import java.util.Optional;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
/**
* {@link CurrentTenantIdentifierResolver} and {@link HibernatePropertiesCustomizer} that resolves the
* {@link TenantAware#getCurrentTenant()} for hibernate.
*/
class TenantIdentifier implements CurrentTenantIdentifierResolver<String> {
private final TenantAware tenantAware;
TenantIdentifier(final TenantAware tenantAware) {
this.tenantAware = tenantAware;
}
@Override
public String resolveCurrentTenantIdentifier() {
// on bootstrapping hibernate requests tenant and want to be non-null
return Optional.ofNullable(tenantAware.getCurrentTenant()).map(String::toUpperCase).orElse("");
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* 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.model;
import java.io.Serial;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
/**
* Base hawkBit entity class containing the common attributes for Hibernate.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@MappedSuperclass
// exception squid:S2160 - BaseEntity equals/hashcode is handling correctly for sub entities
@SuppressWarnings("squid:S2160")
public abstract class AbstractJpaBaseEntity extends AbstractBaseEntity {
protected static final int USERNAME_FIELD_LENGTH = 64;
@Serial
private static final long serialVersionUID = 1L;
@Setter // should be used just for test purposes
@Getter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Setter // should be used just for test purposes
@Getter
@Version
@Column(name = "optlock_revision")
private int optLockRevision;
// Audit fields. use property access to ensure that setters will be called and checked for modification
// (touch implementation depends on setLastModifiedAt(1).
private String createdBy;
private long createdAt;
private String lastModifiedBy;
private long lastModifiedAt;
@CreatedBy
public void setCreatedBy(final String createdBy) {
this.createdBy = createdBy;
}
@Column(name = "created_by", updatable = false, nullable = false, length = USERNAME_FIELD_LENGTH)
@Access(AccessType.PROPERTY)
public String getCreatedBy() {
return createdBy;
}
@CreatedDate
public void setCreatedAt(final long createdAt) {
this.createdAt = createdAt;
}
@Column(name = "created_at", updatable = false, nullable = false)
@Access(AccessType.PROPERTY)
public long getCreatedAt() {
return createdAt;
}
@LastModifiedBy
public void setLastModifiedBy(final String lastModifiedBy) {
if (this.lastModifiedBy != null && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedBy = lastModifiedBy;
}
@Column(name = "last_modified_by", nullable = false, length = USERNAME_FIELD_LENGTH)
@Access(AccessType.PROPERTY)
public String getLastModifiedBy() {
return lastModifiedBy == null ? createdBy : lastModifiedBy;
}
@LastModifiedDate
public void setLastModifiedAt(final long lastModifiedAt) {
if (this.lastModifiedAt != 0 && isController()) {
// initialized and controller = doesn't update
return;
}
this.lastModifiedAt = lastModifiedAt;
}
@Column(name = "last_modified_at", nullable = false)
@Access(AccessType.PROPERTY)
public long getLastModifiedAt() {
return lastModifiedAt == 0 ? createdAt : lastModifiedAt;
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* 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.model;
import java.io.Serial;
import java.util.Objects;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.hawkbit.repository.exception.TenantNotExistException;
import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder;
import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity;
import org.hibernate.annotations.TenantId;
/**
* Holder of the base attributes common to all tenant aware entities.
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED) // Default constructor needed for JPA entities.
@Setter
@Getter
@MappedSuperclass
public abstract class AbstractJpaTenantAwareBaseEntity extends AbstractJpaBaseEntity implements TenantAwareBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Column(name = "tenant", nullable = false, insertable = true, updatable = false, length = 40)
@Size(min = 1, max = 40)
@NotNull
@TenantId // Hibernate MultiTenant support
private String tenant;
/**
* Tenant aware entities extend the equals/hashcode strategy with the tenant name. That would allow for instance in a
* multi-schema based data separation setup to have the same primary key for different entities of different tenants.
*/
@Override
public int hashCode() {
return 31 * super.hashCode() + (tenant == null ? 0 : tenant.hashCode());
}
/**
* Tenant aware entities extend the equals/hashcode strategy with the tenant name. That would allow for instance in a
* multi-schema based data separation setup to have the same primary key for different entities of
* different tenants.
*/
@Override
// exception squid:S2259 - obj is checked for null in super
@SuppressWarnings("squid:S2259")
public boolean equals(final Object obj) {
if (!super.equals(obj)) {
return false;
}
return Objects.equals(getTenant(), ((AbstractJpaTenantAwareBaseEntity) obj).getTenant());
}
@Override
public String toString() {
return getClass().getSimpleName() + " [tenant=" + getTenant() + ", id=" + getId() + "]";
}
/**
* PrePersist listener method for all {@link TenantAwareBaseEntity} entities.
*
* // TODO - check if the tenant support should set tenant from context
* // TODO - should we check if tenant exists in the system? Note: seems it's not good to work with db in the listener
*/
@PrePersist
void prePersist() {
// before persisting the entity check the current ID of the tenant by using the TenantAware service
final String currentTenant = TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant();
if (currentTenant == null) {
throw new TenantNotExistException(
String.format(
"Tenant %s does not exists, cannot create entity %s with id %d",
TenantAwareHolder.getInstance().getTenantAware().getCurrentTenant(), getClass(), getId()));
}
setTenant(currentTenant.toUpperCase());
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* 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.model;
import java.util.List;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.persister.entity.EntityPersister;
/**
* Listens to updates on <code></code>JpaTarget</code> entities, filtering out updates that only change the
* "lastTargetQuery" or "address" fields.
*/
public class EntityPropertyChangeListener implements PostUpdateEventListener {
private static final List<String> TARGET_UPDATE_EVENT_IGNORE_FIELDS = List.of(
"lastTargetQuery", "address", // actual to be skipped
"optLockRevision", "lastModifiedAt", "lastModifiedBy" // system to be skipped
);
private static final Class<?> JPA_TARGET;
static {
try {
JPA_TARGET = Class.forName("org.eclipse.hawkbit.repository.jpa.model.JpaTarget");
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
@Override
public void onPostUpdate(final PostUpdateEvent event) {
if (!JPA_TARGET.isAssignableFrom(event.getEntity().getClass())) {
// only target entity updates goes through here
return;
}
boolean lastTargetQueryChanged = false;
boolean hasNonIgnoredChanges = false;
for (int i : event.getDirtyProperties()) {
final String attribute = event.getPersister().getAttributeMapping(i).getAttributeName();
if ("lastTargetQuery".equals(attribute)) {
lastTargetQueryChanged = true;
} else if (!TARGET_UPDATE_EVENT_IGNORE_FIELDS.contains(attribute)) {
hasNonIgnoredChanges = true;
break;
}
}
if (hasNonIgnoredChanges || !lastTargetQueryChanged) {
AbstractJpaBaseEntity.doNotify(() -> ((EventAwareEntity) event.getEntity()).fireUpdateEvent());
}
}
@Override
public boolean requiresPostCommitHandling(final EntityPersister persister) {
return false;
}
}