Merge pull request #12464 from jrelmar/spring-jdbc-batch
BAEL-5545: prepared a simple demo to benchmark spring jdbc batch inserts
This commit is contained in:
commit
7050d0a1e8
|
@ -31,6 +31,11 @@
|
|||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<Product> 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());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<Product> products);
|
||||
}
|
|
@ -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<Product> products) {
|
||||
for (Product product : products) {
|
||||
jdbcTemplate.update("INSERT INTO PRODUCT (TITLE, CREATED_TS, PRICE) VALUES (?, ?, ?)",
|
||||
product.getTitle(), Timestamp.valueOf(product.getCreatedTs()), product.getPrice());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Product> products = generate(count);
|
||||
long startTime = clock.millis();
|
||||
productRepository.saveAll(products);
|
||||
return clock.millis() - startTime;
|
||||
}
|
||||
|
||||
private List<Product> 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<Product> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<List<Product>> 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"))
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue