diff --git a/persistence-modules/pom.xml b/persistence-modules/pom.xml index 7be71ad215..23d29de1e8 100644 --- a/persistence-modules/pom.xml +++ b/persistence-modules/pom.xml @@ -52,6 +52,7 @@ persistence-libraries querydsl r2dbc + read-only-transactions redis solr diff --git a/persistence-modules/read-only-transactions/README.md b/persistence-modules/read-only-transactions/README.md new file mode 100644 index 0000000000..90a86e551e --- /dev/null +++ b/persistence-modules/read-only-transactions/README.md @@ -0,0 +1,8 @@ +### Relevant Articles: +- + +### Instructions +To run the `com.baeldung.read_only_transactions.TransactionSetupIntegrationTest` first follow the steps described next: +- run the command `docker-compose -f docker-compose-mysql.yml up` +- Open a SQL client of your preference and execute the `create.sql` script. +- You can check the mysql logs using `tail -f mysql/${name of de log file created}.log` \ No newline at end of file diff --git a/persistence-modules/read-only-transactions/create.sql b/persistence-modules/read-only-transactions/create.sql new file mode 100644 index 0000000000..0ba60be150 --- /dev/null +++ b/persistence-modules/read-only-transactions/create.sql @@ -0,0 +1,43 @@ +create table book ( + id bigint(20) AUTO_INCREMENT primary key, + name varchar(255) not null, + uuid varchar(40) +); + + +DELIMITER ;; +DROP PROCEDURE IF EXISTS populate; +create procedure populate() +BEGIN + SET @name1='Josh purchase'; + SET @name2='Henry purchase'; + SET @name3='Betty purchase'; + SET @name4='Kate purchase'; + SET @name5='Mari purchase'; + SET @name=''; + SET @counter=0; + + START TRANSACTION; + + while @counter < 1000000 do + SET @name = case + when MOD(@counter, 5) = 0 THEN @name5 + when MOD(@counter, 3) = 0 THEN @name3 + when MOD(@counter, 4) = 0 THEN @name4 + when MOD(@counter, 2) = 0 THEN @name2 + else @name1 + end; + + insert into book(name, uuid) values(@name, uuid()); + SET @counter=@counter+1; + end while; + + COMMIT; + +END;; + +DELIMITER ; + +CALL populate(); + + diff --git a/persistence-modules/read-only-transactions/docker-compose-mysql.yml b/persistence-modules/read-only-transactions/docker-compose-mysql.yml new file mode 100644 index 0000000000..a922fabbd8 --- /dev/null +++ b/persistence-modules/read-only-transactions/docker-compose-mysql.yml @@ -0,0 +1,15 @@ +version: "3.9" # optional since v1.27.0 +services: + mysql: + build: . + restart: always + ports: + - "3306:3306" + environment: + MYSQL_PASSWORD: "baeldung" + MYSQL_USER: "baeldung" + MYSQL_DATABASE: "baeldung" + MYSQL_ROOT_PASSWORD: "baeldung" + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --innodb_buffer_pool_size=3G --innodb_adaptive_hash_index=off --query_cache_size=0 --query_cache_type=0 --log_output=FILE --general_log=1 + volumes: + - ./mysql:/var/lib/mysql/ \ No newline at end of file diff --git a/persistence-modules/read-only-transactions/pom.xml b/persistence-modules/read-only-transactions/pom.xml new file mode 100644 index 0000000000..d3822f8e58 --- /dev/null +++ b/persistence-modules/read-only-transactions/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + read-only-transactions + 0.0.1-SNAPSHOT + read-only-transactions + + + com.baeldung + persistence-modules + 1.0.0-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + com.zaxxer + HikariCP + + + + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + + org.springframework + spring-test + ${spring-test.version} + test + + + + mysql + mysql-connector-java + ${mysql.version} + + + + org.hibernate + hibernate-core + ${hibernate.version} + + + + com.zaxxer + HikariCP + ${hikari.version} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + + com.h2database + h2 + ${h2.version} + runtime + + + + + + + 8.0.21 + 4.0.3 + 5.6.1.Final + 2.6.1 + 5.3.13 + 5.8.2 + 1.4.200 + + \ No newline at end of file diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Book.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Book.java new file mode 100644 index 0000000000..000859a201 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Book.java @@ -0,0 +1,44 @@ +package com.baeldung.readonlytransactions.h2; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "book") +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String uuid; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/BookService.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/BookService.java new file mode 100644 index 0000000000..0ecbc4a0d4 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/BookService.java @@ -0,0 +1,24 @@ +package com.baeldung.readonlytransactions.h2; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManagerFactory; + +@Service +public class BookService { + + private EntityManagerFactory entityManagerFactory; + + public BookService(@Autowired @Qualifier("h2EntityManagerFactory") EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + @Transactional(readOnly = true) + public Book getBookById(long id) { + return entityManagerFactory.createEntityManager() + .find(Book.class, id); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Config.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Config.java new file mode 100644 index 0000000000..241dad417b --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/Config.java @@ -0,0 +1,54 @@ +package com.baeldung.readonlytransactions.h2; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.util.Properties; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +@Configuration +public class Config { + + @Bean("h2DataSource") + public DataSource dataSource() { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:h2:mem:mydb"); + config.setUsername("sa"); + config.setPassword(""); + config.setDriverClassName("org.h2.Driver"); + return new HikariDataSource(config); + } + + @Bean("h2EntityManagerFactory") + public EntityManagerFactory entityManagerFactory(@Qualifier("h2DataSource") DataSource dataSource) { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(false); + + LocalContainerEntityManagerFactoryBean managerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + managerFactoryBean.setJpaVendorAdapter(vendorAdapter); + managerFactoryBean.setPackagesToScan(Config.class.getPackage() + .getName()); + managerFactoryBean.setDataSource(dataSource); + + Properties properties = new Properties(); + + properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); + properties.setProperty("hibernate.hbm2ddl.auto", "create-drop"); + + properties.setProperty("hibernate.show_sql", "true"); + properties.setProperty("hibernate.format_sql", "true"); + + managerFactoryBean.setJpaProperties(properties); + managerFactoryBean.afterPropertiesSet(); + + return managerFactoryBean.getObject(); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/TransactionConfig.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/TransactionConfig.java new file mode 100644 index 0000000000..62abb85edd --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/h2/TransactionConfig.java @@ -0,0 +1,21 @@ +package com.baeldung.readonlytransactions.h2; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.persistence.EntityManagerFactory; + +@Configuration +@EnableTransactionManagement +public class TransactionConfig { + + @Bean + public PlatformTransactionManager transactionManager(@Qualifier("h2EntityManagerFactory") EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/BaseRepo.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/BaseRepo.java new file mode 100644 index 0000000000..81ff78efb2 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/BaseRepo.java @@ -0,0 +1,29 @@ +package com.baeldung.readonlytransactions.mysql.dao; + +import com.baeldung.readonlytransactions.utils.ExecutorUtils; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +public abstract class BaseRepo { + + protected long execQuery(Consumer function) { + AtomicLong count = new AtomicLong(0); + + ExecutorService executor = ExecutorUtils.createExecutor(10, 10); + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + scheduler.schedule(executor::shutdownNow, 5L, TimeUnit.SECONDS); + scheduler.shutdown(); + + while (!executor.isShutdown()) { + executor.execute(() -> function.accept(count)); + } + + return count.get(); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJPA.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJPA.java new file mode 100644 index 0000000000..727e88219f --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJPA.java @@ -0,0 +1,36 @@ +package com.baeldung.readonlytransactions.mysql.dao; + +import org.hibernate.Session; + +import com.baeldung.readonlytransactions.mysql.entities.Book; + +import java.util.SplittableRandom; +import java.util.concurrent.atomic.AtomicLong; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +public class MyRepoJPA extends BaseRepo { + + private EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-unit"); + private SplittableRandom random = new SplittableRandom(); + + public long runQuery() { + return execQuery(this::runSql); + } + + private void runSql(AtomicLong count) { + if (Thread.interrupted()) { + return; + } + + EntityManager entityManager = entityManagerFactory.createEntityManager(); + Session session = entityManager.unwrap(Session.class); + session.setDefaultReadOnly(true); + entityManager.find(Book.class, 1L + random.nextLong(0, 1000000)); + count.incrementAndGet(); + entityManager.clear(); + } + +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJdbc.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJdbc.java new file mode 100644 index 0000000000..306bfd5f0c --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoJdbc.java @@ -0,0 +1,71 @@ +package com.baeldung.readonlytransactions.mysql.dao; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.SplittableRandom; +import java.util.concurrent.atomic.AtomicLong; + +public class MyRepoJdbc extends BaseRepo { + + static { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private HikariDataSource ds; + private SplittableRandom random = new SplittableRandom(); + + public MyRepoJdbc(boolean readOnly, boolean autocommit) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8"); + config.setUsername("baeldung"); + config.setPassword("baeldung"); + config.setReadOnly(readOnly); + config.setAutoCommit(autocommit); + ds = new HikariDataSource(config); + } + + private Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + public long runQuery(Boolean autoCommit, Boolean readOnly) { + try { + return execQuery(count -> runSql(count, autoCommit, readOnly)); + } finally { + ds.close(); + } + } + + private void runSql(AtomicLong count, Boolean autoCommit, Boolean readOnly) { + if (Thread.interrupted()) { + return; + } + + try (Connection connect = getConnection(); PreparedStatement statement = connect.prepareStatement("select * from transactions where id = ?")) { + if (autoCommit != null) + connect.setAutoCommit(autoCommit); + + if (readOnly != null) + connect.setReadOnly(readOnly); + + statement.setLong(1, 1L + random.nextLong(0, 100000)); + ResultSet resultSet = statement.executeQuery(); + + if (autoCommit != null && !autoCommit) + connect.commit(); + + count.incrementAndGet(); + resultSet.close(); + } catch (Exception ignored) { + } + } +} \ No newline at end of file diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoSpring.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoSpring.java new file mode 100644 index 0000000000..387f5ec722 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/dao/MyRepoSpring.java @@ -0,0 +1,22 @@ +package com.baeldung.readonlytransactions.mysql.dao; + +import com.baeldung.readonlytransactions.mysql.spring.repositories.BookRepository; + +import java.util.SplittableRandom; + +public class MyRepoSpring extends BaseRepo { + + private SplittableRandom random = new SplittableRandom(); + private BookRepository repository; + + public MyRepoSpring(BookRepository repository) { + this.repository = repository; + } + + public long runQuery() { + return execQuery(count -> { + repository.get(1L + random.nextLong(0, 1000000)); + count.incrementAndGet(); + }); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/entities/Book.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/entities/Book.java new file mode 100644 index 0000000000..405cc9c2c2 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/entities/Book.java @@ -0,0 +1,44 @@ +package com.baeldung.readonlytransactions.mysql.entities; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "book") +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String uuid; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/Config.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/Config.java new file mode 100644 index 0000000000..abaa63b197 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/Config.java @@ -0,0 +1,77 @@ +package com.baeldung.readonlytransactions.mysql.spring; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.baeldung.readonlytransactions.mysql.dao.MyRepoSpring; +import com.baeldung.readonlytransactions.mysql.spring.entities.BookEntity; +import com.baeldung.readonlytransactions.mysql.spring.repositories.BookRepository; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.util.Properties; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +@Configuration +@EnableJpaRepositories(basePackageClasses = Config.class, enableDefaultTransactions = false) +@EnableTransactionManagement +@EnableAspectJAutoProxy +public class Config { + + @Bean + public MyRepoSpring repoSpring(BookRepository repository) { + return new MyRepoSpring(repository); + } + + private DataSource dataSource(boolean readOnly, boolean isAutoCommit) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8"); + config.setUsername("baeldung"); + config.setPassword("baeldung"); + config.setReadOnly(readOnly); + config.setAutoCommit(isAutoCommit); + return new HikariDataSource(config); + } + + @Bean + public DataSource dataSource() { + return new RoutingDS(dataSource(false, false), dataSource(true, true)); + } + + @Bean + public EntityManagerFactory entityManagerFactory(DataSource dataSource) { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setGenerateDdl(false); + + LocalContainerEntityManagerFactoryBean managerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + managerFactoryBean.setJpaVendorAdapter(vendorAdapter); + managerFactoryBean.setPackagesToScan(BookEntity.class.getPackage() + .getName()); + managerFactoryBean.setDataSource(dataSource); + + Properties properties = new Properties(); + + properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect"); + properties.setProperty("hibernate.hbm2ddl.auto", "validate"); + + managerFactoryBean.setJpaProperties(properties); + managerFactoryBean.afterPropertiesSet(); + + return managerFactoryBean.getObject(); + } + + @Bean + public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } + +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyContext.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyContext.java new file mode 100644 index 0000000000..2933c8c4a9 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyContext.java @@ -0,0 +1,26 @@ +package com.baeldung.readonlytransactions.mysql.spring; + +import java.util.concurrent.atomic.AtomicInteger; + +public class ReadOnlyContext { + + private static final ThreadLocal READ_ONLY_LEVEL = ThreadLocal.withInitial(() -> new AtomicInteger(0)); + + private ReadOnlyContext() { + } + + public static boolean isReadOnly() { + return READ_ONLY_LEVEL.get() + .get() > 0; + } + + public static void enter() { + READ_ONLY_LEVEL.get() + .incrementAndGet(); + } + + public static void exit() { + READ_ONLY_LEVEL.get() + .decrementAndGet(); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyInterception.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyInterception.java new file mode 100644 index 0000000000..30488970a8 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReadOnlyInterception.java @@ -0,0 +1,29 @@ +package com.baeldung.readonlytransactions.mysql.spring; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ReadOnlyInterception { + + private static final Logger logger = LoggerFactory.getLogger(ReadOnlyInterception.class); + + @Around("@annotation(com.baeldung.readonlytransactions.mysql.spring.ReaderDS)") + public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { + try { + ReadOnlyContext.enter(); + //Debug data source switch + logger.debug("-----------------------------Entering read only zone-----------------------------"); + return joinPoint.proceed(); + } finally { + logger.debug("-----------------------------Leaving read only zone------------------------------"); + ReadOnlyContext.exit(); + } + } + +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReaderDS.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReaderDS.java new file mode 100644 index 0000000000..312c65b3cf --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/ReaderDS.java @@ -0,0 +1,10 @@ +package com.baeldung.readonlytransactions.mysql.spring; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface ReaderDS { +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/RoutingDS.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/RoutingDS.java new file mode 100644 index 0000000000..36ce8a16e6 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/RoutingDS.java @@ -0,0 +1,35 @@ +package com.baeldung.readonlytransactions.mysql.spring; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +public class RoutingDS extends AbstractRoutingDataSource { + + private static final Logger logger = LoggerFactory.getLogger(RoutingDS.class); + + RoutingDS(DataSource writer, DataSource reader) { + + Map dataSources = new HashMap<>(); + dataSources.put("writer", writer); + dataSources.put("reader", reader); + + setTargetDataSources(dataSources); + } + + @Override + protected Object determineCurrentLookupKey() { + String dataSourceMode = ReadOnlyContext.isReadOnly() ? "reader" : "writer"; + + // Testing data source switch + logger.debug("-----------------------------Datasource: {} ---------------------------------", dataSourceMode); + + return dataSourceMode; + } + +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/entities/BookEntity.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/entities/BookEntity.java new file mode 100644 index 0000000000..87c8988a8d --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/entities/BookEntity.java @@ -0,0 +1,45 @@ +package com.baeldung.readonlytransactions.mysql.spring.entities; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "book") +public class BookEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String uuid; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } +} + diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/repositories/BookRepository.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/repositories/BookRepository.java new file mode 100644 index 0000000000..2cc61c016d --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/mysql/spring/repositories/BookRepository.java @@ -0,0 +1,21 @@ +package com.baeldung.readonlytransactions.mysql.spring.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.baeldung.readonlytransactions.mysql.spring.ReaderDS; +import com.baeldung.readonlytransactions.mysql.spring.entities.BookEntity; + +import javax.transaction.Transactional; + +public interface BookRepository extends JpaRepository { + + @ReaderDS + @Query("Select 1 from BookEntity t where t.id = ?1") + Long get(Long id); + + @Transactional + default BookEntity persist(BookEntity book) { + return this.save(book); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/utils/ExecutorUtils.java b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/utils/ExecutorUtils.java new file mode 100644 index 0000000000..60c306b3b8 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/java/com/baeldung/readonlytransactions/utils/ExecutorUtils.java @@ -0,0 +1,16 @@ +package com.baeldung.readonlytransactions.utils; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ExecutorUtils { + + private ExecutorUtils() { + } + + public static ExecutorService createExecutor(int corePoolSize, int maximumPoolSize) { + return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.DiscardOldestPolicy()); + } +} diff --git a/persistence-modules/read-only-transactions/src/main/resources/META-INF/persistence.xml b/persistence-modules/read-only-transactions/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..37fb45ed49 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,23 @@ + + + + + org.hibernate.jpa.HibernatePersistenceProvider + com.baeldung.readonlytransactions.mysql.entities.Book + true + + + + + + + + + + + + diff --git a/persistence-modules/read-only-transactions/src/main/resources/logback.xml b/persistence-modules/read-only-transactions/src/main/resources/logback.xml new file mode 100644 index 0000000000..5d331c64b3 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - + %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/JPATransactionIntegrationTest.java b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/JPATransactionIntegrationTest.java new file mode 100644 index 0000000000..a15651b273 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/JPATransactionIntegrationTest.java @@ -0,0 +1,77 @@ +package com.baeldung.readonlytransactions; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.hibernate.Session; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import com.baeldung.readonlytransactions.h2.Config; +import com.baeldung.readonlytransactions.h2.Book; +import com.baeldung.readonlytransactions.mysql.spring.ReadOnlyInterception; + +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, initializers = JPATransactionIntegrationTest.TestConfig.class, classes = { ReadOnlyInterception.class }) +class JPATransactionIntegrationTest { + + static class TestConfig implements ApplicationContextInitializer { + @Override + public void initialize(GenericApplicationContext applicationContext) { + new AnnotatedBeanDefinitionReader(applicationContext).register(Config.class); + } + } + + @Autowired + @Qualifier("h2EntityManagerFactory") + private EntityManagerFactory entityManagerFactory; + + @BeforeEach + void setUp() { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + entityManager.getTransaction() + .begin(); + entityManager.createQuery("DELETE FROM Book") + .executeUpdate(); + + Book book = new Book(); + book.setName("Test 1"); + book.setUuid(UUID.randomUUID() + .toString()); + + entityManager.merge(book); + entityManager.getTransaction() + .commit(); + } + + @Test + void givenAEntityManagerDefinedAsReadOnly_whenCreatingATransaction_thenAReadOnlyTransactionShouldBeCreated() { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + + entityManager.unwrap(Session.class) + .setDefaultReadOnly(true); + entityManager.getTransaction() + .begin(); + Book book = entityManager.find(Book.class, 1L); + entityManager.getTransaction() + .commit(); + entityManager.unwrap(Session.class) + .setDefaultReadOnly(false); + + assertNotNull(book); + } + +} diff --git a/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/SpringTransactionReadOnlyIntegrationTest.java b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/SpringTransactionReadOnlyIntegrationTest.java new file mode 100644 index 0000000000..2f5fd66140 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/SpringTransactionReadOnlyIntegrationTest.java @@ -0,0 +1,72 @@ +package com.baeldung.readonlytransactions; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import com.baeldung.readonlytransactions.h2.Config; +import com.baeldung.readonlytransactions.h2.Book; +import com.baeldung.readonlytransactions.h2.TransactionConfig; +import com.baeldung.readonlytransactions.h2.BookService; + +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, initializers = SpringTransactionReadOnlyIntegrationTest.TestConfig.class, classes = { BookService.class }) +class SpringTransactionReadOnlyIntegrationTest { + + static class TestConfig implements ApplicationContextInitializer { + @Override + public void initialize(GenericApplicationContext applicationContext) { + AnnotatedBeanDefinitionReader beanDefinitionReader = new AnnotatedBeanDefinitionReader(applicationContext); + + beanDefinitionReader.register(Config.class); + beanDefinitionReader.register(TransactionConfig.class); + } + } + + @Autowired + @Qualifier("h2EntityManagerFactory") + private EntityManagerFactory entityManagerFactory; + + @Autowired + private BookService service; + + @BeforeEach + void setUp() { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + entityManager.getTransaction() + .begin(); + entityManager.createQuery("DELETE FROM Book") + .executeUpdate(); + + Book book = new Book(); + book.setName("Test 1"); + book.setUuid(UUID.randomUUID() + .toString()); + + entityManager.merge(book); + entityManager.getTransaction() + .commit(); + } + + @Test + void givenThatSpringTransactionManagementIsEnabled_whenAMethodIsAnnotatedAsTransactionalReadOnly_thenSpringShouldTakeCareOfTheTransaction() { + Book book = service.getBookById(1L); + + assertNotNull(book); + } +} diff --git a/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/TransactionSetupIntegrationTest.java b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/TransactionSetupIntegrationTest.java new file mode 100644 index 0000000000..9cbc22afe3 --- /dev/null +++ b/persistence-modules/read-only-transactions/src/test/java/com/baeldung/readonlytransactions/TransactionSetupIntegrationTest.java @@ -0,0 +1,106 @@ +package com.baeldung.readonlytransactions; + +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.support.AnnotationConfigContextLoader; + +import com.baeldung.readonlytransactions.mysql.dao.MyRepoJPA; +import com.baeldung.readonlytransactions.mysql.dao.MyRepoJdbc; +import com.baeldung.readonlytransactions.mysql.dao.MyRepoSpring; +import com.baeldung.readonlytransactions.mysql.spring.Config; +import com.baeldung.readonlytransactions.mysql.spring.ReadOnlyInterception; +import com.baeldung.readonlytransactions.mysql.spring.entities.BookEntity; +import com.baeldung.readonlytransactions.mysql.spring.repositories.BookRepository; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Stream; + +// Needs to be run with Docker look at the readme file. +@Disabled +@ExtendWith(SpringExtension.class) +@ContextConfiguration(loader = AnnotationConfigContextLoader.class, initializers = TransactionSetupIntegrationTest.TestConfig.class, classes = { ReadOnlyInterception.class }) +class TransactionSetupIntegrationTest { + + static class TestConfig implements ApplicationContextInitializer { + @Override + public void initialize(GenericApplicationContext applicationContext) { + new AnnotatedBeanDefinitionReader(applicationContext).register(Config.class); + } + } + + private static final Logger logger = LoggerFactory.getLogger(TransactionSetupIntegrationTest.class); + + @Autowired + private MyRepoSpring repoSpring; + + @Autowired + private BookRepository repository; + + @Test + void givenTheDifferentTransactionSetup_whenRunningAThroughputTest_thenWeCanObserveTheSystem() { + Map> jdbcConfigurations = new LinkedHashMap<>(); + + jdbcConfigurations.put("JPA: Session read only true and autocommit disabled", () -> new MyRepoJPA().runQuery()); + + jdbcConfigurations.put("Spring: Session read only and autocommit true", () -> repoSpring.runQuery()); + + jdbcConfigurations.put("JDBC: Global read only and autocommit enabled", () -> new MyRepoJdbc(true, true).runQuery(null, null)); + jdbcConfigurations.put("JDBC: Global read only false and autocommit enabled", () -> new MyRepoJdbc(false, true).runQuery(null, null)); + jdbcConfigurations.put("JDBC: Global read only true and autocommit disabled", () -> new MyRepoJdbc(true, false).runQuery(null, null)); + + jdbcConfigurations.put("JDBC: Session read only and autocommit disabled", () -> new MyRepoJdbc(false, false).runQuery(false, false)); + jdbcConfigurations.put("JDBC: Session read only and autocommit enabled", () -> new MyRepoJdbc(false, false).runQuery(true, true)); + jdbcConfigurations.put("JDBC: Session read only false and autocommit enabled", () -> new MyRepoJdbc(false, false).runQuery(false, true)); + jdbcConfigurations.put("JDBC: Session read only true and autocommit disabled", () -> new MyRepoJdbc(false, false).runQuery(true, false)); + + jdbcConfigurations.entrySet() + .stream() + .flatMap(entry -> { + Stream.Builder builder = Stream.builder(); + return builder.add(entry.getKey() + " Total: " + entry.getValue() + .get()) + .add(entry.getKey() + " Total: " + entry.getValue() + .get()) + .add(entry.getKey() + " Total: " + entry.getValue() + .get()) + .build(); + }) + .collect(toList()) + .stream() + .peek(o -> logger.info("--------------------------------------------------")) + .forEach(logger::info); + } + + @Test + void givenThatSpringTransactionManagementIsEnabled_whenAMethodIsAnnotatedAsTransactionalReadOnly_thenSpringShouldTakeCareOfTheTransaction() { + Long id = repository.get(2L); + + assertNotNull(id); + } + + @Test + void givenThatSpringTransactionManagementIsEnabled_whenAMethodIsAnnotatedAsTransactional_thenSpringShouldTakeCareOfTheTransaction() { + BookEntity book = new BookEntity(); + book.setName("Persistence test"); + book.setUuid(UUID.randomUUID() + .toString()); + book = repository.persist(book); + + assertNotNull(book.getId()); + } +} \ No newline at end of file