diff --git a/data/synapse-data-postgres/pom.xml b/data/synapse-data-postgres/pom.xml index 344abd0f9..3eacf963e 100644 --- a/data/synapse-data-postgres/pom.xml +++ b/data/synapse-data-postgres/pom.xml @@ -56,16 +56,22 @@ org.ehcache ehcache + jakarta - org.hibernate - hibernate-ehcache + org.hibernate.orm + hibernate-jcache jakarta.persistence jakarta.persistence-api + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/data/synapse-data-postgres/src/main/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfig.java b/data/synapse-data-postgres/src/main/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfig.java index 0dfaedb16..f1596a9ff 100644 --- a/data/synapse-data-postgres/src/main/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfig.java +++ b/data/synapse-data-postgres/src/main/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfig.java @@ -14,7 +14,6 @@ package io.americanexpress.synapse.data.postgres.config; import com.zaxxer.hikari.HikariDataSource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; @@ -31,6 +30,7 @@ * 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 @@ + + + + + + + + 10 + + 1000 + + diff --git a/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfigTest.java b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfigTest.java new file mode 100644 index 000000000..3fd143512 --- /dev/null +++ b/data/synapse-data-postgres/src/test/java/io/americanexpress/synapse/data/postgres/config/BasePostgresDataConfigTest.java @@ -0,0 +1,122 @@ +/* + * 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 io.americanexpress.synapse.data.postgres.entity.Product; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnit; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.Statistics; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Integration tests for verifying the second-level cache behavior in the Postgres data module. + *

+ * 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 @@ + + + + + + + + + 3 + + 10 + + \ No newline at end of file diff --git a/data/synapse-data-postgres/src/test/resources/kocho-application.properties b/data/synapse-data-postgres/src/test/resources/kocho-application.properties new file mode 100644 index 000000000..5e184d273 --- /dev/null +++ b/data/synapse-data-postgres/src/test/resources/kocho-application.properties @@ -0,0 +1,20 @@ +#Datasource in memory database H2 connection +spring.datasource.jdbcUrl=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;MODE=LEGACY +spring.datasource.username=sa +spring.datasource.password=sa +spring.datasource.driver-class-name=org.h2.Driver +hibernate.dialect=org.hibernate.dialect.H2Dialect +#H2 default for unit test and local +spring.jpa.properties.hibernate.default_schema=PUBLIC +#Connection pool +spring.datasource.connectionTimeout=15000 +spring.datasource.idleTimeout=300000 +spring.datasource.maxLifetime=900000 +spring.datasource.maximumPoolSize=3 + +spring.datasource.initialization-mode=always + +hibernate.hbm2ddl.auto=none +hibernate.show_sql=true +spring.profiles.active=test +hibernate.format_sql=true \ No newline at end of file diff --git a/data/synapse-data-postgres/src/test/resources/schema.sql b/data/synapse-data-postgres/src/test/resources/schema.sql new file mode 100644 index 000000000..dc9ebe6bc --- /dev/null +++ b/data/synapse-data-postgres/src/test/resources/schema.sql @@ -0,0 +1,23 @@ +DROP SCHEMA IF EXISTS synapse CASCADE; +CREATE SCHEMA synapse; + +SET +SCHEMA_SEARCH_PATH TO synapse; + +DROP TABLE IF EXISTS product CASCADE; + +/* + * product table create script + */ +create table product +( + id serial PRIMARY KEY NOT NULL, + product_name VARCHAR(150) NOT NULL, + created_by VARCHAR(100), + last_modified_by VARCHAR(100), + created_date_time TIMESTAMP DEFAULT current_timestamp, + last_modified_date_time TIMESTAMP, + version INTEGER NOT NULL +); + +COMMIT; diff --git a/pom.xml b/pom.xml index 733b1a7b2..166eb1b9b 100644 --- a/pom.xml +++ b/pom.xml @@ -641,12 +641,10 @@ ehcache 3.10.8 - - - org.hibernate - hibernate-ehcache - 5.6.15.Final + org.hibernate.orm + hibernate-jcache + 6.6.17.Final org.hibernate