BasePostgresDataConfig class is used to hold the common configuration for all data-postgres modules.
*
* @author Gabriel Jimenez
+ * @author Aziz Ali
*/
@Configuration
@EnableTransactionManagement
@@ -41,7 +41,6 @@ public abstract class BasePostgresDataConfig {
/**
* Used to acquire environment variables.
*/
- @Autowired
protected Environment environment;
/**
@@ -49,7 +48,7 @@ public abstract class BasePostgresDataConfig {
*
* @param environment the environment
*/
- public BasePostgresDataConfig(Environment environment) {
+ protected BasePostgresDataConfig(Environment environment) {
this.environment = environment;
}
@@ -64,7 +63,6 @@ public DataSource dataSource() {
HikariDataSource dataSource = DataSourceBuilder.create().type(HikariDataSource.class).build();
dataSource.setSchema(environment.getRequiredProperty("spring.jpa.properties.hibernate.default_schema"));
dataSource.setLeakDetectionThreshold(2000);
- dataSource.setDataSourceProperties(additionalHibernateSpringProperties());
return dataSource;
}
@@ -79,8 +77,12 @@ private Properties additionalHibernateSpringProperties() {
properties.setProperty("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
properties.setProperty("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql"));
properties.setProperty("spring.datasource.initialization-mode", environment.getRequiredProperty("spring.datasource.initialization-mode"));
+ properties.setProperty("hibernate.cache.use_second_level_cache", "true");
properties.setProperty("hibernate.cache.use_query_cache", "true");
- properties.setProperty("hibernate.cache.provider_class", "org.ehcache.hibernate.EhCacheProvider");
+ properties.setProperty("hibernate.cache.provider_class", "org.ehcache.jsr107.EhcacheCachingProvider");
+ properties.setProperty("hibernate.cache.region.factory_class", "org.hibernate.cache.jcache.internal.JCacheRegionFactory" );
+ properties.setProperty("hibernate.javax.cache.uri",
+ environment.getProperty("hibernate.javax.cache.uri", "classpath://ehcache.xml"));
return properties;
}
@@ -94,6 +96,7 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+ entityManagerFactoryBean.setJpaProperties(additionalHibernateSpringProperties());
setPackagesToScan(entityManagerFactoryBean);
return entityManagerFactoryBean;
}
@@ -101,7 +104,7 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
/**
* Set the packages to Scan property to the entityManagerFactory.
*
- * @param entityManagerFactoryBean
+ * @param entityManagerFactoryBean the entity manager factory bean
*/
protected abstract void setPackagesToScan(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean);
}
diff --git a/data/synapse-data-postgres/src/main/resources/ehcache.xml b/data/synapse-data-postgres/src/main/resources/ehcache.xml
new file mode 100644
index 000000000..049053b56
--- /dev/null
+++ b/data/synapse-data-postgres/src/main/resources/ehcache.xml
@@ -0,0 +1,18 @@
+
++ * Tests include cache put, hit, and eviction scenarios using the {@link Product} entity. + * The tests are run with the {@link DataSampleConfig} configuration and use Hibernate statistics + * to assert cache operations. + *
+ * + * @author Aziz Ali + */ +@SpringBootTest(classes = {DataSampleConfig.class}) +class BasePostgresDataConfigTest { + + @PersistenceUnit + private EntityManagerFactory emf; + + private Statistics stats; + private Product product1, product2, product3; + + @BeforeEach + void setUp() { + SessionFactoryImplementor sfi = emf.unwrap(SessionFactoryImplementor.class); + stats = sfi.getStatistics(); + stats.clear(); + stats.setStatisticsEnabled(true); + + EntityManager em = emf.createEntityManager(); + em.getTransaction() + .begin(); + product1 = new Product(); + product1.setProductName("Cached Product1"); + product2 = new Product(); + product2.setProductName("Cached Product2"); + product3 = new Product(); + product3.setProductName("Cached Product3"); + em.persist(product1); + em.persist(product2); + em.persist(product3); + em.flush(); + em.getTransaction() + .commit(); + em.close(); + } + + @Test + void testSecondLevelCachePutOnFind() { + stats.clear(); + findAllProducts(); + assertThat(stats.getSecondLevelCachePutCount()) + .as("Expected three puts on first find") + .isEqualTo(3); + } + + @Test + void testSecondLevelCacheHitOnFind() { + // Populate cache + findAllProducts(); + stats.clear(); + // Access again for hits + findAllProducts(); + assertThat(stats.getSecondLevelCacheHitCount()) + .as("Expected three hits from 2LC") + .isEqualTo(3); + assertThat(stats.getSecondLevelCachePutCount()) + .as("No new puts expected on cache hit") + .isZero(); + } + + @Test + void testSecondLevelCacheMissAfterEviction() throws InterruptedException { + // Populate cache + findAllProducts(); + // Wait for TTL eviction (3 seconds as per ehcache.xml) + Thread.sleep(5000); + stats.clear(); + findAllProducts(); + assertThat(stats.getSecondLevelCacheMissCount()) + .as("Expected three misses after TTL eviction") + .isEqualTo(3); + assertThat(stats.getSecondLevelCachePutCount()) + .as("Expected three puts to reload into 2LC after eviction") + .isEqualTo(3); + } + + private void findAllProducts() { + EntityManager em = emf.createEntityManager(); + em.getTransaction() + .begin(); + em.find(Product.class, product1.getId()); + em.find(Product.class, product2.getId()); + em.find(Product.class, product3.getId()); + em.getTransaction() + .commit(); + em.close(); + } +} \ No newline at end of file diff --git a/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/DataSampleConfig.java b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/DataSampleConfig.java new file mode 100644 index 000000000..c9ff0386f --- /dev/null +++ b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/DataSampleConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.americanexpress.synapse.data.postgres.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.env.Environment; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +/** + * Configuration class for the sample Postgres data module. + *+ * Loads properties from {@code kocho-application.properties}, enables JPA repositories, + * and configures entity scanning for the {@code io.americanexpress.synapse.data.postgres.entity} package. + *
+ * + * @author Aziz Ali + */ +@Configuration +@PropertySource("classpath:/kocho-application.properties") +@EnableJpaRepositories(basePackages = "io.americanexpress.synapse.data.postgres.entity") +@EnableAutoConfiguration +public class DataSampleConfig extends BasePostgresDataConfig { + + static final String PACKAGE_NAME = "io.americanexpress.synapse.data.postgres"; + + public DataSampleConfig(Environment environment) { + super(environment); + } + + @Override + protected void setPackagesToScan(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { + entityManagerFactoryBean.setPackagesToScan(PACKAGE_NAME + ENTITY_PACKAGE_NAME); + } +} diff --git a/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/entity/Product.java b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/entity/Product.java new file mode 100644 index 000000000..c0fe0d44e --- /dev/null +++ b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/entity/Product.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package io.americanexpress.synapse.data.postgres.entity; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Entity representing a product in the database. + *+ * This class is cacheable and uses Hibernate's second-level cache with read-write concurrency. + * Inherits common entity fields from {@link BaseEntity}. + *
+ * + * @author Aziz Ali + */ +@Entity +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Product extends BaseEntity { + + @Column(name = "product_name") + private String productName; + + public String getProductName() { + return productName; + } + + public void setProductName(String title) { + this.productName = title; + } +} \ No newline at end of file diff --git a/data/synapse-data-postgres/src/test/resources/data.sql b/data/synapse-data-postgres/src/test/resources/data.sql new file mode 100644 index 000000000..019c20ac8 --- /dev/null +++ b/data/synapse-data-postgres/src/test/resources/data.sql @@ -0,0 +1,10 @@ +SET +SCHEMA_SEARCH_PATH TO synapse; +insert into product (id, product_name, created_date_time, last_modified_date_time, created_by, + last_modified_by, version) +values (1, 'Synapse', now(), + now(), 'John-Appleseed@email.com', 'John-Appleseed@email.com', 0); +insert into product (id, product_name, created_date_time, last_modified_date_time, created_by, + last_modified_by, version) +values (2, 'Revenge of Synapse', now(), + now(), 'John-Appleseed@email.com', 'John-Appleseed@email.com', 0); diff --git a/data/synapse-data-postgres/src/test/resources/ehcache.xml b/data/synapse-data-postgres/src/test/resources/ehcache.xml new file mode 100644 index 000000000..11332720a --- /dev/null +++ b/data/synapse-data-postgres/src/test/resources/ehcache.xml @@ -0,0 +1,19 @@ + +