[BAEL-4120] Read-only transactions samples (#11841)
* [BAEL-4120] Read-only transactions samples * Extract query runner jobs logic * Apply test name standards * Fix code standards * Use pom properties to define versions * Rename main package * Rename generics * Code clean up * Code clean up on lambdas * Add debug logs * Fix format issues * Rename entity
This commit is contained in:
parent
973c276d7b
commit
ac5dbbc5da
|
@ -52,6 +52,7 @@
|
|||
<module>persistence-libraries</module>
|
||||
<module>querydsl</module>
|
||||
<module>r2dbc</module>
|
||||
<module>read-only-transactions</module>
|
||||
<module>redis</module>
|
||||
<!-- <module>sirix</module> --> <!-- We haven't upgraded to java 11. Fixing in BAEL-10841 -->
|
||||
<module>solr</module>
|
||||
|
|
|
@ -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`
|
|
@ -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();
|
||||
|
||||
|
|
@ -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/
|
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>read-only-transactions</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>read-only-transactions</name>
|
||||
|
||||
<parent>
|
||||
<groupId>com.baeldung</groupId>
|
||||
<artifactId>persistence-modules</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>${spring-test.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
<version>${hibernate.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>${hikari.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit-jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>${h2.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
<properties>
|
||||
<mysql.version>8.0.21</mysql.version>
|
||||
<hikari.version>4.0.3</hikari.version>
|
||||
<hibernate.version>5.6.1.Final</hibernate.version>
|
||||
<spring-boot.version>2.6.1</spring-boot.version>
|
||||
<spring-test.version>5.3.13</spring-test.version>
|
||||
<junit-jupiter.version>5.8.2</junit-jupiter.version>
|
||||
<h2.version>1.4.200</h2.version>
|
||||
</properties>
|
||||
</project>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<AtomicLong> 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.readonlytransactions.mysql.spring;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class ReadOnlyContext {
|
||||
|
||||
private static final ThreadLocal<AtomicInteger> 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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<Object, Object> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BookEntity, Long> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
|
||||
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
|
||||
version="2.1">
|
||||
|
||||
<persistence-unit name="jpa-unit" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
|
||||
<class>com.baeldung.readonlytransactions.mysql.entities.Book</class>
|
||||
<exclude-unlisted-classes>true</exclude-unlisted-classes>
|
||||
<properties>
|
||||
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
|
||||
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8"/>
|
||||
<property name="javax.persistence.jdbc.user" value="baeldung"/>
|
||||
<property name="javax.persistence.jdbc.password" value="baeldung"/>
|
||||
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
|
||||
<property name="hibernate.hbm2ddl.auto" value="validate"/>
|
||||
<property name="hibernate.show_sql" value="false"/>
|
||||
<property name="hibernate.temp.use_jdbc_metadata_defaults" value="true"/>
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT"
|
||||
class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -
|
||||
%msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||
<logger name="org.hibernate.engine.transaction.internal.TransactionImpl" level="DEBUG"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
|
@ -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<GenericApplicationContext> {
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<GenericApplicationContext> {
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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<GenericApplicationContext> {
|
||||
@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<String, Supplier<Long>> 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<String> 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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue