From cc3cf6b51eea0d61892c03c6fba994c45e5eec84 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Tue, 2 May 2023 22:24:52 +0800 Subject: [PATCH 1/8] Add data store customizer --- .../elide/datastores/jpa/JpaDataStore.java | 22 ++- .../spring/config/ElideAutoConfiguration.java | 127 ++++++++++++++---- .../orm/jpa/config/EnableJpaDataStore.java | 36 +++++ .../orm/jpa/config/EnableJpaDataStores.java | 23 ++++ .../jpa/config/JpaDataStoreRegistration.java | 42 ++++++ .../JpaDataStoreRegistrationsBuilder.java | 40 ++++++ ...taStoreRegistrationsBuilderCustomizer.java | 16 +++ .../config/ElideAutoConfigurationTest.java | 119 ++++++++++++++++ .../models/jpa/v3/ArtifactGroupV3.java | 27 ++++ .../example/models/jpa/v3/package-info.java | 12 ++ 10 files changed, 428 insertions(+), 36 deletions(-) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStores.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilder.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilderCustomizer.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/ArtifactGroupV3.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/package-info.java diff --git a/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java b/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java index 64154df865..9835d4fc0c 100644 --- a/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java +++ b/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java @@ -42,41 +42,49 @@ private JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier writeTransactionSupplier, QueryLogger logger, MetamodelSupplier metamodelSupplier, - Type ... models) { + Type[] models) { this.entityManagerSupplier = entityManagerSupplier; this.readTransactionSupplier = readTransactionSupplier; this.writeTransactionSupplier = writeTransactionSupplier; this.metamodelSupplier = metamodelSupplier; this.logger = logger; this.modelsToBind = new HashSet<>(); - Collections.addAll(this.modelsToBind, models); + if (models != null) { + Collections.addAll(this.modelsToBind, models); + } + if (this.metamodelSupplier == null && this.modelsToBind.isEmpty()) { + throw new IllegalArgumentException( + "Either the metamodel supplier or the explicit models to bind needs to be provided."); + } } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier readTransactionSupplier, JpaTransactionSupplier writeTransactionSupplier, + QueryLogger logger, MetamodelSupplier metamodelSupplier) { - this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, DEFAULT_LOGGER, - metamodelSupplier); + this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, logger, + metamodelSupplier, null); } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier readTransactionSupplier, JpaTransactionSupplier writeTransactionSupplier, + QueryLogger logger, Type ... models) { - this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, DEFAULT_LOGGER, null, models); + this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, logger, null, models); } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier transactionSupplier, MetamodelSupplier metamodelSupplier) { - this(entityManagerSupplier, transactionSupplier, transactionSupplier, metamodelSupplier); + this(entityManagerSupplier, transactionSupplier, transactionSupplier, DEFAULT_LOGGER, metamodelSupplier); } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier transactionSupplier, Type ... models) { - this(entityManagerSupplier, transactionSupplier, transactionSupplier, models); + this(entityManagerSupplier, transactionSupplier, transactionSupplier, DEFAULT_LOGGER, models); } @Override diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java index 0620d9f93f..6e7f450d27 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java @@ -68,6 +68,11 @@ import com.yahoo.elide.spring.orm.jpa.PlatformJpaTransactionSupplier; import com.yahoo.elide.swagger.OpenApiBuilder; import com.yahoo.elide.utils.HeaderUtils; +import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; +import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStores; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistration; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilder; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilderCustomizer; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.customizers.OpenApiCustomizer; @@ -83,6 +88,7 @@ import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; @@ -360,10 +366,6 @@ public QueryEngine buildQueryEngine(DataSource defaultDataSource, * @param settings Elide configuration settings. * @return the JpaTransactionSupplier */ - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(PlatformTransactionManager.class) - @Scope(SCOPE_PROTOTYPE) public JpaTransactionSupplier jpaTransactionSupplier(PlatformTransactionManager transactionManager, EntityManagerFactory entityManagerFactory, ElideConfigProperties settings) { return new PlatformJpaTransactionSupplier( @@ -375,20 +377,86 @@ public JpaTransactionSupplier jpaTransactionSupplier(PlatformTransactionManager * Create an Entity Manager Supplier to use. * @return the EntityManagerSupplier */ + public EntityManagerSupplier entityManagerSupplier() { + return new EntityManagerProxySupplier(); + } + @Bean @ConditionalOnMissingBean @Scope(SCOPE_PROTOTYPE) - public EntityManagerSupplier entityManagerSupplier() { - return new EntityManagerProxySupplier(); + public JpaDataStoreRegistrationsBuilder jpaDataStoreRegistrationsBuilder( + ApplicationContext applicationContext, + ElideConfigProperties settings, + Optional optionalQueryLogger, + ObjectProvider customizerProviders) { + JpaDataStoreRegistrationsBuilder builder = new JpaDataStoreRegistrationsBuilder(); + String[] entityManagerFactoryNames = applicationContext.getBeanNamesForType(EntityManagerFactory.class); + String[] platformTransactionManagerNames = applicationContext + .getBeanNamesForType(PlatformTransactionManager.class); + + if (entityManagerFactoryNames.length == 1 && platformTransactionManagerNames.length == 1) { + // Basic scenario + String platformTransactionManagerName = platformTransactionManagerNames[0]; + String entityManagerFactoryName = entityManagerFactoryNames[0]; + builder.add(jpaDataStoreRegistration(applicationContext, entityManagerFactoryName, + platformTransactionManagerName, settings, optionalQueryLogger)); + } else { + // Multiple scenario + Map beans = new HashMap<>(); + beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStore.class)); + beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStores.class)); + if (!beans.isEmpty()) { + beans.values().stream().forEach(bean -> { + EnableJpaDataStore[] annotations = bean.getClass() + .getAnnotationsByType(EnableJpaDataStore.class); + for (EnableJpaDataStore annotation : annotations) { + String entityManagerFactoryName = annotation.entityManagerFactoryRef(); + String platformTransactionManagerName = annotation.transactionManagerRef(); + if (!StringUtils.isBlank(entityManagerFactoryName) + && !StringUtils.isBlank(platformTransactionManagerName)) { + builder.add(jpaDataStoreRegistration(applicationContext, entityManagerFactoryName, + platformTransactionManagerName, settings, optionalQueryLogger)); + } + } + }); + } + } + + customizerProviders.orderedStream().forEach(customizer -> customizer.customize(builder)); + return builder; + } + + /** + * Creates a JpaDataStore registration from inputs. + * + * @param applicationContext the application context + * @param entityManagerFactoryName the bean name of the entity manager factory + * @param platformTransactionManagerName the bean name of the platform transaction manager + * @param settings the settings + * @param optionalQueryLogger the optional query logger + * @return + */ + private JpaDataStoreRegistration jpaDataStoreRegistration(ApplicationContext applicationContext, + String entityManagerFactoryName, String platformTransactionManagerName, ElideConfigProperties settings, + Optional optionalQueryLogger) { + PlatformTransactionManager transactionManager = applicationContext + .getBean(platformTransactionManagerName, PlatformTransactionManager.class); + EntityManagerFactory entityManagerFactory = applicationContext.getBean(entityManagerFactoryName, + EntityManagerFactory.class); + JpaTransactionSupplier jpaTransactionSupplier = jpaTransactionSupplier(transactionManager, + entityManagerFactory, settings); + return JpaDataStoreRegistration.builder().name(entityManagerFactoryName) + .entityManagerSupplier(entityManagerSupplier()).readTransactionSupplier(jpaTransactionSupplier) + .writeTransactionSupplier(jpaTransactionSupplier) + .metamodelSupplier(entityManagerFactory::getMetamodel) + .logger(optionalQueryLogger.orElse(JpaDataStore.DEFAULT_LOGGER)).build(); } /** * Creates the DataStore Elide. Override to use a different store. + * @param builder JpaDataStoreRegistrationsBuilder. * @param settings Elide configuration settings. - * @param entityManagerFactory The JPA factory which creates entity managers. * @param scanner Class Scanner - * @param jpaTransactionSupplier JPA Transaction supplier - * @param entityManagerSupplier Entity Manager supplier * @param optionalQueryEngine QueryEngine instance for aggregation data store. * @param optionalCache Analytics query cache * @param optionalQueryLogger Analytics query logger @@ -398,41 +466,43 @@ public EntityManagerSupplier entityManagerSupplier() { @Bean @ConditionalOnMissingBean @Scope(SCOPE_PROTOTYPE) - public DataStore dataStore(ElideConfigProperties settings, EntityManagerFactory entityManagerFactory, - ClassScanner scanner, JpaTransactionSupplier jpaTransactionSupplier, - EntityManagerSupplier entityManagerSupplier, Optional optionalQueryEngine, + public DataStore dataStore(JpaDataStoreRegistrationsBuilder builder, ElideConfigProperties settings, + ClassScanner scanner, Optional optionalQueryEngine, Optional optionalCache, Optional optionalQueryLogger, ObjectProvider>> customizerProvider) { - return buildDataStore(settings, entityManagerFactory, scanner, jpaTransactionSupplier, jpaTransactionSupplier, - entityManagerSupplier, optionalQueryEngine, optionalCache, optionalQueryLogger, Optional.of( + return buildDataStore(builder, settings, scanner, optionalQueryEngine, optionalCache, optionalQueryLogger, + Optional.of( stores -> customizerProvider.orderedStream().forEach(customizer -> customizer.accept(stores)))); } /** * Creates the DataStore Elide. + * @param builder JpaDataStoreRegistrationsBuilder. * @param settings Elide configuration settings. - * @param entityManagerFactory The JPA factory which creates entity managers. * @param scanner Class Scanner - * @param readJpaTransactionSupplier Read JPA Transaction supplier - * @param writeJpaTransactionSupplier Write JPA Transaction supplier - * @param entityManagerSupplier Entity Manager supplier * @param optionalQueryEngine QueryEngine instance for aggregation data store. * @param optionalCache Analytics query cache * @param optionalQueryLogger Analytics query logger * @param optionalCustomizer Provide customizers to add to the data store * @return An instance of a JPA DataStore. */ - public static DataStore buildDataStore(ElideConfigProperties settings, EntityManagerFactory entityManagerFactory, - ClassScanner scanner, JpaTransactionSupplier readJpaTransactionSupplier, - JpaTransactionSupplier writeJpaTransactionSupplier, EntityManagerSupplier entityManagerSupplier, + public static DataStore buildDataStore(JpaDataStoreRegistrationsBuilder builder, ElideConfigProperties settings, + ClassScanner scanner, Optional optionalQueryEngine, Optional optionalCache, Optional optionalQueryLogger, Optional>> optionalCustomizer) { List stores = new ArrayList<>(); - JpaDataStore jpaDataStore = new JpaDataStore(entityManagerSupplier, readJpaTransactionSupplier, - writeJpaTransactionSupplier, entityManagerFactory::getMetamodel); - - stores.add(jpaDataStore); + builder.build().forEach(registration -> { + if (registration.getModelsToBind() != null && !registration.getModelsToBind().isEmpty()) { + stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), + registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), + registration.getLogger(), registration.getModelsToBind().toArray(Type[]::new))); + } else { + stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), + registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), + registration.getLogger(), registration.getMetamodelSupplier())); + } + }); if (isAggregationStoreEnabled(settings)) { AggregationDataStore.AggregationDataStoreBuilder aggregationDataStoreBuilder = AggregationDataStore @@ -442,10 +512,9 @@ public static DataStore buildDataStore(ElideConfigProperties settings, EntityMan if (isDynamicConfigEnabled(settings)) { optionalQueryEngine.ifPresent(queryEngine -> aggregationDataStoreBuilder .dynamicCompiledClasses(queryEngine.getMetaDataStore().getDynamicTypes())); - if (settings.getAggregationStore().getDynamicConfig().getConfigApi().isEnabled()) { - stores.add(new ConfigDataStore(settings.getAggregationStore().getDynamicConfig().getPath(), - new TemplateConfigValidator(scanner, - settings.getAggregationStore().getDynamicConfig().getPath()))); + if (settings.getDynamicConfig().isConfigApiEnabled()) { + stores.add(new ConfigDataStore(settings.getDynamicConfig().getPath(), + new TemplateConfigValidator(scanner, settings.getDynamicConfig().getPath()))); } } optionalCache.ifPresent(aggregationDataStoreBuilder::cache); diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java new file mode 100644 index 0000000000..6da096daa0 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to configure multiple JpaDataStores. + * + * @see com.yahoo.elide.datastores.jpa.JpaDataStore + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(EnableJpaDataStores.class) +public @interface EnableJpaDataStore { + /** + * The entity manager factory bean name to be used for the JpaDataStore. + * + * @return the entity manager factory bean name. + */ + String entityManagerFactoryRef() default "entityManagerFactory"; + + /** + * The platform transaction manager bean name to be used for the JpaDataStore. + * + * @return the platform transaction manager bean name. + */ + String transactionManagerRef() default "transactionManager"; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStores.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStores.java new file mode 100644 index 0000000000..6f100480f8 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStores.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to configure multiple JpaDataStores. + * + * @see com.yahoo.elide.datastores.jpa.JpaDataStore + * @see com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnableJpaDataStores { + EnableJpaDataStore[] value(); +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java new file mode 100644 index 0000000000..638f9b1dde --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + +import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.datastores.jpa.JpaDataStore.EntityManagerSupplier; +import com.yahoo.elide.datastores.jpa.JpaDataStore.JpaTransactionSupplier; +import com.yahoo.elide.datastores.jpa.JpaDataStore.MetamodelSupplier; +import com.yahoo.elide.datastores.jpql.porting.QueryLogger; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.util.Set; + +/** + * Registration entry to configure a JpaDataStore. + * + * @see com.yahoo.elide.datastores.jpa.JpaDataStore + */ +@Builder +@AllArgsConstructor +public class JpaDataStoreRegistration { + @Getter + private final String name; + @Getter + private final EntityManagerSupplier entityManagerSupplier; + @Getter + private final JpaTransactionSupplier readTransactionSupplier; + @Getter + private final JpaTransactionSupplier writeTransactionSupplier; + @Getter + private final MetamodelSupplier metamodelSupplier; + @Getter + private final Set> modelsToBind; + @Getter + private final QueryLogger logger; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilder.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilder.java new file mode 100644 index 0000000000..8adf806247 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilder.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Builder used to configure registration entries for building a JpaDataStore. + * + * @see com.yahoo.elide.datastores.jpa.JpaDataStore + * @see com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilderCustomizer + */ +public class JpaDataStoreRegistrationsBuilder { + private final List registrations = new ArrayList<>(); + + public JpaDataStoreRegistrationsBuilder registrations(Consumer> customizer) { + customizer.accept(this.registrations); + return this; + } + + public JpaDataStoreRegistrationsBuilder registrations(List registrations) { + this.registrations.clear(); + this.registrations.addAll(registrations); + return this; + } + + public JpaDataStoreRegistrationsBuilder add(JpaDataStoreRegistration registration) { + this.registrations.add(registration); + return this; + } + + public List build() { + return this.registrations; + } +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilderCustomizer.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilderCustomizer.java new file mode 100644 index 0000000000..987c7cbca1 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrationsBuilderCustomizer.java @@ -0,0 +1,16 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + +/** + * Customizer to customize registration entries for building JpaDataStores. + * + * @see com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilder + */ +@FunctionalInterface +public interface JpaDataStoreRegistrationsBuilderCustomizer { + void customize(JpaDataStoreRegistrationsBuilder builder); +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java index 64591bf9ca..13cdeb7132 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java @@ -6,28 +6,44 @@ package com.yahoo.elide.spring.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.yahoo.elide.RefreshableElide; import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreTransaction; +import com.yahoo.elide.core.exceptions.TransactionException; +import com.yahoo.elide.datastores.multiplex.MultiplexManager; +import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; import example.models.jpa.ArtifactGroup; +import example.models.jpa.v2.ArtifactGroupV2; +import example.models.jpa.v3.ArtifactGroupV3; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.RestController; @@ -37,9 +53,13 @@ import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import javax.sql.DataSource; + /** * Tests for ElideAutoConfiguration. */ @@ -261,4 +281,103 @@ void dataStoreTransaction() { }); } + + @Configuration(proxyBeanMethods = false) + public static class MultipleDataSourceConfiguration { + @Bean + public DataSource dataSourceV2() { + return DataSourceBuilder.create().url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1").build(); + } + + @Bean + public DataSource dataSourceV3() { + return DataSourceBuilder.create().url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1").build(); + } + + @Bean + public EntityManagerFactoryBuilder entityManagerFactoryBuilder( + ObjectProvider persistenceUnitManager, + ObjectProvider customizers) { + EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), + new HashMap<>(), persistenceUnitManager.getIfAvailable()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + } + + @Configuration(proxyBeanMethods = false) + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManagerV2") + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManagerV3") + public static class MultipleEntityManagerFactoryConfiguration { + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV2) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put("hibernate.hbm2ddl.auto", "create-drop"); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) + .packages("example.models.jpa.v2").properties(vendorProperties).build(); + return emf; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV3) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put("hibernate.hbm2ddl.auto", "create-drop"); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) + .packages("example.models.jpa.v3").properties(vendorProperties).build(); + return emf; + } + + @Bean + public PlatformTransactionManager transactionManagerV2(EntityManagerFactory entityManagerFactoryV2) { + return new JpaTransactionManager(entityManagerFactoryV2); + } + + @Bean + public PlatformTransactionManager transactionManagerV3(EntityManagerFactory entityManagerFactoryV3) { + return new JpaTransactionManager(entityManagerFactoryV3); + } + } + + @Test + void multiplexDataStoreTransaction() { + contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") + .withUserConfiguration(MultipleDataSourceConfiguration.class, MultipleEntityManagerFactoryConfiguration.class).run(context -> { + DataStore dataStore = context.getBean(DataStore.class); + assertThat(dataStore).isInstanceOf(MultiplexManager.class); + + // The data store will only be initialized properly by elide to populate the dictionary + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + dataStore = refreshableElide.getElide().getDataStore(); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("Group V2"); + transaction.save(artifactGroupV2, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("Group V3"); + transaction.save(artifactGroupV3, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("Group V2"); + + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("Group V3"); + + transaction.save(artifactGroupV2, null); + transaction.save(artifactGroupV3, null); + + assertThatThrownBy(() -> transaction.commit(null)).isInstanceOf(TransactionException.class) + .message().isEqualTo("Transaction synchronization is not active"); + } + }); + } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/ArtifactGroupV3.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/ArtifactGroupV3.java new file mode 100644 index 0000000000..9aceac0de4 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/ArtifactGroupV3.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package example.models.jpa.v3; + +import com.yahoo.elide.annotation.Include; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +@Include(name = "group") +@Entity +@Data +@Table(name = "ArtifactGroup") +public class ArtifactGroupV3 { + @Id + private String name = ""; + + @Column(name = "commonName") + private String title = ""; +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/package-info.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/package-info.java new file mode 100644 index 0000000000..61da7770a3 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v3/package-info.java @@ -0,0 +1,12 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +/** + * Models Package V3. + */ +@ApiVersion(version = "3.0") +package example.models.jpa.v3; + +import com.yahoo.elide.annotation.ApiVersion; From 4c78296ab9ed6521403515c274f4d788bc147da7 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Wed, 3 May 2023 17:12:00 +0800 Subject: [PATCH 2/8] Add jta transaction manager for testing --- .../elide-spring-boot-autoconfigure/pom.xml | 8 ++ .../orm/jpa/PlatformJpaTransaction.java | 12 +- .../config/ElideAutoConfigurationTest.java | 132 ++++++++++++++++-- .../test/resources/transactions.properties | 1 + 4 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/resources/transactions.properties diff --git a/elide-spring/elide-spring-boot-autoconfigure/pom.xml b/elide-spring/elide-spring-boot-autoconfigure/pom.xml index b35e4b71bf..1bf8379ef5 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/pom.xml +++ b/elide-spring/elide-spring-boot-autoconfigure/pom.xml @@ -39,6 +39,7 @@ utf-8 + 6.0.0 @@ -268,6 +269,13 @@ junit-jupiter-engine test + + + com.atomikos + transactions-spring-boot3-starter + ${atomikos.version} + test + org.junit.platform diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java index dbf5093e4b..0710c22d16 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java @@ -19,7 +19,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; -import java.util.Objects; import java.util.function.Consumer; /** @@ -51,9 +50,14 @@ public PlatformJpaTransaction(PlatformTransactionManager transactionManager, Tra public void begin() { this.status = this.transactionManager.getTransaction(this.definition); if (this.em instanceof SupplierEntityManager supplierEntityManager) { - EntityManagerHolder entityManagerHolder = (EntityManagerHolder) Objects - .requireNonNull(TransactionSynchronizationManager.getResource(this.entityManagerFactory)); - supplierEntityManager.setEntityManager(entityManagerHolder.getEntityManager()); + EntityManagerHolder entityManagerHolder = (EntityManagerHolder) TransactionSynchronizationManager + .getResource(this.entityManagerFactory); + if (entityManagerHolder != null) { + // This is the JpaTransactionManager + supplierEntityManager.setEntityManager(entityManagerHolder.getEntityManager()); + } else { + supplierEntityManager.setEntityManager(this.entityManagerFactory.createEntityManager()); + } } else { throw new IllegalStateException("Expected entity manager to be supplied by EntityManagerProxySupplier"); } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java index 13cdeb7132..dd0a6c0f03 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java @@ -6,19 +6,22 @@ package com.yahoo.elide.spring.config; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.yahoo.elide.RefreshableElide; import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreTransaction; -import com.yahoo.elide.core.exceptions.TransactionException; import com.yahoo.elide.datastores.multiplex.MultiplexManager; import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; +import com.atomikos.spring.AtomikosAutoConfiguration; +import com.atomikos.spring.AtomikosDataSourceBean; + import example.models.jpa.ArtifactGroup; import example.models.jpa.v2.ArtifactGroupV2; import example.models.jpa.v3.ArtifactGroupV3; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -35,6 +38,7 @@ import org.springframework.boot.context.annotation.UserConfigurations; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -45,6 +49,7 @@ import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.RestController; @@ -59,6 +64,7 @@ import java.util.Set; import javax.sql.DataSource; +import javax.sql.XADataSource; /** * Tests for ElideAutoConfiguration. @@ -286,12 +292,12 @@ void dataStoreTransaction() { public static class MultipleDataSourceConfiguration { @Bean public DataSource dataSourceV2() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1").build(); + return DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1").build(); } @Bean public DataSource dataSourceV3() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1").build(); + return DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1").build(); } @Bean @@ -313,7 +319,8 @@ public static class MultipleEntityManagerFactoryConfiguration { public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, DefaultListableBeanFactory beanFactory, DataSource dataSourceV2) { Map vendorProperties = new HashMap<>(); - vendorProperties.put("hibernate.hbm2ddl.auto", "create-drop"); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) .packages("example.models.jpa.v2").properties(vendorProperties).build(); return emf; @@ -323,7 +330,8 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManag public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, DefaultListableBeanFactory beanFactory, DataSource dataSourceV3) { Map vendorProperties = new HashMap<>(); - vendorProperties.put("hibernate.hbm2ddl.auto", "create-drop"); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) .packages("example.models.jpa.v3").properties(vendorProperties).build(); return emf; @@ -353,30 +361,130 @@ void multiplexDataStoreTransaction() { try (DataStoreTransaction transaction = dataStore.beginTransaction()) { ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("Group V2"); + artifactGroupV2.setName("JPA Group V2a"); transaction.save(artifactGroupV2, null); transaction.commit(null); } try (DataStoreTransaction transaction = dataStore.beginTransaction()) { ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("Group V3"); + artifactGroupV3.setName("JPA Group V3a"); transaction.save(artifactGroupV3, null); transaction.commit(null); } try (DataStoreTransaction transaction = dataStore.beginTransaction()) { ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("Group V2"); + artifactGroupV2.setName("JPA Group V2b"); ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("Group V3"); + artifactGroupV3.setName("JPA Group V3b"); transaction.save(artifactGroupV2, null); transaction.save(artifactGroupV3, null); - assertThatThrownBy(() -> transaction.commit(null)).isInstanceOf(TransactionException.class) - .message().isEqualTo("Transaction synchronization is not active"); + transaction.commit(null); + } + }); + } + + @Configuration(proxyBeanMethods = false) + public static class MultipleDataSourceJtaConfiguration { + @Bean + public DataSource dataSourceV2() { + DataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1") + .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).build(); + AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); + atomikosDataSource.setXaDataSource((XADataSource) xaDataSource); + return atomikosDataSource; + } + + @Bean + public DataSource dataSourceV3() { + DataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1") + .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).build(); + AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); + atomikosDataSource.setXaDataSource((XADataSource) xaDataSource); + return atomikosDataSource; + } + + @Bean + public EntityManagerFactoryBuilder entityManagerFactoryBuilder( + ObjectProvider persistenceUnitManager, + ObjectProvider customizers) { + EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), + new HashMap<>(), persistenceUnitManager.getIfAvailable()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + } + + @Configuration(proxyBeanMethods = false) + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManager") + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManager") + public static class MultipleEntityManagerFactoryJtaConfiguration { + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV2, JtaTransactionManager transactionManager) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) + .packages("example.models.jpa.v2").properties(vendorProperties).jta(true).build(); + return emf; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV3, JtaTransactionManager transactionManager) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) + .packages("example.models.jpa.v3").properties(vendorProperties).jta(true).build(); + return emf; + } + } + + @Test + void multiplexDataStoreJtaTransaction() { + contextRunner + .withPropertyValues("spring.cloud.refresh.enabled=false", + "atomikos.properties.max-timeout=0") + .withConfiguration(AutoConfigurations.of(AtomikosAutoConfiguration.class)) + .withUserConfiguration(MultipleDataSourceJtaConfiguration.class, MultipleEntityManagerFactoryJtaConfiguration.class).run(context -> { + DataStore dataStore = context.getBean(DataStore.class); + assertThat(dataStore).isInstanceOf(MultiplexManager.class); + + // The data store will only be initialized properly by elide to populate the dictionary + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + dataStore = refreshableElide.getElide().getDataStore(); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA Group V2"); + transaction.save(artifactGroupV2, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA Group V3"); + transaction.save(artifactGroupV3, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA Group V2"); + + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA Group V3"); + + transaction.save(artifactGroupV2, null); + transaction.save(artifactGroupV3, null); + + transaction.commit(null); } }); } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/transactions.properties b/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/transactions.properties new file mode 100644 index 0000000000..80520bd830 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/transactions.properties @@ -0,0 +1 @@ +com.atomikos.icatch.registered=1 From b56e231f8d2064ac7ec6a6d76e0a31163fdfc798 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Wed, 3 May 2023 21:12:27 +0800 Subject: [PATCH 3/8] Fix reverse the order that the transactions are committed --- .../datastore/inmemory/HashMapDataStore.java | 49 +++++-- .../inmemory/HashMapStoreTransaction.java | 67 ++++++++- .../yahoo/elide/core/utils/ObjectCloner.java | 21 +++ .../yahoo/elide/core/utils/ObjectCloners.java | 96 +++++++++++++ .../elide/core/utils/ClassScannerTest.java | 2 +- .../core/utils/ObjectClonerTestObject.java | 44 ++++++ .../elide/core/utils/ObjectClonersTest.java | 62 +++++++++ .../metadata/MetaDataStoreTransaction.java | 57 +++++--- .../inmemory/HashMapDataStoreTest.java | 131 +++++++++++++++--- .../multiplex/MultiplexManager.java | 39 ++++++ .../multiplex/MultiplexTransaction.java | 61 +++++--- .../multiplex/MultiplexWriteTransaction.java | 45 +++--- .../multiplex/MultiplexManagerTest.java | 93 ++++++++++++- 13 files changed, 662 insertions(+), 105 deletions(-) create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloner.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloners.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonerTestObject.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonersTest.java diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java index 1140001fc0..f23f2df4eb 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java @@ -14,7 +14,8 @@ import com.yahoo.elide.core.type.ClassType; import com.yahoo.elide.core.type.Type; import com.yahoo.elide.core.utils.ClassScanner; -import com.google.common.collect.Sets; +import com.yahoo.elide.core.utils.ObjectCloner; +import com.yahoo.elide.core.utils.ObjectCloners; import lombok.Getter; @@ -27,6 +28,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Simple in-memory only database. @@ -35,29 +38,42 @@ public class HashMapDataStore implements DataStore, DataStoreTestHarness { protected final Map, Map> dataStore = Collections.synchronizedMap(new HashMap<>()); @Getter protected EntityDictionary dictionary; @Getter private final ConcurrentHashMap, AtomicLong> typeIds = new ConcurrentHashMap<>(); + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final ObjectCloner objectCloner; public HashMapDataStore(ClassScanner scanner, Package beanPackage) { - this(scanner, Sets.newHashSet(beanPackage)); + this(scanner, beanPackage, ObjectCloners::clone); + } + + public HashMapDataStore(ClassScanner scanner, Package beanPackage, ObjectCloner objectCloner) { + this(scanner, Collections.singleton(beanPackage), objectCloner); } public HashMapDataStore(ClassScanner scanner, Set beanPackages) { + this(scanner, beanPackages, ObjectCloners::clone); + } + + public HashMapDataStore(ClassScanner scanner, Set beanPackages, ObjectCloner objectCloner) { + this.objectCloner = objectCloner; for (Package beanPackage : beanPackages) { - scanner.getAllClasses(beanPackage.getName()).stream() - .map(ClassType::new) - .filter(modelType -> EntityDictionary.getFirstAnnotation(modelType, - Arrays.asList(Include.class, Exclude.class)) instanceof Include) - .forEach(modelType -> dataStore.put(modelType, - Collections.synchronizedMap(new LinkedHashMap<>()))); + process(scanner.getAllClasses(beanPackage.getName())); } } public HashMapDataStore(Collection> beanClasses) { - beanClasses.stream() - .map(ClassType::new) + this(beanClasses, ObjectCloners::clone); + } + + public HashMapDataStore(Collection> beanClasses, ObjectCloner objectCloner) { + this.objectCloner = objectCloner; + process(beanClasses); + } + + protected void process(Collection> beanClasses) { + beanClasses.stream().map(ClassType::of) .filter(modelType -> EntityDictionary.getFirstAnnotation(modelType, Arrays.asList(Include.class, Exclude.class)) instanceof Include) - .forEach(modelType -> dataStore.put(modelType, - Collections.synchronizedMap(new LinkedHashMap<>()))); + .forEach(modelType -> dataStore.put(modelType, Collections.synchronizedMap(new LinkedHashMap<>()))); } @Override @@ -71,7 +87,14 @@ public void populateEntityDictionary(EntityDictionary dictionary) { @Override public DataStoreTransaction beginTransaction() { - return new HashMapStoreTransaction(dataStore, dictionary, typeIds); + return new HashMapStoreTransaction(this.readWriteLock, this.dataStore, this.dictionary, + this.typeIds, this.objectCloner, false); + } + + @Override + public DataStoreTransaction beginReadTransaction() { + return new HashMapStoreTransaction(this.readWriteLock, this.dataStore, this.dictionary, + this.typeIds, this.objectCloner, true); } @Override diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java index b026c15216..56048c1407 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java @@ -14,6 +14,7 @@ import com.yahoo.elide.core.request.EntityProjection; import com.yahoo.elide.core.request.Relationship; import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.core.utils.ObjectCloner; import com.yahoo.elide.core.utils.coerce.converters.Serde; import jakarta.persistence.GeneratedValue; @@ -21,9 +22,12 @@ import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; /** * HashMapDataStore transaction handler. @@ -33,13 +37,27 @@ public class HashMapStoreTransaction implements DataStoreTransaction { private final List operations; private final EntityDictionary dictionary; private final Map, AtomicLong> typeIds; + private final Lock lock; + private final boolean readOnly; + private final ObjectCloner objectCloner; + private boolean committed = false; - public HashMapStoreTransaction(Map, Map> dataStore, - EntityDictionary dictionary, Map, AtomicLong> typeIds) { + public HashMapStoreTransaction(ReadWriteLock readWriteLock, Map, Map> dataStore, + EntityDictionary dictionary, Map, AtomicLong> typeIds, ObjectCloner objectCloner, + boolean readOnly) { + this.readOnly = readOnly; this.dataStore = dataStore; this.dictionary = dictionary; this.operations = new ArrayList<>(); this.typeIds = typeIds; + this.objectCloner = objectCloner; + + if (readWriteLock != null) { + this.lock = readOnly ? readWriteLock.readLock() : readWriteLock.writeLock(); + this.lock.lock(); + } else { + this.lock = null; + } } @Override @@ -91,6 +109,7 @@ public void commit(RequestScope scope) { } }); operations.clear(); + committed = true; } } @@ -140,6 +159,7 @@ public DataStoreIterable loadObjects(EntityProjection projection, RequestScope scope) { synchronized (dataStore) { Map data = dataStore.get(projection.getType()); + cacheForRollback(projection.getType(), data); return new DataStoreIterableBuilder<>(data.values()).allInMemory().build(); } } @@ -151,6 +171,7 @@ public Object loadObject(EntityProjection projection, Serializable id, RequestSc synchronized (dataStore) { Map data = dataStore.get(projection.getType()); + cacheForRollback(projection.getType(), data); if (data == null) { return null; } @@ -161,9 +182,49 @@ public Object loadObject(EntityProjection projection, Serializable id, RequestSc } } + /** + * Contains a copy of the objects loaded from the hash map store as they may be + * updated like a persistent object. Since what is returned is a reference to + * the object in the underlying store when updated it immediately reflects in + * the store. As such a copy of the original objects need to made in order to + * rollback. + */ + private Map, Map> rollbackCache = new HashMap<>(); + + protected void cacheForRollback(Type type, Map data) { + if (!readOnly) { + this.rollbackCache.computeIfAbsent(type, key -> { + if (data != null) { + Map copy = new HashMap<>(); + data.entrySet().stream().forEach(entry -> { + Object value = this.objectCloner.clone(entry.getValue(), type); + copy.put(entry.getKey(), value); + }); + return copy; + } + return null; + }); + } + } + @Override public void close() throws IOException { - operations.clear(); + try { + if (!committed && !readOnly) { + rollback(); + } + operations.clear(); + } finally { + if (this.lock != null) { + this.lock.unlock(); + } + } + } + + public void rollback() { + // Rollback data + dataStore.putAll(this.rollbackCache); + this.rollbackCache.clear(); } private boolean containsObject(Object obj) { diff --git a/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloner.java b/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloner.java new file mode 100644 index 0000000000..04a74722ff --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloner.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.utils; + +import com.yahoo.elide.core.type.ClassType; +import com.yahoo.elide.core.type.Type; + +/** + * Clones an object. + */ +@FunctionalInterface +public interface ObjectCloner { + T clone(T source, Type cls); + + default T clone(T source) { + return clone(source, ClassType.of(source.getClass())); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloners.java b/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloners.java new file mode 100644 index 0000000000..e91e31b210 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/utils/ObjectCloners.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.utils; + +import com.yahoo.elide.core.type.ClassType; +import com.yahoo.elide.core.type.Field; +import com.yahoo.elide.core.type.Method; +import com.yahoo.elide.core.type.Type; + +/** + * Utility methods to clone an object. + */ +public class ObjectCloners { + private ObjectCloners() { + } + + /** + * Attempt to shallow clone an object. + * + * @param source the object to clone + * @return cloned object if successful or original object + */ + public static T clone(T source) { + return clone(source, ClassType.of(source.getClass())); + } + + /** + * Attempt to shallow clone an object. + * + * @param source the object to clone + * @param cls the type of object + * @return cloned object if successful or original object + */ + public static T clone(T source, Type cls) { + try { + @SuppressWarnings("unchecked") + T target = (T) cls.newInstance(); + copyProperties(source, target, cls); + return target; + } catch (InstantiationException | IllegalAccessException e) { + // ignore + } + // Failed to clone return original object + return source; + } + + /** + * Attempt to copy properties from the source to the target. + * @param source the bean to copy from + * @param target the bean to copy to + * @param cls the class + */ + public static void copyProperties(Object source, Object target, Type cls) { + for (Field field : cls.getFields()) { + try { + field.set(target, field.get(source)); + } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { + // ignore + } + } + for (Field field : cls.getDeclaredFields()) { + try { + field.setAccessible(true); + field.set(target, field.get(source)); + } catch (IllegalArgumentException | IllegalAccessException | SecurityException e) { + // ignore + } + } + for (Method method : cls.getMethods()) { + if (method.getName().startsWith("set")) { + try { + Method getMethod = cls.getMethod("get" + method.getName().substring(3)); + method.invoke(target, getMethod.invoke(source)); + } catch (NoSuchMethodException e) { + try { + Method isMethod = cls.getMethod("is" + method.getName().substring(3)); + method.invoke(target, isMethod.invoke(source)); + } catch (IllegalStateException + | IllegalArgumentException + | ReflectiveOperationException + | SecurityException e2) { + // ignore + } + } catch (IllegalStateException + | IllegalArgumentException + | ReflectiveOperationException + | SecurityException e) { + // ignore + } + } + } + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/utils/ClassScannerTest.java b/elide-core/src/test/java/com/yahoo/elide/core/utils/ClassScannerTest.java index 6a93c07795..0fdd6c5cbf 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/utils/ClassScannerTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/utils/ClassScannerTest.java @@ -26,7 +26,7 @@ public ClassScannerTest() { @Test public void testGetAllClasses() { Set> classes = scanner.getAllClasses("com.yahoo.elide.core.utils"); - assertEquals(34, classes.size()); + assertEquals(39, classes.size()); assertTrue(classes.contains(ClassScannerTest.class)); } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonerTestObject.java b/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonerTestObject.java new file mode 100644 index 0000000000..618f144095 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonerTestObject.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.utils; + +import java.util.List; + +/** + * Test object for ObjectCloner. + */ +public class ObjectClonerTestObject { + private Long id; + private boolean admin; + private String name; + private List list; + + public void setId(Long id) { + this.id = id; + } + public boolean isAdmin() { + return admin; + } + public void setAdmin(boolean admin) { + this.admin = admin; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public List getList() { + return list; + } + public void setList(List list) { + this.list = list; + } + + public Long id() { + return this.id; + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonersTest.java b/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonersTest.java new file mode 100644 index 0000000000..0370080904 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/utils/ObjectClonersTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +/** + * Test for ObjectCloner. + */ +public class ObjectClonersTest { + public static class NoDefaultConstructorObject { + private Long id; + + public NoDefaultConstructorObject(NoDefaultConstructorObject copy) { + this.id = copy.id; + } + + public NoDefaultConstructorObject(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } + + @Test + void testClone() { + ObjectClonerTestObject object = new ObjectClonerTestObject(); + object.setId(1L); + object.setAdmin(true); + object.setName("name"); + object.setList(Collections.singletonList("value")); + + ObjectClonerTestObject clone = ObjectCloners.clone(object); + assertTrue(clone.isAdmin()); + assertEquals("name", clone.getName()); + assertEquals("value", clone.getList().get(0)); + assertEquals(1L, clone.id()); + assertTrue(clone != object); + } + + @Test + void testShouldReturnOriginalObject() { + NoDefaultConstructorObject object = new NoDefaultConstructorObject(1L); + NoDefaultConstructorObject clone = ObjectCloners.clone(object); + assertEquals(1L, clone.getId()); + assertTrue(clone == object); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStoreTransaction.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStoreTransaction.java index 85a6c1c706..b8d4e8d5a2 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStoreTransaction.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStoreTransaction.java @@ -6,6 +6,7 @@ package com.yahoo.elide.datastores.aggregation.metadata; import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.datastore.DataStore; import com.yahoo.elide.core.datastore.DataStoreIterable; import com.yahoo.elide.core.datastore.DataStoreTransaction; import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; @@ -16,6 +17,7 @@ import java.io.IOException; import java.io.Serializable; +import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -30,6 +32,8 @@ public class MetaDataStoreTransaction implements DataStoreTransaction { private final Map hashMapDataStores; + private final Map transactions = new HashMap<>(); + public MetaDataStoreTransaction(Map hashMapDataStores) { this.hashMapDataStores = hashMapDataStores; } @@ -66,10 +70,8 @@ public DataStoreIterable getToManyRelation( Relationship relationship, RequestScope scope ) { - return hashMapDataStores - .computeIfAbsent(scope.getApiVersion(), REQUEST_ERROR) - .beginTransaction() - .getToManyRelation(relationTx, entity, relationship, scope); + DataStoreTransaction dataStoreTransaction = getTransaction(scope); + return dataStoreTransaction.getToManyRelation(relationTx, entity, relationship, scope); } @Override @@ -79,31 +81,52 @@ public Object getToOneRelation( Relationship relationship, RequestScope scope ) { - return hashMapDataStores - .computeIfAbsent(scope.getApiVersion(), REQUEST_ERROR) - .beginTransaction() - .getToOneRelation(relationTx, entity, relationship, scope); + DataStoreTransaction dataStoreTransaction = getTransaction(scope); + return dataStoreTransaction.getToOneRelation(relationTx, entity, relationship, scope); } @Override public DataStoreIterable loadObjects(EntityProjection projection, RequestScope scope) { - return hashMapDataStores - .computeIfAbsent(scope.getApiVersion(), REQUEST_ERROR) - .beginTransaction() - .loadObjects(projection, scope); + DataStoreTransaction dataStoreTransaction = getTransaction(scope); + return dataStoreTransaction.loadObjects(projection, scope); } @Override public Object loadObject(EntityProjection projection, Serializable id, RequestScope scope) { - return hashMapDataStores - .computeIfAbsent(scope.getApiVersion(), REQUEST_ERROR) - .beginTransaction() - .loadObject(projection, id, scope); + DataStoreTransaction dataStoreTransaction = getTransaction(scope); + return dataStoreTransaction.loadObject(projection, id, scope); + } + + protected DataStoreTransaction getTransaction(RequestScope scope) { + DataStore dataStore = hashMapDataStores.computeIfAbsent(scope.getApiVersion(), REQUEST_ERROR); + return transactions.computeIfAbsent(scope.getApiVersion(), + key -> dataStore.beginReadTransaction()); } @Override public void close() throws IOException { - // Do nothing + IOException exception = null; + for (DataStoreTransaction transaction : transactions.values()) { + try { + transaction.close(); + } catch (IOException e) { + if (exception == null) { + exception = e; + } else { + exception.addSuppressed(e); + } + } catch (RuntimeException e) { + if (exception == null) { + exception = new IOException(e); + } else { + exception.addSuppressed(e); + } + } + } + transactions.clear(); + if (exception != null) { + throw exception; + } } @Override diff --git a/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java b/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java index 80dfb4de49..5b0553b402 100644 --- a/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java +++ b/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java @@ -10,8 +10,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.datastore.DataStoreTransaction; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.core.request.EntityProjection; import com.yahoo.elide.core.type.ClassType; @@ -30,25 +34,29 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; /** * HashMapDataStore tests. */ public class HashMapDataStoreTest { - private InMemoryDataStore inMemoryDataStore; + private HashMapDataStore hashMapDataStore; private EntityDictionary entityDictionary; @BeforeEach public void setup() { entityDictionary = EntityDictionary.builder().build(); - inMemoryDataStore = new InMemoryDataStore(DefaultClassScanner.getInstance(), FirstBean.class.getPackage()); - inMemoryDataStore.populateEntityDictionary(entityDictionary); + hashMapDataStore = new HashMapDataStore(DefaultClassScanner.getInstance(), FirstBean.class.getPackage()); + hashMapDataStore.populateEntityDictionary(entityDictionary); } private T createNewInheritanceObject(Class type) throws IOException, InstantiationException, IllegalAccessException { T obj = type.newInstance(); - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { t.createObject(obj, null); t.commit(null); } @@ -57,13 +65,13 @@ private T createNewInheritanceObject(Class type) @Test public void dataStoreTestInheritance() throws IOException, InstantiationException, IllegalAccessException { - Map entry = inMemoryDataStore.get(ClassType.of(FirstBean.class)); + Map entry = hashMapDataStore.get(ClassType.of(FirstBean.class)); assertEquals(0, entry.size()); FirstChildBean child = createNewInheritanceObject(FirstChildBean.class); // Adding Child object, adds a parent entry. - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { Iterable beans = t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null); @@ -88,20 +96,20 @@ public void dataStoreTestInheritance() throws IOException, InstantiationExceptio @Test public void dataStoreTestInheritanceDelete() throws IOException, InstantiationException, IllegalAccessException { - Map entry = inMemoryDataStore.get(ClassType.of(FirstBean.class)); + Map entry = hashMapDataStore.get(ClassType.of(FirstBean.class)); assertEquals(0, entry.size()); FirstChildBean child = createNewInheritanceObject(FirstChildBean.class); createNewInheritanceObject(FirstBean.class); // Delete Child - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { t.delete(child, null); t.commit(null); } // Only 1 parent entry should remain. - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { Iterable beans = t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null); @@ -114,21 +122,21 @@ public void dataStoreTestInheritanceDelete() throws IOException, InstantiationEx @Test public void dataStoreTestInheritanceUpdate() throws IOException, InstantiationException, IllegalAccessException { - Map entry = inMemoryDataStore.get(ClassType.of(FirstBean.class)); + Map entry = hashMapDataStore.get(ClassType.of(FirstBean.class)); assertEquals(0, entry.size()); FirstChildBean child = createNewInheritanceObject(FirstChildBean.class); createNewInheritanceObject(FirstBean.class); // update Child - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { child.setNickname("hello"); t.save(child, null); t.commit(null); } // Only 1 parent entry should remain. - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { Iterable beans = t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null); @@ -142,7 +150,7 @@ public void dataStoreTestInheritanceUpdate() throws IOException, InstantiationEx @Test public void checkLoading() { - final EntityDictionary entityDictionary = inMemoryDataStore.getDictionary(); + final EntityDictionary entityDictionary = hashMapDataStore.getDictionary(); assertNotNull(entityDictionary.getJsonAliasFor(ClassType.of(FirstBean.class))); assertNotNull(entityDictionary.getJsonAliasFor(ClassType.of(SecondBean.class))); assertThrows(IllegalArgumentException.class, () -> entityDictionary.getJsonAliasFor(ClassType.of(NonEntity.class))); @@ -154,7 +162,7 @@ public void testValidCommit() throws Exception { FirstBean object = new FirstBean(); object.id = "0"; object.name = "Test"; - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { assertFalse(t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null).iterator().hasNext()); @@ -164,7 +172,7 @@ public void testValidCommit() throws Exception { .build(), null).iterator().hasNext()); t.commit(null); } - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { Iterable beans = t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null); @@ -182,7 +190,7 @@ public void testCanGenerateIdsAfterDataCommitted() throws Exception { object.id = "1"; object.name = "number one"; - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { t.createObject(object, null); t.save(object, null); t.commit(null); @@ -193,7 +201,7 @@ public void testCanGenerateIdsAfterDataCommitted() throws Exception { object2.id = null; object2.name = "number two"; - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { t.createObject(object2, null); t.save(object2, null); t.commit(null); @@ -201,7 +209,7 @@ public void testCanGenerateIdsAfterDataCommitted() throws Exception { // and a meaningful ID is assigned Set names = new HashSet<>(); - try (DataStoreTransaction t = inMemoryDataStore.beginTransaction()) { + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { for (Object objBean : t.loadObjects(EntityProjection.builder() .type(FirstBean.class) .build(), null)) { @@ -213,4 +221,91 @@ public void testCanGenerateIdsAfterDataCommitted() throws Exception { assertEquals(ImmutableSet.of("number one", "number two"), names); } + + @Test + public void testRollback() throws Exception { + FirstBean object = new FirstBean(); + object.id = "1"; + object.name = "number one"; + + RequestScope scope = mock(RequestScope.class); + when(scope.getDictionary()).thenReturn(entityDictionary); + + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { + t.createObject(object, null); + t.save(object, null); + t.commit(null); + } + + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { + // The FirstBean loaded is the same reference from the HashMapDataStore so + // modifying it actually updates the underlying store + FirstBean loaded = t.loadObject(EntityProjection.builder().type(FirstBean.class).build(), "1", scope); + loaded.name = "updated"; + + // There is no commit so this will rollback + } + + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { + FirstBean loaded = t.loadObject(EntityProjection.builder().type(FirstBean.class).build(), "1", scope); + assertEquals("number one", loaded.name); + } + } + + /** + * Tests if another thread reading the hash map data store will read dirty + * uncommitted data. Typically a read write lock is required to ensure readers + * don't read dirty data. + * + * @throws Exception the exception + */ + @Test + public void testShouldNotReadDirtyData() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(1); + try { + FirstBean object = new FirstBean(); + object.id = "1"; + object.name = "number one"; + + RequestScope scope = mock(RequestScope.class); + when(scope.getDictionary()).thenReturn(entityDictionary); + + Future future; + + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { + t.createObject(object, null); + t.save(object, null); + t.commit(null); + } + + try (DataStoreTransaction t = hashMapDataStore.beginTransaction()) { + // The FirstBean loaded is the same reference from the HashMapDataStore so + // modifying it actually updates the underlying store making it dirty + FirstBean loaded = t.loadObject(EntityProjection.builder().type(FirstBean.class).build(), "1", scope); + loaded.name = "updated"; + + future = executor.submit(() -> { + try (DataStoreTransaction r = hashMapDataStore.beginReadTransaction()) { + FirstBean other = r.loadObject(EntityProjection.builder().type(FirstBean.class).build(), "1", scope); + return other; + } + }); + + Thread.sleep(1000); + // There is no commit so this should rollback + } + + try (DataStoreTransaction r = hashMapDataStore.beginReadTransaction()) { + // Verify that the rolled back first bean is still number one + FirstBean rolledBack = r.loadObject(EntityProjection.builder().type(FirstBean.class).build(), "1", scope); + assertEquals("number one", rolledBack.name); + } + + // Verify that the first bean read in the different thread did not read the dirty bean + FirstBean firstBean = future.get(30, TimeUnit.SECONDS); + assertEquals("number one", firstBean.name); + } finally { + executor.shutdownNow(); + } + } } diff --git a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java index 94b7fe9ce2..7cc49797c1 100644 --- a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java +++ b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexManager.java @@ -12,6 +12,8 @@ import com.yahoo.elide.core.dictionary.EntityDictionary; import com.yahoo.elide.core.security.PermissionExecutor; import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.core.utils.ObjectCloner; +import com.yahoo.elide.core.utils.ObjectCloners; import lombok.AccessLevel; import lombok.Setter; @@ -21,6 +23,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Predicate; /** * Allows multiple database handlers to each process their own beans while keeping the main @@ -41,6 +44,8 @@ public final class MultiplexManager implements DataStore { protected final List dataStores; protected final ConcurrentHashMap, DataStore> dataStoreMap = new ConcurrentHashMap<>(); + protected final Predicate applyCompensatingTransactions; + protected final ObjectCloner objectCloner; @Setter(AccessLevel.PROTECTED) private EntityDictionary dictionary; @@ -50,7 +55,41 @@ public final class MultiplexManager implements DataStore { * @param dataStores list of sub-managers */ public MultiplexManager(DataStore... dataStores) { + this(ObjectCloners::clone, dataStore -> true, dataStores); + } + + /** + * Create a single DataStore to handle provided managers within a single + * transaction. + * + * @param objectCloner to use for cloning objects to apply to compensating + * transaction + * @param dataStores list of sub-managers + */ + public MultiplexManager(ObjectCloner objectCloner, DataStore... dataStores) { + this(objectCloner, dataStore -> true, dataStores); + } + + /** + * Create a single DataStore to handle provided managers within a single + * transaction. + * + * @param objectCloner to use for cloning objects to apply to + * compensating transaction + * @param applyCompensatingTransactions apply compensating transactions on + * rollback to previously committed + * datastores + * @param dataStores list of sub-managers + */ + public MultiplexManager(ObjectCloner objectCloner, Predicate applyCompensatingTransactions, + DataStore... dataStores) { + this.objectCloner = objectCloner; this.dataStores = Arrays.asList(dataStores); + this.applyCompensatingTransactions = applyCompensatingTransactions; + } + + protected boolean isApplyCompensatingTransactions(DataStore dataStore) { + return this.applyCompensatingTransactions.test(dataStore); } @Override diff --git a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java index 33c724ea50..0a20157d1e 100644 --- a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java +++ b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java @@ -23,9 +23,12 @@ import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.ListIterator; import java.util.Set; +import java.util.function.Consumer; /** * Multiplex transaction handler. Process each sub-database transactions within a single transaction. @@ -66,42 +69,60 @@ public DataStoreIterable loadObjects( } @Override - public void flush(RequestScope requestScope) { - transactions.values().stream() - .filter(dataStoreTransaction -> dataStoreTransaction != null) - .forEach(dataStoreTransaction -> dataStoreTransaction.flush(requestScope)); + public void flush(RequestScope scope) { + processTransactions(dataStoreTransaction -> dataStoreTransaction.flush(scope)); } @Override public void preCommit(RequestScope scope) { - transactions.values().stream() - .filter(dataStoreTransaction -> dataStoreTransaction != null) - .forEach(tx -> tx.preCommit(scope)); + processTransactions(dataStoreTransaction -> dataStoreTransaction.preCommit(scope)); } @Override public void commit(RequestScope scope) { // flush all before commit flush(scope); - transactions.values().stream() - .filter(dataStoreTransaction -> dataStoreTransaction != null) - .forEach(dataStoreTransaction -> dataStoreTransaction.commit(scope)); + processTransactions(dataStoreTransaction -> dataStoreTransaction.commit(scope)); + } + + /** + * Processes the transactions in reverse order and is non null. + * + * @param processor process the transaction + */ + protected void processTransactions(Consumer processor) { + // Transactions must be processed in reverse order + ListIterator iterator = new ArrayList<>(transactions.values()) + .listIterator(transactions.size()); + while (iterator.hasPrevious()) { + DataStoreTransaction dataStoreTransaction = iterator.previous(); + if (dataStoreTransaction != null) { + processor.accept(dataStoreTransaction); + } + } } @Override public void close() throws IOException { IOException cause = null; - for (DataStoreTransaction transaction : transactions.values()) { - try { - transaction.close(); - } catch (IOException | Error | RuntimeException e) { - if (cause != null) { - cause.addSuppressed(e); - } else if (e instanceof IOException) { - cause = (IOException) e; - } else { - cause = new IOException(e); + + // Transactions must be processed in reverse order + ListIterator iterator = new ArrayList<>(transactions.values()) + .listIterator(transactions.size()); + while (iterator.hasPrevious()) { + DataStoreTransaction dataStoreTransaction = iterator.previous(); + if (dataStoreTransaction != null) { + try { + dataStoreTransaction.close(); + } catch (IOException | Error | RuntimeException e) { + if (cause != null) { + cause.addSuppressed(e); + } else if (e instanceof IOException ioException) { + cause = ioException; + } else { + cause = new IOException(e); + } } } } diff --git a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexWriteTransaction.java b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexWriteTransaction.java index 35339309ed..6e40a6daaf 100644 --- a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexWriteTransaction.java +++ b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexWriteTransaction.java @@ -15,8 +15,6 @@ import com.yahoo.elide.core.exceptions.TransactionException; import com.yahoo.elide.core.request.EntityProjection; import com.yahoo.elide.core.request.Relationship; -import com.yahoo.elide.core.type.Field; -import com.yahoo.elide.core.type.Method; import com.yahoo.elide.core.type.Type; import jakarta.ws.rs.WebApplicationException; @@ -28,6 +26,7 @@ import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; +import java.util.ListIterator; import java.util.Map.Entry; /** @@ -67,11 +66,18 @@ public void commit(RequestScope scope) { // flush all before commits flush(scope); - ArrayList commitList = new ArrayList<>(); - for (Entry entry : transactions.entrySet()) { + List commitList = new ArrayList<>(); + + // Transactions must be committed in reverse order + ListIterator> iterator = new ArrayList<>(transactions.entrySet()) + .listIterator(transactions.size()); + while (iterator.hasPrevious()) { + Entry entry = iterator.previous(); try { entry.getValue().commit(scope); - commitList.add(entry.getKey()); + if (this.multiplexManager.isApplyCompensatingTransactions(entry.getKey())) { + commitList.add(entry.getKey()); + } } catch (HttpStatusException | WebApplicationException e) { reverseTransactions(commitList, e, scope); throw e; @@ -88,7 +94,7 @@ public void commit(RequestScope scope) { * @param restoreList List of database managers to reverse the last commit * @param cause cause to add any suppressed exceptions */ - private void reverseTransactions(ArrayList restoreList, Throwable cause, RequestScope requestScope) { + private void reverseTransactions(List restoreList, Throwable cause, RequestScope requestScope) { for (DataStore dataStore : restoreList) { try (DataStoreTransaction transaction = dataStore.beginTransaction()) { List list = dirtyObjects.get(dataStore); @@ -97,7 +103,10 @@ private void reverseTransactions(ArrayList restoreList, Throwable cau if (cloned == NEWLY_CREATED_OBJECT) { transaction.delete(dirtyObject, requestScope); } else { - transaction.save(cloned, requestScope); + // If cloned is null this is an update to an object that wasn't created yet + if (cloned != null) { + transaction.save(cloned, requestScope); + } } } transaction.commit(requestScope); @@ -149,27 +158,7 @@ private Object cloneObject(Object object) { } Type cls = multiplexManager.getDictionary().lookupBoundClass(EntityDictionary.getType(object)); - try { - Object clone = cls.newInstance(); - for (Field field : cls.getFields()) { - field.set(clone, field.get(object)); - } - for (Method method : cls.getMethods()) { - if (method.getName().startsWith("set")) { - try { - Method getMethod = cls.getMethod("get" + method.getName().substring(3)); - method.invoke(clone, getMethod.invoke(object)); - } catch (IllegalStateException | IllegalArgumentException - | ReflectiveOperationException | SecurityException e) { - return null; - } - } - } - return clone; - } catch (InstantiationException | IllegalAccessException e) { - // ignore - } - return null; + return this.multiplexManager.objectCloner.clone(object, cls); } @Override diff --git a/elide-datastore/elide-datastore-multiplex/src/test/java/com/yahoo/elide/datastores/multiplex/MultiplexManagerTest.java b/elide-datastore/elide-datastore-multiplex/src/test/java/com/yahoo/elide/datastores/multiplex/MultiplexManagerTest.java index ac8d282955..ab081aaa59 100644 --- a/elide-datastore/elide-datastore-multiplex/src/test/java/com/yahoo/elide/datastores/multiplex/MultiplexManagerTest.java +++ b/elide-datastore/elide-datastore-multiplex/src/test/java/com/yahoo/elide/datastores/multiplex/MultiplexManagerTest.java @@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; /** @@ -90,8 +91,23 @@ public void testValidCommit() throws IOException { } } + /** + * Tests the case where there is no commit to the hash map data store occurs and + * a rollback occurs subsequently. + * + * It is expected that the update to the FirstBean from the hash map data store + * to set the name to update does not update the underlying store if there is no + * commit to the hash map data store. + * + * @throws IOException the exception + * @throws IllegalArgumentException the exception + * @throws InvocationTargetException the exception + * @throws NoSuchMethodException the exception + * @throws SecurityException the exception + */ @Test - public void partialCommitFailure() throws IOException { + public void partialCommitFailureNoCommit() throws IOException, IllegalArgumentException, InvocationTargetException, + NoSuchMethodException, SecurityException { final EntityDictionary entityDictionary = EntityDictionary.builder().build(); final HashMapDataStore ds1 = new HashMapDataStore(DefaultClassScanner.getInstance(), FirstBean.class.getPackage()); @@ -107,7 +123,7 @@ public void partialCommitFailure() throws IOException { .type(FirstBean.class) .build(), null).iterator().hasNext()); - FirstBean firstBean = FirstBean.class.newInstance(); + FirstBean firstBean = FirstBean.class.getDeclaredConstructor().newInstance(); firstBean.setName("name"); t.createObject(firstBean, null); //t.save(firstBean); @@ -124,7 +140,7 @@ public void partialCommitFailure() throws IOException { .build(), null).iterator().next(); firstBean.setName("update"); t.save(firstBean, null); - OtherBean otherBean = OtherBean.class.newInstance(); + OtherBean otherBean = OtherBean.class.getDeclaredConstructor().newInstance(); t.createObject(otherBean, null); //t.save(firstBean); @@ -139,8 +155,75 @@ public void partialCommitFailure() throws IOException { .build(), null); assertNotNull(beans); ArrayList list = Lists.newArrayList(beans.iterator()); - assertEquals(list.size(), 1); - assertEquals(((FirstBean) list.get(0)).getName(), "name"); + assertEquals(1, list.size()); + assertEquals("name", ((FirstBean) list.get(0)).getName()); + } + } + + /** + * Tests the case where the commit to the hash map data store occurs and a + * rollback occurs subsequently which the multiplex manager will attempt to + * reverse. + * + * @throws IOException the exception + * @throws IllegalArgumentException the exception + * @throws InvocationTargetException the exception + * @throws NoSuchMethodException the exception + * @throws SecurityException the exception + */ + @Test + public void partialCommitFailureReverseTransactions() throws IOException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException, SecurityException { + final EntityDictionary entityDictionary = EntityDictionary.builder().build(); + final HashMapDataStore ds1 = new HashMapDataStore(DefaultClassScanner.getInstance(), + FirstBean.class.getPackage()); + final DataStore ds2 = new TestDataStore(OtherBean.class.getPackage()); + final MultiplexManager multiplexManager = new MultiplexManager(ds1, ds2); + multiplexManager.populateEntityDictionary(entityDictionary); + + assertEquals(ds1, multiplexManager.getSubManager(ClassType.of(FirstBean.class))); + assertEquals(ds2, multiplexManager.getSubManager(ClassType.of(OtherBean.class))); + + try (DataStoreTransaction t = ds1.beginTransaction()) { + assertFalse(t.loadObjects(EntityProjection.builder() + .type(FirstBean.class) + .build(), null).iterator().hasNext()); + + FirstBean firstBean = FirstBean.class.getDeclaredConstructor().newInstance(); + firstBean.setName("name"); + t.createObject(firstBean, null); + //t.save(firstBean); + assertFalse(t.loadObjects(EntityProjection.builder() + .type(FirstBean.class) + .build(), null).iterator().hasNext()); + t.commit(null); + } catch (InstantiationException | IllegalAccessException e) { + log.error("", e); + } + try (DataStoreTransaction t = multiplexManager.beginTransaction()) { + OtherBean otherBean = OtherBean.class.getDeclaredConstructor().newInstance(); + t.createObject(otherBean, null); + + FirstBean firstBean = (FirstBean) t.loadObjects(EntityProjection.builder() + .type(FirstBean.class) + .build(), null).iterator().next(); + firstBean.setName("update"); + //t.save(firstBean, null); + t.save(firstBean, null); + + assertThrows(TransactionException.class, () -> t.commit(null)); + } catch (InstantiationException | IllegalAccessException e) { + log.error("", e); + } + // verify state + try (DataStoreTransaction t = ds1.beginTransaction()) { + Iterable beans = t.loadObjects(EntityProjection.builder() + .type(FirstBean.class) + .build(), null); + assertNotNull(beans); + ArrayList list = Lists.newArrayList(beans.iterator()); + assertEquals(1, list.size()); + assertEquals("name", ((FirstBean) list.get(0)).getName()); } } From f002634a399cd744d6d59ad05f53a240cf845be4 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Thu, 4 May 2023 03:40:06 +0800 Subject: [PATCH 4/8] Add support in annotation to list managed classes --- .gitignore | 1 + .../elide/datastores/jpa/JpaDataStore.java | 14 +- .../datastores/jpa/JpaDataStoreTest.java | 7 + .../spring/config/ElideAutoConfiguration.java | 112 ++-- .../orm/jpa/PlatformJpaTransaction.java | 3 +- .../orm/jpa/config/EnableJpaDataStore.java | 8 + .../jpa/config/JpaDataStoreRegistration.java | 10 +- .../jpa/config/JpaDataStoreRegistrations.java | 104 +++ .../config/ElideAutoConfigurationTest.java | 327 ---------- ...ElideAutoConfigurationTransactionTest.java | 599 ++++++++++++++++++ .../src/test/resources/application.yaml | 3 + 11 files changed, 793 insertions(+), 395 deletions(-) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrations.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTransactionTest.java diff --git a/.gitignore b/.gitignore index c618f5bbe7..4f5d67d5ef 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dependency-reduced-pom.xml *.factorypath *.vscode .DS_Store +tmlog*.log \ No newline at end of file diff --git a/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java b/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java index 9835d4fc0c..b5694a19b4 100644 --- a/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java +++ b/elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/JpaDataStore.java @@ -35,19 +35,19 @@ public class JpaDataStore implements JPQLDataStore { protected final JpaTransactionSupplier writeTransactionSupplier; protected final MetamodelSupplier metamodelSupplier; protected final Set> modelsToBind; - protected final QueryLogger logger; + protected final QueryLogger queryLogger; private JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier readTransactionSupplier, JpaTransactionSupplier writeTransactionSupplier, - QueryLogger logger, + QueryLogger queryLogger, MetamodelSupplier metamodelSupplier, Type[] models) { this.entityManagerSupplier = entityManagerSupplier; this.readTransactionSupplier = readTransactionSupplier; this.writeTransactionSupplier = writeTransactionSupplier; this.metamodelSupplier = metamodelSupplier; - this.logger = logger; + this.queryLogger = queryLogger; this.modelsToBind = new HashSet<>(); if (models != null) { Collections.addAll(this.modelsToBind, models); @@ -61,18 +61,18 @@ private JpaDataStore(EntityManagerSupplier entityManagerSupplier, public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier readTransactionSupplier, JpaTransactionSupplier writeTransactionSupplier, - QueryLogger logger, + QueryLogger queryLogger, MetamodelSupplier metamodelSupplier) { - this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, logger, + this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, queryLogger, metamodelSupplier, null); } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, JpaTransactionSupplier readTransactionSupplier, JpaTransactionSupplier writeTransactionSupplier, - QueryLogger logger, + QueryLogger queryLogger, Type ... models) { - this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, logger, null, models); + this(entityManagerSupplier, readTransactionSupplier, writeTransactionSupplier, queryLogger, null, models); } public JpaDataStore(EntityManagerSupplier entityManagerSupplier, diff --git a/elide-datastore/elide-datastore-jpa/src/test/java/com/yahoo/elide/datastores/jpa/JpaDataStoreTest.java b/elide-datastore/elide-datastore-jpa/src/test/java/com/yahoo/elide/datastores/jpa/JpaDataStoreTest.java index 46f211a295..8d27e28486 100644 --- a/elide-datastore/elide-datastore-jpa/src/test/java/com/yahoo/elide/datastores/jpa/JpaDataStoreTest.java +++ b/elide-datastore/elide-datastore-jpa/src/test/java/com/yahoo/elide/datastores/jpa/JpaDataStoreTest.java @@ -7,6 +7,7 @@ package com.yahoo.elide.datastores.jpa; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -97,4 +98,10 @@ class Test { assertNotNull(dictionary.lookupBoundClass(ClassType.of(Test.class))); } + + @Test + void shouldThrowIllegalArgumentExceptionIfNoMetamodelSupplier() { + EntityManager managerMock = mock(EntityManager.class); + assertThrows(IllegalArgumentException.class, () -> new JpaDataStore(() -> managerMock, unused -> null)); + } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java index 6e7f450d27..9aed5da417 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java @@ -71,6 +71,7 @@ import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStores; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistration; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrations; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilder; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilderCustomizer; import com.fasterxml.jackson.databind.ObjectMapper; @@ -360,27 +361,25 @@ public QueryEngine buildQueryEngine(DataSource defaultDataSource, } /** - * Create a JPA Transaction Supplier to use. - * @param transactionManager Spring Platform Transaction Manager - * @param entityManagerFactory An instance of EntityManagerFactory - * @param settings Elide configuration settings. - * @return the JpaTransactionSupplier - */ - public JpaTransactionSupplier jpaTransactionSupplier(PlatformTransactionManager transactionManager, - EntityManagerFactory entityManagerFactory, ElideConfigProperties settings) { - return new PlatformJpaTransactionSupplier( - new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED), transactionManager, - entityManagerFactory, settings.getJpaStore().isDelegateToInMemoryStore()); - } - - /** - * Create an Entity Manager Supplier to use. - * @return the EntityManagerSupplier + * Creates the default JpaDataStoreRegistrationsBuilder and applies + * customizations. + * + *

+ * If this bean is already defined Elide will not attempt to discover + * JpaDataStore registrations and the JpaDataStores to create can be fully + * configured. + * + *

+ * If only minor customizations are required a + * {@link JpaDataStoreRegistrationsBuilderCustomizer} can be defined to customize the + * builder. + * + * @param applicationContext the application context. + * @param settings Elide configuration settings. + * @param optionalQueryLogger the optional query logger. + * @param customizerProviders the customizer providers. + * @return the default JpaDataStoreRegistrationsBuilder. */ - public EntityManagerSupplier entityManagerSupplier() { - return new EntityManagerProxySupplier(); - } - @Bean @ConditionalOnMissingBean @Scope(SCOPE_PROTOTYPE) @@ -394,32 +393,32 @@ public JpaDataStoreRegistrationsBuilder jpaDataStoreRegistrationsBuilder( String[] platformTransactionManagerNames = applicationContext .getBeanNamesForType(PlatformTransactionManager.class); - if (entityManagerFactoryNames.length == 1 && platformTransactionManagerNames.length == 1) { - // Basic scenario + Map beans = new HashMap<>(); + beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStore.class)); + beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStores.class)); + if (!beans.isEmpty()) { + // If there is explicit configuration + beans.values().stream().forEach(bean -> { + EnableJpaDataStore[] annotations = bean.getClass() + .getAnnotationsByType(EnableJpaDataStore.class); + for (EnableJpaDataStore annotation : annotations) { + String entityManagerFactoryName = annotation.entityManagerFactoryRef(); + String platformTransactionManagerName = annotation.transactionManagerRef(); + if (!StringUtils.isBlank(entityManagerFactoryName) + && !StringUtils.isBlank(platformTransactionManagerName)) { + builder.add(buildJpaDataStoreRegistration(applicationContext, entityManagerFactoryName, + platformTransactionManagerName, settings, optionalQueryLogger, + annotation.managedClasses())); + } + } + }); + } else if (entityManagerFactoryNames.length == 1 && platformTransactionManagerNames.length == 1) { + // If there is no explicit configuration but just one entity manager factory and + // transaction manager configure it String platformTransactionManagerName = platformTransactionManagerNames[0]; String entityManagerFactoryName = entityManagerFactoryNames[0]; - builder.add(jpaDataStoreRegistration(applicationContext, entityManagerFactoryName, - platformTransactionManagerName, settings, optionalQueryLogger)); - } else { - // Multiple scenario - Map beans = new HashMap<>(); - beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStore.class)); - beans.putAll(applicationContext.getBeansWithAnnotation(EnableJpaDataStores.class)); - if (!beans.isEmpty()) { - beans.values().stream().forEach(bean -> { - EnableJpaDataStore[] annotations = bean.getClass() - .getAnnotationsByType(EnableJpaDataStore.class); - for (EnableJpaDataStore annotation : annotations) { - String entityManagerFactoryName = annotation.entityManagerFactoryRef(); - String platformTransactionManagerName = annotation.transactionManagerRef(); - if (!StringUtils.isBlank(entityManagerFactoryName) - && !StringUtils.isBlank(platformTransactionManagerName)) { - builder.add(jpaDataStoreRegistration(applicationContext, entityManagerFactoryName, - platformTransactionManagerName, settings, optionalQueryLogger)); - } - } - }); - } + builder.add(buildJpaDataStoreRegistration(applicationContext, entityManagerFactoryName, + platformTransactionManagerName, settings, optionalQueryLogger, new Class[] {})); } customizerProviders.orderedStream().forEach(customizer -> customizer.customize(builder)); @@ -434,22 +433,19 @@ public JpaDataStoreRegistrationsBuilder jpaDataStoreRegistrationsBuilder( * @param platformTransactionManagerName the bean name of the platform transaction manager * @param settings the settings * @param optionalQueryLogger the optional query logger - * @return + * @return the JpaDataStoreRegistration read from the application context. */ - private JpaDataStoreRegistration jpaDataStoreRegistration(ApplicationContext applicationContext, + private JpaDataStoreRegistration buildJpaDataStoreRegistration(ApplicationContext applicationContext, String entityManagerFactoryName, String platformTransactionManagerName, ElideConfigProperties settings, - Optional optionalQueryLogger) { - PlatformTransactionManager transactionManager = applicationContext + Optional optionalQueryLogger, + Class[] managedClasses) { + PlatformTransactionManager platformTransactionManager = applicationContext .getBean(platformTransactionManagerName, PlatformTransactionManager.class); EntityManagerFactory entityManagerFactory = applicationContext.getBean(entityManagerFactoryName, EntityManagerFactory.class); - JpaTransactionSupplier jpaTransactionSupplier = jpaTransactionSupplier(transactionManager, - entityManagerFactory, settings); - return JpaDataStoreRegistration.builder().name(entityManagerFactoryName) - .entityManagerSupplier(entityManagerSupplier()).readTransactionSupplier(jpaTransactionSupplier) - .writeTransactionSupplier(jpaTransactionSupplier) - .metamodelSupplier(entityManagerFactory::getMetamodel) - .logger(optionalQueryLogger.orElse(JpaDataStore.DEFAULT_LOGGER)).build(); + return JpaDataStoreRegistrations.buildJpaDataStoreRegistration(entityManagerFactoryName, entityManagerFactory, + platformTransactionManagerName, platformTransactionManager, settings, optionalQueryLogger, + managedClasses); } /** @@ -493,14 +489,14 @@ public static DataStore buildDataStore(JpaDataStoreRegistrationsBuilder builder, List stores = new ArrayList<>(); builder.build().forEach(registration -> { - if (registration.getModelsToBind() != null && !registration.getModelsToBind().isEmpty()) { + if (registration.getManagedClasses() != null && !registration.getManagedClasses().isEmpty()) { stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), - registration.getLogger(), registration.getModelsToBind().toArray(Type[]::new))); + registration.getQueryLogger(), registration.getManagedClasses().toArray(Type[]::new))); } else { stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), - registration.getLogger(), registration.getMetamodelSupplier())); + registration.getQueryLogger(), registration.getMetamodelSupplier())); } }); diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java index 0710c22d16..3ebe2be38a 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/PlatformJpaTransaction.java @@ -53,9 +53,10 @@ public void begin() { EntityManagerHolder entityManagerHolder = (EntityManagerHolder) TransactionSynchronizationManager .getResource(this.entityManagerFactory); if (entityManagerHolder != null) { - // This is the JpaTransactionManager + // This is for the JpaTransactionManager supplierEntityManager.setEntityManager(entityManagerHolder.getEntityManager()); } else { + // This is for the JtaTransactionManager supplierEntityManager.setEntityManager(this.entityManagerFactory.createEntityManager()); } } else { diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java index 6da096daa0..a5916c8c0e 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/EnableJpaDataStore.java @@ -33,4 +33,12 @@ * @return the platform transaction manager bean name. */ String transactionManagerRef() default "transactionManager"; + + /** + * The entity classes to manage. Otherwise all entities in the entity manager + * factory will be managed. + * + * @return the entity classes to manage + */ + Class[] managedClasses() default {}; } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java index 638f9b1dde..4e4d2e4a35 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistration.java @@ -36,7 +36,13 @@ public class JpaDataStoreRegistration { @Getter private final MetamodelSupplier metamodelSupplier; @Getter - private final Set> modelsToBind; + private final Set> managedClasses; @Getter - private final QueryLogger logger; + private final QueryLogger queryLogger; + + /** + * Used to build a JpaDataStore registration. + */ + public static class JpaDataStoreRegistrationBuilder { + } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrations.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrations.java new file mode 100644 index 0000000000..fa2b32ee95 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/orm/jpa/config/JpaDataStoreRegistrations.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.orm.jpa.config; + + +import com.yahoo.elide.core.type.ClassType; +import com.yahoo.elide.core.type.Type; +import com.yahoo.elide.datastores.jpa.JpaDataStore; +import com.yahoo.elide.datastores.jpa.JpaDataStore.EntityManagerSupplier; +import com.yahoo.elide.datastores.jpa.JpaDataStore.JpaTransactionSupplier; +import com.yahoo.elide.spring.config.ElideConfigProperties; +import com.yahoo.elide.spring.orm.jpa.EntityManagerProxySupplier; +import com.yahoo.elide.spring.orm.jpa.PlatformJpaTransactionSupplier; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistration.JpaDataStoreRegistrationBuilder; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import jakarta.persistence.EntityManagerFactory; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * JpaDataStoreRegistrations. + */ +public class JpaDataStoreRegistrations { + private JpaDataStoreRegistrations() { + } + + /** + * Creates a JpaDataStore registration from inputs. + * + * @param entityManagerFactory the bean name of the entity manager factory + * @param platformTransactionManager the bean name of the platform transaction manager + * @param settings the settings + * @param optionalQueryLogger the optional query logger + * @return the JpaDataStore registration. + */ + public static JpaDataStoreRegistration buildJpaDataStoreRegistration(String entityManagerFactoryName, + EntityManagerFactory entityManagerFactory, String platformTransactionManagerName, + PlatformTransactionManager platformTransactionManager, ElideConfigProperties settings, + Optional optionalQueryLogger, + Class[] managedClasses) { + DefaultTransactionDefinition writeJpaTransactionDefinition = new DefaultTransactionDefinition( + TransactionDefinition.PROPAGATION_REQUIRED); + writeJpaTransactionDefinition.setName( + "Elide Write Transaction (" + entityManagerFactoryName + "," + platformTransactionManagerName + ")"); + JpaTransactionSupplier writeJpaTransactionSupplier = buildJpaTransactionSupplier(platformTransactionManager, + entityManagerFactory, writeJpaTransactionDefinition, settings); + + DefaultTransactionDefinition readJpaTransactionDefinition = new DefaultTransactionDefinition( + TransactionDefinition.PROPAGATION_REQUIRED); + readJpaTransactionDefinition.setName( + "Elide Read Transaction (" + entityManagerFactoryName + "," + platformTransactionManagerName + ")"); + readJpaTransactionDefinition.setReadOnly(true); + JpaTransactionSupplier readJpaTransactionSupplier = buildJpaTransactionSupplier(platformTransactionManager, + entityManagerFactory, readJpaTransactionDefinition, settings); + + JpaDataStoreRegistrationBuilder builder = JpaDataStoreRegistration.builder().name(entityManagerFactoryName) + .entityManagerSupplier(buildEntityManagerSupplier()) + .readTransactionSupplier(readJpaTransactionSupplier) + .writeTransactionSupplier(writeJpaTransactionSupplier) + .queryLogger(optionalQueryLogger.orElse(JpaDataStore.DEFAULT_LOGGER)); + if (managedClasses != null && managedClasses.length > 0) { + Set> models = new HashSet<>(); + Arrays.stream(managedClasses).map(ClassType::of).forEach(models::add); + builder.managedClasses(models); + } else { + builder.metamodelSupplier(entityManagerFactory::getMetamodel); + } + + return builder.build(); + } + + /** + * Create a JPA Transaction Supplier to use. + * @param transactionManager Spring Platform Transaction Manager + * @param entityManagerFactory An instance of EntityManagerFactory + * @param settings Elide configuration settings. + * @return the JpaTransactionSupplier. + */ + public static JpaTransactionSupplier buildJpaTransactionSupplier(PlatformTransactionManager transactionManager, + EntityManagerFactory entityManagerFactory, TransactionDefinition transactionDefinition, + ElideConfigProperties settings) { + return new PlatformJpaTransactionSupplier( + transactionDefinition, transactionManager, + entityManagerFactory, settings.getJpaStore().isDelegateToInMemoryStore()); + } + + /** + * Create an Entity Manager Supplier to use. + * @return a EntityManagerProxySupplier. + */ + public static EntityManagerSupplier buildEntityManagerSupplier() { + return new EntityManagerProxySupplier(); + } +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java index dd0a6c0f03..71e972b35a 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTest.java @@ -7,65 +7,26 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.yahoo.elide.RefreshableElide; -import com.yahoo.elide.core.datastore.DataStore; -import com.yahoo.elide.core.datastore.DataStoreTransaction; -import com.yahoo.elide.datastores.multiplex.MultiplexManager; -import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; - -import com.atomikos.spring.AtomikosAutoConfiguration; -import com.atomikos.spring.AtomikosDataSourceBean; - -import example.models.jpa.ArtifactGroup; -import example.models.jpa.v2.ArtifactGroupV2; -import example.models.jpa.v3.ArtifactGroupV3; - -import org.hibernate.cfg.AvailableSettings; -import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.context.annotation.UserConfigurations; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; -import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.orm.jpa.EntityManagerFactoryUtils; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.RestController; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; - -import java.io.IOException; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; -import javax.sql.DataSource; -import javax.sql.XADataSource; - /** * Tests for ElideAutoConfiguration. */ @@ -200,292 +161,4 @@ void overrideController(OverrideControllerInput input) { } }); } - - @Configuration(proxyBeanMethods = false) - @EntityScan(basePackages = "example.models.jpa") - public static class EntityConfiguration { - } - - @Test - void dataStoreTransaction() { - contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") - .withUserConfiguration(EntityConfiguration.class).run(context -> { - - DataStore dataStore = context.getBean(DataStore.class); - PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); - EntityManagerFactory entityManagerFactory = context.getBean(EntityManagerFactory.class); - - TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroup artifactGroup = new ArtifactGroup(); - artifactGroup.setName("Group"); - transaction.createObject(artifactGroup, null); - transaction.flush(null); - - ArtifactGroup found = transactionTemplate.execute(status -> { - assertThat(status.isNewTransaction()).isFalse(); - EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); - return entityManager.find(ArtifactGroup.class, artifactGroup.getName()); - }); - assertThat(artifactGroup).isEqualTo(found); - // Not committed so should rollback - } - - transactionTemplate.execute(status -> { - assertThat(status.isNewTransaction()).isTrue(); - EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); - ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); - assertThat(found).isNull(); - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroup artifactGroup = new ArtifactGroup(); - artifactGroup.setName("Group"); - transaction.createObject(artifactGroup, null); - transaction.commit(null); - } catch (IOException e) { - } - - found = entityManager.find(ArtifactGroup.class, "Group"); - assertThat(found).isNotNull(); - status.setRollbackOnly(); // Rollback - return null; - }); - - // Verify it has been rolled back - transactionTemplate.execute(status -> { - assertThat(status.isNewTransaction()).isTrue(); - EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); - ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); - assertThat(found).isNull(); - return null; - }); - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroup artifactGroup = new ArtifactGroup(); - artifactGroup.setName("Group"); - transaction.createObject(artifactGroup, null); - transaction.flush(null); - - ArtifactGroup found = transactionTemplate.execute(status -> { - assertThat(status.isNewTransaction()).isFalse(); - EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); - return entityManager.find(ArtifactGroup.class, artifactGroup.getName()); - }); - assertThat(artifactGroup).isEqualTo(found); - transaction.commit(null); - } - - // Verify that it has been committed - transactionTemplate.execute(status -> { - assertThat(status.isNewTransaction()).isTrue(); - EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); - ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); - assertThat(found).isNotNull(); - return null; - }); - - }); - } - - @Configuration(proxyBeanMethods = false) - public static class MultipleDataSourceConfiguration { - @Bean - public DataSource dataSourceV2() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1").build(); - } - - @Bean - public DataSource dataSourceV3() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1").build(); - } - - @Bean - public EntityManagerFactoryBuilder entityManagerFactoryBuilder( - ObjectProvider persistenceUnitManager, - ObjectProvider customizers) { - EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), - new HashMap<>(), persistenceUnitManager.getIfAvailable()); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; - } - } - - @Configuration(proxyBeanMethods = false) - @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManagerV2") - @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManagerV3") - public static class MultipleEntityManagerFactoryConfiguration { - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSourceV2) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) - .packages("example.models.jpa.v2").properties(vendorProperties).build(); - return emf; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSourceV3) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) - .packages("example.models.jpa.v3").properties(vendorProperties).build(); - return emf; - } - - @Bean - public PlatformTransactionManager transactionManagerV2(EntityManagerFactory entityManagerFactoryV2) { - return new JpaTransactionManager(entityManagerFactoryV2); - } - - @Bean - public PlatformTransactionManager transactionManagerV3(EntityManagerFactory entityManagerFactoryV3) { - return new JpaTransactionManager(entityManagerFactoryV3); - } - } - - @Test - void multiplexDataStoreTransaction() { - contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") - .withUserConfiguration(MultipleDataSourceConfiguration.class, MultipleEntityManagerFactoryConfiguration.class).run(context -> { - DataStore dataStore = context.getBean(DataStore.class); - assertThat(dataStore).isInstanceOf(MultiplexManager.class); - - // The data store will only be initialized properly by elide to populate the dictionary - RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); - dataStore = refreshableElide.getElide().getDataStore(); - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("JPA Group V2a"); - transaction.save(artifactGroupV2, null); - transaction.commit(null); - } - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("JPA Group V3a"); - transaction.save(artifactGroupV3, null); - transaction.commit(null); - } - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("JPA Group V2b"); - - ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("JPA Group V3b"); - - transaction.save(artifactGroupV2, null); - transaction.save(artifactGroupV3, null); - - transaction.commit(null); - } - }); - } - - @Configuration(proxyBeanMethods = false) - public static class MultipleDataSourceJtaConfiguration { - @Bean - public DataSource dataSourceV2() { - DataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1") - .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).build(); - AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); - atomikosDataSource.setXaDataSource((XADataSource) xaDataSource); - return atomikosDataSource; - } - - @Bean - public DataSource dataSourceV3() { - DataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1") - .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).build(); - AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); - atomikosDataSource.setXaDataSource((XADataSource) xaDataSource); - return atomikosDataSource; - } - - @Bean - public EntityManagerFactoryBuilder entityManagerFactoryBuilder( - ObjectProvider persistenceUnitManager, - ObjectProvider customizers) { - EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), - new HashMap<>(), persistenceUnitManager.getIfAvailable()); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; - } - } - - @Configuration(proxyBeanMethods = false) - @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManager") - @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManager") - public static class MultipleEntityManagerFactoryJtaConfiguration { - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSourceV2, JtaTransactionManager transactionManager) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) - .packages("example.models.jpa.v2").properties(vendorProperties).jta(true).build(); - return emf; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSourceV3, JtaTransactionManager transactionManager) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) - .packages("example.models.jpa.v3").properties(vendorProperties).jta(true).build(); - return emf; - } - } - - @Test - void multiplexDataStoreJtaTransaction() { - contextRunner - .withPropertyValues("spring.cloud.refresh.enabled=false", - "atomikos.properties.max-timeout=0") - .withConfiguration(AutoConfigurations.of(AtomikosAutoConfiguration.class)) - .withUserConfiguration(MultipleDataSourceJtaConfiguration.class, MultipleEntityManagerFactoryJtaConfiguration.class).run(context -> { - DataStore dataStore = context.getBean(DataStore.class); - assertThat(dataStore).isInstanceOf(MultiplexManager.class); - - // The data store will only be initialized properly by elide to populate the dictionary - RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); - dataStore = refreshableElide.getElide().getDataStore(); - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("JTA Group V2"); - transaction.save(artifactGroupV2, null); - transaction.commit(null); - } - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("JTA Group V3"); - transaction.save(artifactGroupV3, null); - transaction.commit(null); - } - - try (DataStoreTransaction transaction = dataStore.beginTransaction()) { - ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); - artifactGroupV2.setName("JTA Group V2"); - - ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); - artifactGroupV3.setName("JTA Group V3"); - - transaction.save(artifactGroupV2, null); - transaction.save(artifactGroupV3, null); - - transaction.commit(null); - } - }); - } } diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTransactionTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTransactionTest.java new file mode 100644 index 0000000000..99b899ec1f --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/config/ElideAutoConfigurationTransactionTest.java @@ -0,0 +1,599 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.config; + +import static com.yahoo.elide.core.dictionary.EntityDictionary.NO_VERSION; +import static org.assertj.core.api.Assertions.assertThat; + +import com.yahoo.elide.RefreshableElide; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.datastore.DataStore; +import com.yahoo.elide.core.datastore.DataStoreTransaction; +import com.yahoo.elide.core.dictionary.EntityDictionary; +import com.yahoo.elide.core.request.EntityProjection; +import com.yahoo.elide.datastores.jpa.JpaDataStore; +import com.yahoo.elide.datastores.multiplex.MultiplexManager; +import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistration; +import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrations; +import com.atomikos.spring.AtomikosAutoConfiguration; +import com.atomikos.spring.AtomikosDataSourceBean; + +import example.models.jpa.ArtifactGroup; +import example.models.jpa.v2.ArtifactGroupV2; +import example.models.jpa.v3.ArtifactGroupV3; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.persistenceunit.PersistenceUnitManager; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.sql.DataSource; +import javax.sql.XADataSource; + +/** + * Tests for ElideAutoConfiguration transaction. + */ +class ElideAutoConfigurationTransactionTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElideAutoConfiguration.class, DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class, RefreshAutoConfiguration.class)); + + @Configuration(proxyBeanMethods = false) + @EntityScan(basePackages = "example.models.jpa") + public static class EntityConfiguration { + } + + @Test + void dataStoreTransaction() { + contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") + .withUserConfiguration(EntityConfiguration.class).run(context -> { + + DataStore dataStore = context.getBean(DataStore.class); + PlatformTransactionManager transactionManager = context.getBean(PlatformTransactionManager.class); + EntityManagerFactory entityManagerFactory = context.getBean(EntityManagerFactory.class); + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroup artifactGroup = new ArtifactGroup(); + artifactGroup.setName("Group"); + transaction.createObject(artifactGroup, null); + transaction.flush(null); + + ArtifactGroup found = transactionTemplate.execute(status -> { + assertThat(status.isNewTransaction()).isFalse(); + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + return entityManager.find(ArtifactGroup.class, artifactGroup.getName()); + }); + assertThat(artifactGroup).isEqualTo(found); + // Not committed so should rollback + } + + transactionTemplate.execute(status -> { + assertThat(status.isNewTransaction()).isTrue(); + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); + assertThat(found).isNull(); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroup artifactGroup = new ArtifactGroup(); + artifactGroup.setName("Group"); + transaction.createObject(artifactGroup, null); + transaction.commit(null); + } catch (IOException e) { + } + + found = entityManager.find(ArtifactGroup.class, "Group"); + assertThat(found).isNotNull(); + status.setRollbackOnly(); // Rollback + return null; + }); + + // Verify it has been rolled back + transactionTemplate.execute(status -> { + assertThat(status.isNewTransaction()).isTrue(); + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); + assertThat(found).isNull(); + return null; + }); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroup artifactGroup = new ArtifactGroup(); + artifactGroup.setName("Group"); + transaction.createObject(artifactGroup, null); + transaction.flush(null); + + ArtifactGroup found = transactionTemplate.execute(status -> { + assertThat(status.isNewTransaction()).isFalse(); + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + return entityManager.find(ArtifactGroup.class, artifactGroup.getName()); + }); + assertThat(artifactGroup).isEqualTo(found); + transaction.commit(null); + } + + // Verify that it has been committed + transactionTemplate.execute(status -> { + assertThat(status.isNewTransaction()).isTrue(); + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + ArtifactGroup found = entityManager.find(ArtifactGroup.class, "Group"); + assertThat(found).isNotNull(); + return null; + }); + + }); + } + + @Configuration(proxyBeanMethods = false) + public static class MultipleDataSourceJpaConfiguration { + @Bean + public DataSource dataSourceV2() { + return DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1").username("sa").password("").build(); + } + + @Bean + public DataSource dataSourceV3() { + return DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1").username("sa").password("").build(); + } + + @Bean + public EntityManagerFactoryBuilder entityManagerFactoryBuilder( + ObjectProvider persistenceUnitManager, + ObjectProvider customizers) { + EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), + new HashMap<>(), persistenceUnitManager.getIfAvailable()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + } + + /** + * This creates 2 entity manager factories each with its own JPA transaction + * manager. As they are using separate transaction managers, commits and + * rollbacks don't affect each other. + */ + @Configuration(proxyBeanMethods = false) + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManagerV2") + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManagerV3") + public static class MultipleEntityManagerFactoryJpaConfiguration { + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV2) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) + .packages("example.models.jpa.v2").properties(vendorProperties).build(); + return emf; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV3) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) + .packages("example.models.jpa.v3").properties(vendorProperties).build(); + return emf; + } + + @Bean + public PlatformTransactionManager transactionManagerV2(EntityManagerFactory entityManagerFactoryV2) { + return new JpaTransactionManager(entityManagerFactoryV2); + } + + @Bean + public PlatformTransactionManager transactionManagerV3(EntityManagerFactory entityManagerFactoryV3) { + return new JpaTransactionManager(entityManagerFactoryV3); + } + } + + @Test + void multiplexDataStoreJpaTransaction() { + contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") + .withUserConfiguration(MultipleDataSourceJpaConfiguration.class, MultipleEntityManagerFactoryJpaConfiguration.class).run(context -> { + DataStore dataStore = context.getBean(DataStore.class); + assertThat(dataStore).isInstanceOf(MultiplexManager.class); + + // The data store will only be initialized properly by elide to populate the dictionary + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + dataStore = refreshableElide.getElide().getDataStore(); + + RequestScope scope = new RequestScope(null, null, NO_VERSION, null, null, null, null, null, + UUID.randomUUID(), refreshableElide.getElide().getElideSettings()); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JPA Group V2a"); + transaction.save(artifactGroupV2, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JPA Group V3a"); + transaction.save(artifactGroupV3, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JPA Group V2b"); + + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JPA Group V3b"); + + transaction.save(artifactGroupV2, null); + transaction.save(artifactGroupV3, null); + + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JPA Group V2b", scope); + assertThat(artifactGroupV2).isNotNull(); + + ArtifactGroupV3 artifactGroupV3 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JPA Group V3b", scope); + assertThat(artifactGroupV3).isNotNull(); + } + + try (DataStoreTransaction transaction = dataStore.beginReadTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JPA Group V2b", scope); + assertThat(artifactGroupV2).isNotNull(); + + ArtifactGroupV3 artifactGroupV3 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JPA Group V3b", scope); + assertThat(artifactGroupV3).isNotNull(); + } + }); + } + + @Test + void multipleDataStoreJpaTransaction() { + contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false") + .withUserConfiguration(MultipleDataSourceJpaConfiguration.class, MultipleEntityManagerFactoryJpaConfiguration.class) + .run(context -> { + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + RequestScope scope = new RequestScope(null, null, NO_VERSION, null, null, null, null, null, + UUID.randomUUID(), refreshableElide.getElide().getElideSettings()); + EntityManagerFactory entityManagerFactoryV2 = context.getBean("entityManagerFactoryV2", + EntityManagerFactory.class); + EntityManagerFactory entityManagerFactoryV3 = context.getBean("entityManagerFactoryV3", + EntityManagerFactory.class); + JpaTransactionManager transactionManagerV2 = context.getBean("transactionManagerV2", JpaTransactionManager.class); + JpaTransactionManager transactionManagerV3 = context.getBean("transactionManagerV3", JpaTransactionManager.class); + ElideConfigProperties settings = new ElideConfigProperties(); + settings.getJpaStore().setDelegateToInMemoryStore(true); + JpaDataStoreRegistration registrationV2 = JpaDataStoreRegistrations.buildJpaDataStoreRegistration( + "entityManagerFactoryV2", entityManagerFactoryV2, "transactionManagerV2", + transactionManagerV2, settings, Optional.empty(), null); + JpaDataStoreRegistration registrationV3 = JpaDataStoreRegistrations.buildJpaDataStoreRegistration( + "entityManagerFactoryV3", entityManagerFactoryV3, "transactionManagerV3", + transactionManagerV3, settings, Optional.empty(), null); + + EntityDictionary entityDictionary = EntityDictionary.builder().build(); + + JpaDataStore jpaDataStoreV2 = new JpaDataStore(registrationV2.getEntityManagerSupplier(), + registrationV2.getReadTransactionSupplier(), registrationV2.getWriteTransactionSupplier(), + registrationV2.getQueryLogger(), registrationV2.getMetamodelSupplier()); + + JpaDataStore jpaDataStoreV3 = new JpaDataStore(registrationV3.getEntityManagerSupplier(), + registrationV3.getReadTransactionSupplier(), registrationV3.getWriteTransactionSupplier(), + registrationV3.getQueryLogger(), registrationV3.getMetamodelSupplier()); + + jpaDataStoreV2.populateEntityDictionary(entityDictionary); + jpaDataStoreV3.populateEntityDictionary(entityDictionary); + + // Test that the rollback in the outer transaction works + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JPA V2"); + transaction1.save(artifactGroupV2, null); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JPA V3"); + transaction2.save(artifactGroupV3, null); + transaction2.commit(null); + } + // transaction1 wasn't committed and should rollback + } + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = transaction2.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JPA V3", scope); + // although the outer transaction was rolled back the entity was created as the + // 2 transaction managers are separate a compensating transaction would be + // required in this case to reverse the inner transaction + assertThat(artifactGroupV3).isNotNull(); + transaction2.delete(artifactGroupV3, null); + } + + // Test that the commit in the outer transaction works + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JPA V2"); + transaction1.save(artifactGroupV2, null); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JPA V3"); + transaction2.save(artifactGroupV3, null); + transaction2.commit(null); + } + transaction1.commit(null); + } + + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction1.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JPA V2", scope); + assertThat(artifactGroupV2).isNotNull(); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = transaction2.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JPA V3", scope); + assertThat(artifactGroupV3).isNotNull(); + } + } + }); + } + + @Configuration(proxyBeanMethods = false) + public static class MultipleDataSourceJtaConfiguration { + @Bean + public DataSource dataSourceV2() { + XADataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1") + .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).username("sa") + .password("").build(); + AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); + atomikosDataSource.setXaDataSource(xaDataSource); + return atomikosDataSource; + } + + @Bean + public DataSource dataSourceV3() { + XADataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1") + .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).username("sa") + .password("").build(); + AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); + atomikosDataSource.setXaDataSource(xaDataSource); + return atomikosDataSource; + } + + @Bean + public EntityManagerFactoryBuilder entityManagerFactoryBuilder( + ObjectProvider persistenceUnitManager, + ObjectProvider customizers) { + EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), + new HashMap<>(), persistenceUnitManager.getIfAvailable()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + } + + /** + * This creates 2 entity manager factories with a shared JTA transaction + * manager. They will participate in a shared transaction. + */ + @Configuration(proxyBeanMethods = false) + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV2", transactionManagerRef = "transactionManager") + @EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactoryV3", transactionManagerRef = "transactionManager") + public static class MultipleEntityManagerFactoryJtaConfiguration { + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV2(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV2, JtaTransactionManager transactionManager) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV2) + .packages("example.models.jpa.v2").properties(vendorProperties).jta(true).build(); + return emf; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactoryV3(EntityManagerFactoryBuilder builder, + DefaultListableBeanFactory beanFactory, DataSource dataSourceV3, JtaTransactionManager transactionManager) { + Map vendorProperties = new HashMap<>(); + vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); + final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSourceV3) + .packages("example.models.jpa.v3").properties(vendorProperties).jta(true).build(); + return emf; + } + } + + @Test + void multiplexDataStoreJtaTransaction() { + contextRunner + .withPropertyValues("spring.cloud.refresh.enabled=false", + "atomikos.properties.max-timeout=0") + .withConfiguration(AutoConfigurations.of(AtomikosAutoConfiguration.class)) + .withUserConfiguration(MultipleDataSourceJtaConfiguration.class, MultipleEntityManagerFactoryJtaConfiguration.class).run(context -> { + DataStore dataStore = context.getBean(DataStore.class); + assertThat(dataStore).isInstanceOf(MultiplexManager.class); + + // The data store will only be initialized properly by elide to populate the dictionary + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + dataStore = refreshableElide.getElide().getDataStore(); + + RequestScope scope = new RequestScope(null, null, NO_VERSION, null, null, null, null, null, + UUID.randomUUID(), refreshableElide.getElide().getElideSettings()); + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA Group V2a"); + transaction.save(artifactGroupV2, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA Group V3a"); + transaction.save(artifactGroupV3, null); + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA Group V2b"); + + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA Group V3b"); + + transaction.save(artifactGroupV2, null); + transaction.save(artifactGroupV3, null); + + transaction.commit(null); + } + + try (DataStoreTransaction transaction = dataStore.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JTA Group V2b", scope); + assertThat(artifactGroupV2).isNotNull(); + + ArtifactGroupV3 artifactGroupV3 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JTA Group V3b", scope); + assertThat(artifactGroupV3).isNotNull(); + } + + try (DataStoreTransaction transaction = dataStore.beginReadTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JTA Group V2b", scope); + assertThat(artifactGroupV2).isNotNull(); + + ArtifactGroupV3 artifactGroupV3 = transaction.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JTA Group V3b", scope); + assertThat(artifactGroupV3).isNotNull(); + } + }); + } + + @Test + void multipleDataStoreJtaTransaction() { + contextRunner.withPropertyValues("spring.cloud.refresh.enabled=false", "atomikos.properties.max-timeout=0") + .withConfiguration(AutoConfigurations.of(AtomikosAutoConfiguration.class)) + .withUserConfiguration(MultipleDataSourceJtaConfiguration.class, + MultipleEntityManagerFactoryJtaConfiguration.class) + .run(context -> { + RefreshableElide refreshableElide = context.getBean(RefreshableElide.class); + RequestScope scope = new RequestScope(null, null, NO_VERSION, null, null, null, null, null, + UUID.randomUUID(), refreshableElide.getElide().getElideSettings()); + EntityManagerFactory entityManagerFactoryV2 = context.getBean("entityManagerFactoryV2", + EntityManagerFactory.class); + EntityManagerFactory entityManagerFactoryV3 = context.getBean("entityManagerFactoryV3", + EntityManagerFactory.class); + JtaTransactionManager jtaTransactionManager = context.getBean(JtaTransactionManager.class); + ElideConfigProperties settings = new ElideConfigProperties(); + settings.getJpaStore().setDelegateToInMemoryStore(true); + JpaDataStoreRegistration registrationV2 = JpaDataStoreRegistrations.buildJpaDataStoreRegistration( + "entityManagerFactoryV2", entityManagerFactoryV2, "transactionManager", + jtaTransactionManager, settings, Optional.empty(), null); + JpaDataStoreRegistration registrationV3 = JpaDataStoreRegistrations.buildJpaDataStoreRegistration( + "entityManagerFactoryV3", entityManagerFactoryV3, "transactionManager", + jtaTransactionManager, settings, Optional.empty(), null); + + EntityDictionary entityDictionary = EntityDictionary.builder().build(); + + JpaDataStore jpaDataStoreV2 = new JpaDataStore(registrationV2.getEntityManagerSupplier(), + registrationV2.getReadTransactionSupplier(), registrationV2.getWriteTransactionSupplier(), + registrationV2.getQueryLogger(), registrationV2.getMetamodelSupplier()); + + JpaDataStore jpaDataStoreV3 = new JpaDataStore(registrationV3.getEntityManagerSupplier(), + registrationV3.getReadTransactionSupplier(), registrationV3.getWriteTransactionSupplier(), + registrationV3.getQueryLogger(), registrationV3.getMetamodelSupplier()); + + jpaDataStoreV2.populateEntityDictionary(entityDictionary); + jpaDataStoreV3.populateEntityDictionary(entityDictionary); + + // Test that the rollback in the outer transaction works + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA V2"); + transaction1.save(artifactGroupV2, null); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA V3"); + transaction2.save(artifactGroupV3, null); + transaction2.commit(null); + } + // transaction1 wasn't committed and should rollback + } + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = transaction2.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JTA V3", scope); + // as the outer transaction was rolled back the entity isn't created + assertThat(artifactGroupV3).isNull(); + } + + // Test that the commit in the outer transaction works + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = new ArtifactGroupV2(); + artifactGroupV2.setName("JTA V2"); + transaction1.save(artifactGroupV2, null); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = new ArtifactGroupV3(); + artifactGroupV3.setName("JTA V3"); + transaction2.save(artifactGroupV3, null); + transaction2.commit(null); + } + transaction1.commit(null); + } + + try (DataStoreTransaction transaction1 = jpaDataStoreV2.beginTransaction()) { + ArtifactGroupV2 artifactGroupV2 = transaction1.loadObject( + EntityProjection.builder().type(ArtifactGroupV2.class).build(), "JTA V2", scope); + assertThat(artifactGroupV2).isNotNull(); + + try (DataStoreTransaction transaction2 = jpaDataStoreV3.beginTransaction()) { + ArtifactGroupV3 artifactGroupV3 = transaction2.loadObject( + EntityProjection.builder().type(ArtifactGroupV3.class).build(), "JTA V3", scope); + assertThat(artifactGroupV3).isNotNull(); + } + } + + }); + } +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/application.yaml b/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/application.yaml index 0a5964a45e..4a06e6f306 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/application.yaml +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/resources/application.yaml @@ -38,6 +38,9 @@ elide: config-api: enabled: false spring: + autoconfigure: + exclude: + - com.atomikos.spring.AtomikosAutoConfiguration jpa: show-sql: true properties: From 923155cb73b4c94616cbf14c6af94be80103cd00 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Tue, 9 May 2023 15:16:03 +0800 Subject: [PATCH 5/8] Add datastore customization --- .../spring/config/ElideAutoConfiguration.java | 97 +++++++++++-------- .../datastore/config/DataStoreBuilder.java | 57 +++++++++++ .../config/DataStoreBuilderCustomizer.java | 15 +++ .../config/DataStoreBuilderTest.java | 70 +++++++++++++ 4 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilder.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderCustomizer.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderTest.java diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java index 9aed5da417..0bc66acff0 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAutoConfiguration.java @@ -43,9 +43,6 @@ import com.yahoo.elide.datastores.aggregation.queryengines.sql.query.AggregateBeforeJoinOptimizer; import com.yahoo.elide.datastores.aggregation.validator.TemplateConfigValidator; import com.yahoo.elide.datastores.jpa.JpaDataStore; -import com.yahoo.elide.datastores.jpa.JpaDataStore.EntityManagerSupplier; -import com.yahoo.elide.datastores.jpa.JpaDataStore.JpaTransactionSupplier; -import com.yahoo.elide.datastores.multiplex.MultiplexManager; import com.yahoo.elide.graphql.QueryRunners; import com.yahoo.elide.jsonapi.JsonApiMapper; import com.yahoo.elide.jsonapi.links.DefaultJSONApiLinks; @@ -63,17 +60,17 @@ import com.yahoo.elide.spring.controllers.ExportController; import com.yahoo.elide.spring.controllers.GraphqlController; import com.yahoo.elide.spring.controllers.JsonApiController; +import com.yahoo.elide.spring.datastore.config.DataStoreBuilder; +import com.yahoo.elide.spring.datastore.config.DataStoreBuilderCustomizer; import com.yahoo.elide.spring.jackson.ObjectMapperBuilder; -import com.yahoo.elide.spring.orm.jpa.EntityManagerProxySupplier; -import com.yahoo.elide.spring.orm.jpa.PlatformJpaTransactionSupplier; -import com.yahoo.elide.swagger.OpenApiBuilder; -import com.yahoo.elide.utils.HeaderUtils; import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStore; import com.yahoo.elide.spring.orm.jpa.config.EnableJpaDataStores; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistration; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrations; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilder; import com.yahoo.elide.spring.orm.jpa.config.JpaDataStoreRegistrationsBuilderCustomizer; +import com.yahoo.elide.swagger.OpenApiBuilder; +import com.yahoo.elide.utils.HeaderUtils; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.customizers.OpenApiCustomizer; @@ -81,7 +78,6 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -97,8 +93,6 @@ import org.springframework.core.annotation.Order; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.util.function.SingletonSupplier; import graphql.execution.DataFetcherExceptionHandler; @@ -121,7 +115,6 @@ import java.util.Optional; import java.util.Set; import java.util.TimeZone; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -449,30 +442,53 @@ private JpaDataStoreRegistration buildJpaDataStoreRegistration(ApplicationContex } /** - * Creates the DataStore Elide. Override to use a different store. - * @param builder JpaDataStoreRegistrationsBuilder. - * @param settings Elide configuration settings. - * @param scanner Class Scanner + * Creates the default DataStoreBuilder to build the DataStore and applies + * customizations. + *

+ * Override this if the default auto configured DataStores are not desirable. + * + *

+ * If only minor customizations are required a + * {@link DataStoreBuilderCustomizer} can be defined to customize the builder. + * + * @param builder JpaDataStoreRegistrationsBuilder. + * @param settings Elide configuration settings. + * @param scanner Class Scanner * @param optionalQueryEngine QueryEngine instance for aggregation data store. - * @param optionalCache Analytics query cache + * @param optionalCache Analytics query cache * @param optionalQueryLogger Analytics query logger - * @param customizerProvider Provide customizers to add to the data store - * @return An instance of a JPA DataStore. + * @param customizerProvider Provide customizers to add to the data store + * @return the DataStoreBuilder. */ @Bean @ConditionalOnMissingBean @Scope(SCOPE_PROTOTYPE) - public DataStore dataStore(JpaDataStoreRegistrationsBuilder builder, ElideConfigProperties settings, + public DataStoreBuilder dataStoreBuilder(JpaDataStoreRegistrationsBuilder builder, ElideConfigProperties settings, ClassScanner scanner, Optional optionalQueryEngine, Optional optionalCache, Optional optionalQueryLogger, - ObjectProvider>> customizerProvider) { - return buildDataStore(builder, settings, scanner, optionalQueryEngine, optionalCache, optionalQueryLogger, + ObjectProvider customizerProvider) { + return buildDataStoreBuilder(builder, settings, scanner, optionalQueryEngine, optionalCache, + optionalQueryLogger, Optional.of( - stores -> customizerProvider.orderedStream().forEach(customizer -> customizer.accept(stores)))); + dataStoreBuilder -> customizerProvider.orderedStream() + .forEach(customizer -> customizer.customize(dataStoreBuilder)))); } /** - * Creates the DataStore Elide. + * Creates the DataStore. Override to use a different store. + * + * @param dataStoreBuilder + * @return the DataStore to be used by Elide. + */ + @Bean + @ConditionalOnMissingBean + @Scope(SCOPE_PROTOTYPE) + public DataStore dataStore(DataStoreBuilder dataStoreBuilder) { + return dataStoreBuilder.build(); + } + + /** + * Creates the default DataStoreBuilder to build the DataStore. * @param builder JpaDataStoreRegistrationsBuilder. * @param settings Elide configuration settings. * @param scanner Class Scanner @@ -480,21 +496,23 @@ public DataStore dataStore(JpaDataStoreRegistrationsBuilder builder, ElideConfig * @param optionalCache Analytics query cache * @param optionalQueryLogger Analytics query logger * @param optionalCustomizer Provide customizers to add to the data store - * @return An instance of a JPA DataStore. + * @return the DataStoreBuilder. */ - public static DataStore buildDataStore(JpaDataStoreRegistrationsBuilder builder, ElideConfigProperties settings, + public static DataStoreBuilder buildDataStoreBuilder(JpaDataStoreRegistrationsBuilder builder, + ElideConfigProperties settings, ClassScanner scanner, Optional optionalQueryEngine, Optional optionalCache, - Optional optionalQueryLogger, Optional>> optionalCustomizer) { - List stores = new ArrayList<>(); + Optional optionalQueryLogger, + Optional optionalCustomizer) { + DataStoreBuilder dataStoreBuilder = new DataStoreBuilder(); builder.build().forEach(registration -> { if (registration.getManagedClasses() != null && !registration.getManagedClasses().isEmpty()) { - stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), + dataStoreBuilder.dataStore(new JpaDataStore(registration.getEntityManagerSupplier(), registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), registration.getQueryLogger(), registration.getManagedClasses().toArray(Type[]::new))); } else { - stores.add(new JpaDataStore(registration.getEntityManagerSupplier(), + dataStoreBuilder.dataStore(new JpaDataStore(registration.getEntityManagerSupplier(), registration.getReadTransactionSupplier(), registration.getWriteTransactionSupplier(), registration.getQueryLogger(), registration.getMetamodelSupplier())); } @@ -508,9 +526,11 @@ public static DataStore buildDataStore(JpaDataStoreRegistrationsBuilder builder, if (isDynamicConfigEnabled(settings)) { optionalQueryEngine.ifPresent(queryEngine -> aggregationDataStoreBuilder .dynamicCompiledClasses(queryEngine.getMetaDataStore().getDynamicTypes())); - if (settings.getDynamicConfig().isConfigApiEnabled()) { - stores.add(new ConfigDataStore(settings.getDynamicConfig().getPath(), - new TemplateConfigValidator(scanner, settings.getDynamicConfig().getPath()))); + if (settings.getAggregationStore().getDynamicConfig().getConfigApi().isEnabled()) { + dataStoreBuilder + .dataStore(new ConfigDataStore(settings.getAggregationStore().getDynamicConfig().getPath(), + new TemplateConfigValidator(scanner, + settings.getAggregationStore().getDynamicConfig().getPath()))); } } optionalCache.ifPresent(aggregationDataStoreBuilder::cache); @@ -518,16 +538,11 @@ public static DataStore buildDataStore(JpaDataStoreRegistrationsBuilder builder, AggregationDataStore aggregationDataStore = aggregationDataStoreBuilder.build(); // meta data store needs to be put at first to populate meta data models - optionalQueryEngine.ifPresent(queryEngine -> stores.add(queryEngine.getMetaDataStore())); - stores.add(aggregationDataStore); - } - optionalCustomizer.ifPresent(customizer -> customizer.accept(stores)); - - if (stores.size() == 1) { - return stores.get(0); - } else { - return new MultiplexManager(stores.toArray(DataStore[]::new)); + optionalQueryEngine.ifPresent(queryEngine -> dataStoreBuilder.dataStore(queryEngine.getMetaDataStore())); + dataStoreBuilder.dataStore(aggregationDataStore); } + optionalCustomizer.ifPresent(customizer -> customizer.customize(dataStoreBuilder)); + return dataStoreBuilder; } /** diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilder.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilder.java new file mode 100644 index 0000000000..d5764ad2a6 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.datastore.config; + +import com.yahoo.elide.core.datastore.DataStore; +import com.yahoo.elide.datastores.multiplex.MultiplexManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Builder used a build the DataStore. + *

+ * By default this will use the {@link MultiplexManager} when there are multiple + * DataStores. + */ +public class DataStoreBuilder { + private final List dataStores = new ArrayList<>(); + private Function multiplexer = MultiplexManager::new; + + public DataStoreBuilder dataStores(List dataStores) { + this.dataStores.clear(); + this.dataStores.addAll(dataStores); + return this; + } + + public DataStoreBuilder dataStores(Consumer> customizer) { + customizer.accept(this.dataStores); + return this; + } + + public DataStoreBuilder dataStore(DataStore dataStore) { + this.dataStores.add(dataStore); + return this; + } + + public DataStoreBuilder multiplexer(Function multiplexer) { + this.multiplexer = Objects.requireNonNull(multiplexer, "Multiplexer cannot be null"); + return this; + } + + public DataStore build() { + if (this.dataStores.isEmpty()) { + return null; + } + if (this.dataStores.size() == 1) { + return this.dataStores.get(0); + } + return multiplexer.apply(this.dataStores.toArray(DataStore[]::new)); + } +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderCustomizer.java b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderCustomizer.java new file mode 100644 index 0000000000..49ea25bc86 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderCustomizer.java @@ -0,0 +1,15 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.datastore.config; + +/** + * Used to customize the DataStoreBuilder. + * + * @see DataStoreBuilder + */ +public interface DataStoreBuilderCustomizer { + void customize(DataStoreBuilder dataStoreBuilder); +} diff --git a/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderTest.java b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderTest.java new file mode 100644 index 0000000000..817580c842 --- /dev/null +++ b/elide-spring/elide-spring-boot-autoconfigure/src/test/java/com/yahoo/elide/spring/datastore/config/DataStoreBuilderTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2023, the original author or authors. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.spring.datastore.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.yahoo.elide.core.datastore.DataStore; +import com.yahoo.elide.core.datastore.DataStoreTransaction; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; +import com.yahoo.elide.core.dictionary.EntityDictionary; + +import example.models.jpa.ArtifactGroup; +import example.models.jpa.ArtifactProduct; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Tests for DataStoreBuilder. + */ +class DataStoreBuilderTest { + + @Test + void dataStore() { + DataStoreBuilder builder = new DataStoreBuilder(); + builder.dataStore(new HashMapDataStore(Arrays.asList(ArtifactGroup.class))); + assertThat(builder.build()).isInstanceOf(HashMapDataStore.class); + } + + @Test + void dataStores() { + DataStoreBuilder builder = new DataStoreBuilder(); + builder.dataStores(Collections.singletonList(new HashMapDataStore(Arrays.asList(ArtifactGroup.class)))); + assertThat(builder.build()).isInstanceOf(HashMapDataStore.class); + } + + @Test + void dataStoresCustomizer() { + DataStoreBuilder builder = new DataStoreBuilder(); + builder.dataStore(new HashMapDataStore(Arrays.asList(ArtifactGroup.class))); + builder.dataStores(dataStores -> dataStores.clear()); + assertThat(builder.build()).isNull(); + } + + @Test + void multiplexer() { + DataStoreBuilder builder = new DataStoreBuilder(); + builder.dataStore(new HashMapDataStore(Arrays.asList(ArtifactGroup.class))); + builder.dataStore(new HashMapDataStore(Arrays.asList(ArtifactProduct.class))); + builder.multiplexer(dataStores -> new CustomMultiplexManager()); + assertThat(builder.build()).isInstanceOf(CustomMultiplexManager.class); + } + + public static class CustomMultiplexManager implements DataStore { + + @Override + public void populateEntityDictionary(EntityDictionary dictionary) { + } + + @Override + public DataStoreTransaction beginTransaction() { + return null; + } + } +} From 3bbafaef72f2f03894396a7c75749d7b9dbdbcf5 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Sat, 13 May 2023 18:04:19 +0800 Subject: [PATCH 6/8] Fix test due to lock contention --- elide-integration-tests/pom.xml | 6 +++++- .../initialization/InMemoryDataStoreHarness.java | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/elide-integration-tests/pom.xml b/elide-integration-tests/pom.xml index 0fb9f8ab04..a2a6950112 100644 --- a/elide-integration-tests/pom.xml +++ b/elide-integration-tests/pom.xml @@ -50,7 +50,11 @@ elide-async ${elide.version} - + + com.yahoo.elide + elide-datastore-multiplex + ${elide.version} + org.projectlombok diff --git a/elide-integration-tests/src/test/java/com/yahoo/elide/initialization/InMemoryDataStoreHarness.java b/elide-integration-tests/src/test/java/com/yahoo/elide/initialization/InMemoryDataStoreHarness.java index 17cbdff134..fdf9d15b24 100644 --- a/elide-integration-tests/src/test/java/com/yahoo/elide/initialization/InMemoryDataStoreHarness.java +++ b/elide-integration-tests/src/test/java/com/yahoo/elide/initialization/InMemoryDataStoreHarness.java @@ -12,6 +12,7 @@ import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; import com.yahoo.elide.core.datastore.test.DataStoreTestHarness; import com.yahoo.elide.core.utils.DefaultClassScanner; +import com.yahoo.elide.datastores.multiplex.MultiplexManager; import com.google.common.collect.Sets; import example.Address; import example.Company; @@ -29,6 +30,7 @@ public class InMemoryDataStoreHarness implements DataStoreTestHarness { private InMemoryDataStore memoryStore; private HashMapDataStore mapStore; + private HashMapDataStore asyncStore; public InMemoryDataStoreHarness() { Set beanPackages = Sets.newHashSet( @@ -37,14 +39,18 @@ public InMemoryDataStoreHarness() { Invoice.class.getPackage(), Manager.class.getPackage(), BookV2.class.getPackage(), - AsyncQuery.class.getPackage(), Company.class.getPackage(), Address.class.getPackage() + ); + Set asyncBeanPackages = Sets.newHashSet( + AsyncQuery.class.getPackage() ); + mapStore = new HashMapDataStore(DefaultClassScanner.getInstance(), beanPackages); - memoryStore = new InMemoryDataStore(mapStore); + asyncStore = new HashMapDataStore(DefaultClassScanner.getInstance(), asyncBeanPackages); + memoryStore = new InMemoryDataStore(new MultiplexManager(mapStore, asyncStore)); } @Override @@ -55,5 +61,6 @@ public DataStore getDataStore() { @Override public void cleanseTestData() { mapStore.cleanseTestData(); + asyncStore.cleanseTestData(); } } From 8634e81a9630c252354391fd853bc776363dfcda Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Tue, 30 May 2023 12:04:53 +0800 Subject: [PATCH 7/8] Update pom --- .../elide-spring-boot-autoconfigure/pom.xml | 2 - pom.xml | 110 ++++++++++++------ 2 files changed, 75 insertions(+), 37 deletions(-) diff --git a/elide-spring/elide-spring-boot-autoconfigure/pom.xml b/elide-spring/elide-spring-boot-autoconfigure/pom.xml index 1bf8379ef5..baf75f24cf 100644 --- a/elide-spring/elide-spring-boot-autoconfigure/pom.xml +++ b/elide-spring/elide-spring-boot-autoconfigure/pom.xml @@ -39,7 +39,6 @@ utf-8 - 6.0.0 @@ -273,7 +272,6 @@ com.atomikos transactions-spring-boot3-starter - ${atomikos.version} test diff --git a/pom.xml b/pom.xml index 0b15490ab3..46f6b36b73 100644 --- a/pom.xml +++ b/pom.xml @@ -85,8 +85,10 @@ ${project.basedir}/target/lombok + 1.10.13 4.12.0 2.28.0 + 6.0.0 1.34.0 3.1.6 4.8.158 @@ -97,14 +99,17 @@ 2.12.0 3.12.0 0.11.0 - 3.0.1 + 1.2.3 + 3.0.1 20.2 22.3.2 + 31.1-jre 4.3.1 6.2.3.Final 8.0.0.Final 6.1.8.Final 3.0.0 + 4.5.13 2.10.1 2.1.214 5.0.1 @@ -116,15 +121,21 @@ 2.0.0 3.1.0 3.0.2 + 3.29.2-GA + 2.4.0 3.1.2 11.0.15 4.3.2 + 1.5.1 + 1.0.3 2.8.0 2.2.14 5.9.3 1.4.7 1.18.26 5.3.0 + 2.2.21 + 2.1.0 2.0.7 2.0 6.0.9 @@ -132,11 +143,35 @@ 4.0.2 2.1.0 2.2.10 + 1.2.1 10.1.8 5.3.1 + 4.3.0 + 10.8.0 + 1.5 + 0.8.10 + 2.3.1 + 3.2.1 + 3.10.1 + 3.1.1 + 3.3.0 + 3.3.0 + 3.5.0 + 3.0.1 + 3.0.0 + 3.2.1 + 2.22.2 3.12.1 + 2.5.3 + 1.13.0 + 1.13.0 + 1.18.20.0 + 3.11.4 + 3.5.3 0.9.21 + 1.6.13 + true ${project.basedir}/.. @@ -163,6 +198,11 @@ artemis-jakarta-client-all ${artemis.version} + + com.atomikos + transactions-spring-boot3-starter + ${atomikos.version} + org.apache.calcite calcite-core @@ -211,7 +251,7 @@ com.apollographql.federation federation-graphql-java-support - ${federation-graphql-java-support-api} + ${federation-graphql-java-support-api.version} com.graphql-java @@ -347,17 +387,17 @@ com.google.guava guava - 31.1-jre + ${guava.version} cz.jirutka.rsql rsql-parser - 2.1.0 + ${rsql-parser.version} io.reactivex.rxjava2 rxjava - 2.2.21 + ${rxjava.version} org.springframework @@ -367,12 +407,12 @@ org.owasp.encoder encoder - 1.2.3 + ${encoder.version} org.fusesource.jansi jansi - 2.4.0 + ${jansi.version} @@ -406,12 +446,12 @@ org.apache.httpcomponents httpclient - 4.5.13 + ${httpclient.version} com.github.opendevl json2flat - 1.0.3 + ${json2flat.version} com.zaxxer @@ -423,22 +463,22 @@ org.apache.ant ant - 1.10.13 + ${ant.version} org.javassist javassist - 3.29.2-GA + ${javaassist.version} org.skyscreamer jsonassert - 1.5.1 + ${jsonassert.version} com.github.stefanbirkner system-lambda - 1.2.1 + ${system-lambda.version} ch.qos.logback @@ -541,7 +581,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + ${maven-gpg-plugin.version} sign-artifacts @@ -561,7 +601,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + ${nexus-staging-maven-plugin.version} true ossrh @@ -580,17 +620,17 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0 + ${maven-surefire-plugin.version} org.apache.maven.plugins maven-failsafe-plugin - 2.22.2 + ${maven-failsafe-plugin.version} org.apache.maven.plugins maven-checkstyle-plugin - 3.2.1 + ${maven-checkstyle-plugin.version} validate @@ -619,7 +659,7 @@ com.puppycrawl.tools checkstyle - 10.8.0 + ${checkstyle.version} compile @@ -627,12 +667,12 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.1 + ${maven-deploy-plugin.version} org.apache.maven.plugins maven-jar-plugin - 3.3.0 + ${maven-jar-plugin.version} maven-site-plugin @@ -645,7 +685,7 @@ org.codehaus.gmaven gmaven-plugin - 1.5 + ${gmaven-plugin.version} 2.0 @@ -664,7 +704,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + ${maven-compiler-plugin.version} ${source.jdk.version} ${target.jdk.version} @@ -684,7 +724,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + ${maven-source-plugin.version} attach-sources @@ -697,7 +737,7 @@ org.projectlombok lombok-maven-plugin - 1.18.20.0 + ${lombok-maven-plugin.version} ${project.basedir}/src/main/java ${project.basedir}/target/generated-sources/antlr4 @@ -716,7 +756,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + ${maven-javadoc-plugin.version} attach-javadocs @@ -735,18 +775,18 @@ org.apache.maven.plugins maven-release-plugin - 2.5.3 + ${maven-release-plugin.version} org.apache.maven.scm maven-scm-provider-gitexe - 1.13.0 + ${maven-scm-provider-gitexe.version} org.apache.maven.scm maven-scm-api - 1.13.0 + ${maven-scm-api.version} @@ -756,7 +796,7 @@ org.jacoco jacoco-maven-plugin - 0.8.10 + ${jacoco-maven-plugin.version} default-prepare-agent @@ -811,7 +851,7 @@ org.eluder.coveralls coveralls-maven-plugin - 4.3.0 + ${coveralls-maven-plugin.version} ${env.COVERALLS_REPO_TOKEN} @@ -819,14 +859,14 @@ javax.xml.bind jaxb-api - 2.3.1 + ${jaxb-api.version} com.versioneye versioneye-maven-plugin - 3.11.4 + ${versioneye-maven-plugin.version} ${env.versioneye_api_key} com.yahoo.elide @@ -838,7 +878,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.3.0 + ${maven-enforcer-plugin.version} enforce-java @@ -860,7 +900,7 @@ org.apache.maven.wagon wagon-ssh-external - 3.5.3 + ${wagon-ssh-external.version} From 7154416a18e03534797112f35d5ac434b0064ce7 Mon Sep 17 00:00:00 2001 From: Justin Tay Date: Wed, 31 May 2023 11:43:09 +0800 Subject: [PATCH 8/8] Remove synchronized blocks --- .../inmemory/HashMapStoreTransaction.java | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java index 56048c1407..b31e6d6d92 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java @@ -92,25 +92,21 @@ public void delete(Object object, RequestScope requestScope) { @Override public void commit(RequestScope scope) { - synchronized (dataStore) { - operations.stream() - .filter(op -> op.getInstance() != null) - .forEach(op -> { - Object instance = op.getInstance(); - String id = op.getId(); - Map data = dataStore.get(op.getType()); - if (op.getOpType() == Operation.OpType.DELETE) { - data.remove(id); - } else { - if (op.getOpType() == Operation.OpType.CREATE && data.get(id) != null) { - throw new TransactionException(new IllegalStateException("Duplicate key")); - } - data.put(id, instance); - } - }); - operations.clear(); - committed = true; - } + operations.stream().filter(op -> op.getInstance() != null).forEach(op -> { + Object instance = op.getInstance(); + String id = op.getId(); + Map data = dataStore.get(op.getType()); + if (op.getOpType() == Operation.OpType.DELETE) { + data.remove(id); + } else { + if (op.getOpType() == Operation.OpType.CREATE && data.get(id) != null) { + throw new TransactionException(new IllegalStateException("Duplicate key")); + } + data.put(id, instance); + } + }); + operations.clear(); + committed = true; } @Override @@ -127,10 +123,7 @@ public void createObject(Object entity, RequestScope scope) { //GeneratedValue means the DB needs to assign the ID. if (dictionary.getAttributeOrRelationAnnotation(entityClass, GeneratedValue.class, idFieldName) != null) { // TODO: Id's are not necessarily numeric. - AtomicLong nextId; - synchronized (dataStore) { - nextId = getId(entityClass); - } + AtomicLong nextId = getId(entityClass); id = String.valueOf(nextId.getAndIncrement()); setId(entity, id); } else { @@ -157,11 +150,9 @@ public DataStoreIterable getToManyRelation(DataStoreTransaction relation @Override public DataStoreIterable loadObjects(EntityProjection projection, RequestScope scope) { - synchronized (dataStore) { - Map data = dataStore.get(projection.getType()); - cacheForRollback(projection.getType(), data); - return new DataStoreIterableBuilder<>(data.values()).allInMemory().build(); - } + Map data = dataStore.get(projection.getType()); + cacheForRollback(projection.getType(), data); + return new DataStoreIterableBuilder<>(data.values()).allInMemory().build(); } @Override @@ -169,17 +160,15 @@ public Object loadObject(EntityProjection projection, Serializable id, RequestSc EntityDictionary dictionary = scope.getDictionary(); - synchronized (dataStore) { - Map data = dataStore.get(projection.getType()); - cacheForRollback(projection.getType(), data); - if (data == null) { - return null; - } - Serde serde = dictionary.getSerdeLookup().apply(id.getClass()); - - String idString = (serde == null) ? id.toString() : (String) serde.serialize(id); - return data.get(idString); + Map data = dataStore.get(projection.getType()); + cacheForRollback(projection.getType(), data); + if (data == null) { + return null; } + Serde serde = dictionary.getSerdeLookup().apply(id.getClass()); + + String idString = (serde == null) ? id.toString() : (String) serde.serialize(id); + return data.get(idString); } /**