Hibernate support (#2147)
* Hibernate support --------- Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user