diff --git a/persistence-modules/spring-jdbc/pom.xml b/persistence-modules/spring-jdbc/pom.xml index 28a858dd43..08e43e8292 100644 --- a/persistence-modules/spring-jdbc/pom.xml +++ b/persistence-modules/spring-jdbc/pom.xml @@ -31,6 +31,11 @@ mysql-connector-java runtime + + org.postgresql + postgresql + runtime + \ No newline at end of file diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/SpringJdbcBatchPerformanceApplication.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/SpringJdbcBatchPerformanceApplication.java new file mode 100644 index 0000000000..d523717118 --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/SpringJdbcBatchPerformanceApplication.java @@ -0,0 +1,48 @@ +package com.baeldung.spring.jdbc.batch; + +import com.baeldung.spring.jdbc.batch.service.ProductService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +import java.util.Collections; + +@SpringBootApplication +@ComponentScan(basePackages = "com.baeldung.spring.jdbc.batch") +public class SpringJdbcBatchPerformanceApplication implements CommandLineRunner { + + @Autowired + @Qualifier("batchProductService") + private ProductService batchProductService; + @Autowired + @Qualifier("simpleProductService") + private ProductService simpleProductService; + + public static void main(String[] args) { + SpringApplication.run(SpringJdbcBatchPerformanceApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + int[] recordCounts = { 1, 10, 100, 1000, 10_000, 100_000, 1000_000 }; + + for (int recordCount : recordCounts) { + long regularElapsedTime = simpleProductService.createProducts(recordCount); + long batchElapsedTime = batchProductService.createProducts(recordCount); + + System.out.println(String.join("", Collections.nCopies(50, "-"))); + System.out.format("%-20s%-5s%-10s%-5s%8sms\n", "Regular inserts", "|", recordCount, "|", regularElapsedTime); + System.out.format("%-20s%-5s%-10s%-5s%8sms\n", "Batch inserts", "|", recordCount, "|", batchElapsedTime); + System.out.printf("Total gain: %d %s\n", calculateGainInPercent(regularElapsedTime, batchElapsedTime), "%"); + } + + } + + int calculateGainInPercent(long before, long after) { + return (int) Math.floor(100D * (before - after) / before); + } +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/config/AppConfig.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/config/AppConfig.java new file mode 100644 index 0000000000..1dd7c63adc --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/config/AppConfig.java @@ -0,0 +1,27 @@ +package com.baeldung.spring.jdbc.batch.config; + +import com.baeldung.spring.jdbc.batch.repo.BatchProductRepository; +import com.baeldung.spring.jdbc.batch.repo.SimpleProductRepository; +import com.baeldung.spring.jdbc.batch.service.ProductService; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import java.time.Clock; +import java.util.Random; + +@Configuration +@PropertySource("classpath:com/baeldung/spring/jdbc/batch/application.properties") +public class AppConfig { + + @Bean + public ProductService simpleProductService(SimpleProductRepository simpleProductRepository) { + return new ProductService(simpleProductRepository, new Random(), Clock.systemUTC()); + } + + @Bean + public ProductService batchProductService(BatchProductRepository batchProductRepository) { + return new ProductService(batchProductRepository, new Random(), Clock.systemUTC()); + } +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/model/Product.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/model/Product.java new file mode 100644 index 0000000000..6454952fdc --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/model/Product.java @@ -0,0 +1,54 @@ +package com.baeldung.spring.jdbc.batch.model; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class Product { + private long id; + private String title; + private LocalDateTime createdTs; + private BigDecimal price; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDateTime getCreatedTs() { + return createdTs; + } + + public void setCreatedTs(LocalDateTime createdTs) { + this.createdTs = createdTs; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Product{"); + sb.append("id=").append(id); + sb.append(", title='").append(title).append('\''); + sb.append(", createdTs=").append(createdTs); + sb.append(", price=").append(price); + sb.append('}'); + return sb.toString(); + } +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/BatchProductRepository.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/BatchProductRepository.java new file mode 100644 index 0000000000..9b4f0208ab --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/BatchProductRepository.java @@ -0,0 +1,34 @@ +package com.baeldung.spring.jdbc.batch.repo; + +import com.baeldung.spring.jdbc.batch.model.Product; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.util.List; + +@Repository +public class BatchProductRepository implements ProductRepository { + + private final JdbcTemplate jdbcTemplate; + + public BatchProductRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + @Transactional + public void saveAll(List products) { + jdbcTemplate.batchUpdate("INSERT INTO PRODUCT (TITLE, CREATED_TS, PRICE) VALUES (?, ?, ?)", + products, + 100, + (PreparedStatement ps, Product product) -> { + ps.setString(1, product.getTitle()); + ps.setTimestamp(2, Timestamp.valueOf(product.getCreatedTs())); + ps.setBigDecimal(3, product.getPrice()); + }); + } +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/ProductRepository.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/ProductRepository.java new file mode 100644 index 0000000000..ed193f87dd --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/ProductRepository.java @@ -0,0 +1,9 @@ +package com.baeldung.spring.jdbc.batch.repo; + +import com.baeldung.spring.jdbc.batch.model.Product; + +import java.util.List; + +public interface ProductRepository { + void saveAll(List products); +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/SimpleProductRepository.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/SimpleProductRepository.java new file mode 100644 index 0000000000..4a381dd8b9 --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/repo/SimpleProductRepository.java @@ -0,0 +1,30 @@ +package com.baeldung.spring.jdbc.batch.repo; + +import com.baeldung.spring.jdbc.batch.model.Product; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.util.List; + +@Repository +public class SimpleProductRepository implements ProductRepository { + + private final JdbcTemplate jdbcTemplate; + + public SimpleProductRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + @Transactional + public void saveAll(List products) { + for (Product product : products) { + jdbcTemplate.update("INSERT INTO PRODUCT (TITLE, CREATED_TS, PRICE) VALUES (?, ?, ?)", + product.getTitle(), Timestamp.valueOf(product.getCreatedTs()), product.getPrice()); + } + } + +} diff --git a/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/service/ProductService.java b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/service/ProductService.java new file mode 100644 index 0000000000..436764ffcf --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/java/com/baeldung/spring/jdbc/batch/service/ProductService.java @@ -0,0 +1,55 @@ +package com.baeldung.spring.jdbc.batch.service; + +import com.baeldung.spring.jdbc.batch.model.Product; +import com.baeldung.spring.jdbc.batch.repo.ProductRepository; + +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class ProductService { + + private final ProductRepository productRepository; + private final Random random; + private final Clock clock; + + public ProductService(ProductRepository productRepository, Random random, Clock clock) { + this.productRepository = productRepository; + this.random = random; + this.clock = clock; + } + + @Transactional + public long createProducts(int count) { + List products = generate(count); + long startTime = clock.millis(); + productRepository.saveAll(products); + return clock.millis() - startTime; + } + + private List generate(int count) { + final String[] titles = { "car", "plane", "house", "yacht" }; + final BigDecimal[] prices = { + new BigDecimal("12483.12"), + new BigDecimal("8539.99"), + new BigDecimal("88894"), + new BigDecimal("458694") + }; + + final List products = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + Product product = new Product(); + product.setCreatedTs(LocalDateTime.now(clock)); + product.setPrice(prices[random.nextInt(4)]); + product.setTitle(titles[random.nextInt(4)]); + products.add(product); + } + return products; + } +} diff --git a/persistence-modules/spring-jdbc/src/main/resources/com/baeldung/spring/jdbc/batch/application.properties b/persistence-modules/spring-jdbc/src/main/resources/com/baeldung/spring/jdbc/batch/application.properties new file mode 100644 index 0000000000..9898ae022b --- /dev/null +++ b/persistence-modules/spring-jdbc/src/main/resources/com/baeldung/spring/jdbc/batch/application.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:postgresql://localhost:5432/sample-baeldung-db +spring.datasource.username=postgres +spring.datasource.password=root +spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.hikari.data-source-properties.reWriteBatchedInserts=true \ No newline at end of file diff --git a/persistence-modules/spring-jdbc/src/test/java/com/baeldung/spring/jdbc/batch/service/ProductServiceUnitTest.java b/persistence-modules/spring-jdbc/src/test/java/com/baeldung/spring/jdbc/batch/service/ProductServiceUnitTest.java new file mode 100644 index 0000000000..26a5f64dc0 --- /dev/null +++ b/persistence-modules/spring-jdbc/src/test/java/com/baeldung/spring/jdbc/batch/service/ProductServiceUnitTest.java @@ -0,0 +1,76 @@ +package com.baeldung.spring.jdbc.batch.service; + +import com.baeldung.spring.jdbc.batch.model.Product; +import com.baeldung.spring.jdbc.batch.repo.ProductRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@ExtendWith(MockitoExtension.class) +class ProductServiceUnitTest { + + ProductRepository productRepository; + Random random; + Clock clock; + ProductService productService; + + @Captor + ArgumentCaptor> proArgumentCaptor; + + @BeforeEach + void setUp() { + this.productRepository = mock(ProductRepository.class); + this.random = mock(Random.class, withSettings().withoutAnnotations()); + this.clock = mock(Clock.class); + this.productService = new ProductService(this.productRepository, this.random, this.clock); + } + + @Test + void testWhenCreateProductsThenShouldSaveAndReturnElapsedTime() { + when(random.nextInt(4)) + .thenReturn(1, 3, 2, 0); + when(clock.instant()) + .thenReturn(Instant.parse("2022-04-09T10:15:30.00Z")); + when(clock.getZone()) + .thenReturn(ZoneId.of("UTC")); + + when(clock.millis()) + .thenReturn(100L,500L); + + final long actualElapsedTime = productService.createProducts(2); + + assertThat(actualElapsedTime) + .isEqualTo(400L); + + verify(productRepository,times(1)) + .saveAll(proArgumentCaptor.capture()); + + assertThat(proArgumentCaptor.getValue()) + .hasSize(2) + .extracting("title", "createdTs", "price") + .containsExactly( + tuple("yacht", LocalDateTime.parse("2022-04-09T10:15:30"), new BigDecimal("8539.99")), + tuple("car", LocalDateTime.parse("2022-04-09T10:15:30"), new BigDecimal("88894")) + ); + } +} \ No newline at end of file