Các lưu ý về strategy của GeneratedValue khi sử dụng hibernate

Bài viết được sự cho phép của tác giả Trần Văn Dem

Hiện nay việc tìm kiếm các hướng dẫn về sử dụng hibernate, spring jpa là rất dễ. Tuy nhiên các hướng dẫn này thường chỉ giới thiệu cách sử dụng, quản lý Id của Entity thông qua strategy : AUTO,TABLE,SEQUENCE,IDENTITY. Nhưng rất ít hoặc rất khó tìm bài hướng dẫn nào nói cụ thể về các kiểu strategy này và cách sử dụng hiệu trong dự án. Bài viết này tôi sẽ giúp các bạn hiểu rõ hơn về các loại strategy này từ đó có thể tự tin lựa chọn trong project tránh các lỗi không đáng có.

Trước khi tìm hiểu về các loại strategy thì chúng ta nên phân biệt các loại dữ liệu này. Trước tiên thì 2 loại này sẽ là cách cơ sở dữ liệu của bạn dùng để tạo id cho bảng lưu trữ dữ liệu.

Cách sử dụng auto_increment

create table user
(
    id   int auto_increment
        primary key,
    name varchar(255) null,
    age  int          null
);


insert into user (age, name) values (104, 'mai4')
insert into user (age, name values (103, 'mai3')

SELECT * FROM user;

+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | mai4 |  104 |
|  2 | mai3 |  103 |
+----+------+------+
2 rows in set (0,00 sec)

Kiểu dữ liệu này chắc hầu hết mọi người đều đã biết và sử dụng thường xuyên kiểu này. Các loại database sau hỗ trợ: mysqlmariadb,…

  Cấu hình Spring Data JPA với @EnableJpaRepositories annotation

  Các Annotation của Hibernate

Cách sử dụng sequence với mariadb. Chi tiết về sequece của mariadb mọi người tham khảo tại link

CREATE SEQUENCE serial START WITH 1 INCREMENT BY 10;
insert into user (age, name,id) values (104, 'mai4',nextval(serial));
insert into user (age, name,id) values (103, 'mai3',nextval(serial));

SELECT * FROM user;

+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | mai4 |  104 |
| 11 | mai3 |  103 |
+----+------+------+
2 rows in set (0,00 sec)

Các loại database sau hỗ trợ : postgresqlmariadb,oracle,… Nhìn sơ qua thì khi sử dụng sequence câu lệnh insert sẽ dài và khó nhớ hơn.

Tham khảo việc làm JavaScript hấp dẫn trên TopDev

Tạo 1 entity đơn giản như bên dưới lưu ý allocationSize phải đúng với giá trị INCREMENT BY khi tạo sequence. Loại strategy này sẽ hỗ trợ việc batch insert. Ví dụ bên dưới thực hiện với mariadb.

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "serial"
    )
    @SequenceGenerator(
            name = "serial",
            sequenceName = "serial",
            allocationSize = 10
    )
    private int id;

    private String name;

    private int age;

}

Tạo 1 router để save vào database sau:

    @PutMapping("/multi/user")
    String insertUser() {
        List<User> savedUser = new ArrayList<>();

        for (int i =0 ; i < 5 ; i ++){
            User user = new User();
            int index = count.addAndGet(1);
            user.setName("demtv" + index);
            user.setAge(index);
            savedUser.add(user);
        }
        repository.saveAll(savedUser);
        return "done";
    }

Chỉnh sửa config của spring boot để kết nối với mariadb.

spring:
  application:
    name: user-service
    url: jdbc:mariadb://127.0.0.1:3307/blog?characterEncoding=utf8
    driver-class-name: org.mariadb.jdbc.Driver
    username: root
    password: demtv
  jpa:
    properties:
      hibernate:
        jdbc:
             batch_size : 5
        order_inserts : true

    show-sql : true

Để sử dụng được mariadb chúng ta nên sử dụng mariadb client sau.

        <!-- https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client -->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.3</version>
        </dependency>

Chúng ta cần ghi đè lại phương thức log của hibernate để biết được có sử dụng batch process hay không? Tham khảo tại link

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;


import javax.sql.DataSource;
import java.lang.reflect.Method;

@Component
public class DatasourceProxyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        if (bean instanceof DataSource) {
            ProxyFactory factory = new ProxyFactory(bean);
            factory.setProxyTargetClass(true);
            factory.addAdvice(new ProxyDataSourceInterceptor((DataSource) bean));
            return factory.getProxy();
        }

        return bean;
    }

    private static class ProxyDataSourceInterceptor implements MethodInterceptor {

        private final DataSource dataSource;

        public ProxyDataSourceInterceptor(final DataSource dataSource) {
            this.dataSource = ProxyDataSourceBuilder.create(dataSource).name("Batch-Insert-Logger").asJson().countQuery().logQueryToSysOut().build();
        }

        @Override
        public Object invoke(final MethodInvocation invocation) throws Throwable {
            Method proxyMethod = ReflectionUtils.findMethod(dataSource.getClass(), invocation.getMethod().getName());
            if (proxyMethod != null) {
                return proxyMethod.invoke(dataSource, invocation.getArguments());
            }
            return invocation.proceed();
        }
    }
}

Thực hiện gọi đến route và chúng ta check log trên service ta được kết quả sau :

Hibernate: select nextval(serial)
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select nextval(serial)"], "params":[[]]}
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
{"name":"Batch-Insert-Logger", "time":2, "success":true, "type":"Prepared", "batch":true, "querySize":1, "batchSize":5, "query":["insert into user (age, name, id) values (?, ?, ?)"], "params":[["1","demtv1","12"],["2","demtv2","13"],["3","demtv3","14"],["4","demtv4","15"],["5","demtv5","16"]]}

Check log của mariadb ta được kết quả:

SET GLOBAL log_output = 'TABLE';
SET GLOBAL general_log = 'ON';
select * from mysql.general_log a  order by event_time desc;

+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+
| event_time                 | user_host                  | thread_id | server_id | command_type | argument                                                                                                       |
+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+
| 2021-07-18 08:52:07.774988 | root[root] @  [172.17.0.1] |        49 |         1 | Query        | SHOW WARNINGS                                                                                                  |
| 2021-07-18 08:52:07.773198 | root[root] @  [172.17.0.1] |        49 |         1 | Query        | SELECT @@session.tx_isolation                                                                                  |
| 2021-07-18 08:51:56.061139 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | set autocommit=1                                                                                               |
| 2021-07-18 08:51:56.057017 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | COMMIT                                                                                                         |
| 2021-07-18 08:51:56.055347 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (5, 'demtv5', 16)                                                      |
| 2021-07-18 08:51:56.055283 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (4, 'demtv4', 15)                                                      |
| 2021-07-18 08:51:56.055213 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (3, 'demtv3', 14)                                                      |
| 2021-07-18 08:51:56.055135 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (2, 'demtv2', 13)                                                      |
| 2021-07-18 08:51:56.054717 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (1, 'demtv1', 12)                                                      |
| 2021-07-18 08:51:56.031163 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | select nextval(serial)                                                                                         |
+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+

select * from user;

+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | mai4   |  104 |
| 11 | mai3   |  103 |
| 12 | demtv1 |    1 |
| 13 | demtv2 |    2 |
| 14 | demtv3 |    3 |
| 15 | demtv4 |    4 |
| 16 | demtv5 |    5 |
+----+--------+------+

Ta thấy các id được service tạo ra nó có thứ tự liên tiếp khác với những id được chúng ta tạo với 2 câu lệnh insert đầu tiên. Hình ảnh bên dưới mô tả cơ chế GenerationType.SEQUENCE của hibernate.

Pooled

Đầu tiên hibernate sẽ gọi vào database để lấy nextVal của sequece lưu lại nextVal này và tiếp tự generate id của entity từ nextVal – allocateSize-1 đến nextVal hoạt động tạo id này sẽ không cần truy cập vào database nên sẽ tối ưu về mặt tốc độ. Mặt khác vì kiểu sequence là kiểu đặc biệt nên 2 service cùng gọi để lấy nextVal tại 1 thời điểm thì kết quả trả về cho 2 service là khác nhau cho nên sẽ không có trường hợp bị trùng id giữa các service khác nhau sử dụng cùng một sequence để tạo id.

Chúng ta sẽ sử dụng loại strategy này với mysql, strategy này ứng với dạng auto_increment. Chỉnh sửa một chút về file config, Entity, thư viện.

Modify Entity

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private int id;

    private String name;

    private int age;

}

Modify Config và thư viện kết nối

spring:
  application:
    name: user-service
  config:
    import : optional:configserver:http://localhost:8888/
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=utf8
    driver-class-name: com.mysql.jdbc.Driver
    username: demtv
    password: Anhdem96!@
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 5
        order_inserts : true

    show-sql : true

server:
  port: 8082
        <dependency> 
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>

Vì trong hibernate khi batch insert chúng ta bắt buộc phải truyền theo Id, nhưng dạng này lại dựa vào cơ chế auto_increment của database nên lúc insert chúng ta chưa biết được id của nó là gì khiến cho dạng này hibernate sẽ không hỗ trợ batch insert mặc dù chúng ta vẫn cấu hình batch insert cho nó. Thực hiện gọi đến route và ta được kết quả.

Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["1","demtv1"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["2","demtv2"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["3","demtv3"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["4","demtv4"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["5","demtv5"]]}

Check kết quả trong mysql ta được:

select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | mai3   |  103 |
|  2 | demtv1 |    1 |
|  3 | demtv2 |    2 |
|  4 | demtv3 |    3 |
|  5 | demtv4 |    4 |
|  6 | demtv5 |    5 |
+----+--------+------+

Bằng các cách tạo id hiện nay, các service của chúng ta không cần phải trọc vào database vẫn có thể tạo ra các id khác nhau (Mình sẽ viết phương pháp này trong bài tiếp) . Ngay cả khi chúng ta thực hiện truyền id này vào trong entity sử dụng strategy GenerationType.IDENTITY hibernate cũng không thực hiện batch insert, thậm trí khi truyền Id chúng ta lại có một hiệu năng còn tệ hơn.


    @PutMapping("/multi/user")
    String insertMultiUser() {
        List<User> savedUser = new ArrayList<>();
        for (int i = 1; i < 6; i++) {
            User user = new User();
            user.setId(1000 * i);
            int index = count.addAndGet(1);
            user.setName("demtv" + index);
            user.setAge(index);
            savedUser.add(user);
        }
        repository.saveAll(savedUser);
        return "done";
    }

Thực hiện gọi route ta có kết quả

{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["1000"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["1","demtv1"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["2000"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["2","demtv2"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["3000"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["3","demtv3"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["4000"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["4","demtv4"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["5000"]]}
Hibernate: insert into user (age, name) values (?, ?)
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into user (age, name) values (?, ?)"], "params":[["5","demtv5"]]}

Theo kết quả query của hibernate bên trên thì trước mỗi câu insert chúng ta đều phải có một câu select để kiểm tra id của chúng ta truyền vào đã tồn tại trong bảng hay chưa? Rồi mới đến bước insert vào database. Điều đó khiến hiệu năng giảm xuống. Tiếp theo check kết quả mysql chúng ta mới thấy sự bất ngờ.

mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | mai3   |  103 |
|  2 | demtv1 |    1 |
|  3 | demtv2 |    2 |
|  4 | demtv3 |    3 |
|  5 | demtv4 |    4 |
|  6 | demtv5 |    5 |
|  7 | demtv1 |    1 |
|  8 | demtv2 |    2 |
|  9 | demtv3 |    3 |
| 10 | demtv4 |    4 |
| 11 | demtv5 |    5 |
+----+--------+------+
11 rows in set (0,00 sec)

Bất ngờ là id chúng ta truyền vào không hề xuất hiện trong bảng khi sử dụng strategy này.

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "serial"
    )
    @SequenceGenerator(
            name = "serial",
            sequenceName = "serial",
            allocationSize = 10
    )
    private int id;

    private String name;

    private int age;

}


config
spring:
  application:
    name: user-service
  config:
    import : optional:configserver:http://localhost:8888/
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=utf8
    driver-class-name: com.mysql.jdbc.Driver
    username: demtv
    password: Anhdem96!@
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 5
        order_inserts : true
        ddl-auto : create
    generate-ddl : true
    show-sql : true
Hibernate: select next_val as id_val from serial for update
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select next_val as id_val from serial for update"], "params":[[]]}
Hibernate: update serial set next_val= ? where next_val=?
{"name":"Batch-Insert-Logger", "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["update serial set next_val= ? where next_val=?"], "params":[["11","1"]]}
Hibernate: select next_val as id_val from serial for update
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select next_val as id_val from serial for update"], "params":[[]]}
Hibernate: update serial set next_val= ? where next_val=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["update serial set next_val= ? where next_val=?"], "params":[["21","11"]]}
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
{"name":"Batch-Insert-Logger", "time":2, "success":true, "type":"Prepared", "batch":true, "querySize":1, "batchSize":5, "query":["insert into user (age, name, id) values (?, ?, ?)"], "params":[["1","demtv1","1"],["2","demtv2","2"],["3","demtv3","3"],["4","demtv4","4"],["5","demtv5","5"]]}

Chúc mừng chúng ta cuối cùng đã thực hiện được batch insert với mysql. Nhưng sự thật có đáng để vui hay không? Theo như log bên trên mỗi lần thực hiện insert hibernate lại vào bảng serial lấy ra id tiếp theo Hibernate: select next_val as id_val from serial for update Cơ chế này sẽ làm giảm hiệu năng chương trình đi rất nhiều. Tiếp đến câu lệnh hibernate dùng để lấy id cũng gây lock table serial lại khiến hiệu năng lại giảm thêm. Cơ chế này không phải cơ chế SEQUENCE mà là cơ chế “TABLE” một trong các cơ chế mọi người nên tránh sử dụng. Chỉ sủ dụng khi loại database của mọi người không hỗ trọ cơ chế “auto_increment” và “sequence”. Với mysql hibernate chỉ sử dụng tốt nhất với strategy = GenerationType.IDENTITY

Giả sử với phương pháp tạo Id, chúng ta không cần thiết phải dựa vào database để tạo id. Chúng ta dùng id tự tạo ra để insert vào database.

import lombok.Data;

import javax.persistence.*;

@Entity
@Table(name = "user")
@Data
public class User {
    @Id
    private int id;

    private String name;

    private int age;

}
    @PutMapping("/multi/user")
    String insertMultiUser() {
        List<User> savedUser = new ArrayList<>();
        for (int i = 1; i < 6; i++) {
            User user = new User();
            user.setId(1000 * (i + 1));
            int index = count.addAndGet(1);
            user.setName("demtv" + index);
            user.setAge(index);
            savedUser.add(user);
        }
        repository.saveAll(savedUser);
        return "done";
    }

Chúng ta gọi đến route và check log

Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["2000"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["3000"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["4000"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["5000"]]}
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?
{"name":"Batch-Insert-Logger", "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from user user0_ where user0_.id=?"], "params":[["6000"]]}
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
Hibernate: insert into user (age, name, id) values (?, ?, ?)
{"name":"Batch-Insert-Logger", "time":2, "success":true, "type":"Prepared", "batch":true, "querySize":1, "batchSize":5, "query":["insert into user (age, name, id) values (?, ?, ?)"], "params":[["1","demtv1","2000"],["2","demtv2","3000"],["3","demtv3","4000"],["4","demtv4","5000"],["5","demtv5","6000"]]}

Cách này cũng giống như khi sử dụng strategy=IDENTITY cũng gây hiệu năng giảm sút vì cần các câu select trước các câu insert.

Các bạn có thể biết cách batch insert không phải là cách insert nhanh nhất khi thực hiện insert dữ liệu vào database. Nó chỉ tiết kiệm được IO truyền qua mạng bằng cách gửi nhiều lệnh insert lên database thực hiện một lần. Kiểu insert nhanh nhất vào database phải là bulk insert rất tiếp hibernate không hỗ trợ kiểu này. Nhưng với mariadb và mysql chúng ta có config rewriteBatchedStatements nếu set config này rewriteBatchedStatements=true thì jdbc sẽ viết lại các câu lệnh batch insert thành bulk insert.

spring:
  application:
    name: user-service
    url: jdbc:mariadb://127.0.0.1:3307/blog?characterEncoding=utf8&rewriteBatchedStatements=true
    driver-class-name: org.mariadb.jdbc.Driver
    username: root
    password: demtv
  jpa:
    properties:
      hibernate:
        jdbc:
             batch_size : 5
        order_inserts : true

    show-sql : true

Gọi đến route và check lại log trên server mariadb ta được như sau

select * from mysql.general_log a  order by event_time desc;

+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+
| event_time                 | user_host                  | thread_id | server_id | command_type | argument                                                                                                       |
+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+
| 2021-07-18 08:52:07.774988 | root[root] @  [172.17.0.1] |        49 |         1 | Query        | SHOW WARNINGS                                                                                                  |
| 2021-07-18 08:52:07.773198 | root[root] @  [172.17.0.1] |        49 |         1 | Query        | SELECT @@session.tx_isolation                                                                                  |
| 2021-07-18 08:51:56.061139 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | set autocommit=1                                                                                               |
| 2021-07-18 08:51:56.057017 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | COMMIT                                                                                                         |
| 2021-07-18 08:51:56.055347 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | insert into user (age, name, id) values (5, 'demtv5', 16),(4, 'demtv4', 15) ,(3, 'demtv3', 14),(2, 'demtv2', 13),(1, 'demtv1', 12)|
| 2021-07-18 08:51:56.031163 | root[root] @  [172.17.0.1] |        71 |         1 | Query        | select nextval(serial)                                                                                         |
+----------------------------+----------------------------+-----------+-----------+--------------+----------------------------------------------------------------------------------------------------------------+

Chú ý phần argument của câu lệnh insert nó đã được viết lại thành bulk insert điều này tăng hiệu năng của chương trình.

Sau khi thử nghiệm các loại strategy khác nhau chúng ta có kết luận sau:

  • Sử dụng SEQUENCE khi database hỗ trợ dạng này. Kể cả hỗ trợ SEQUENCE lẫn IDENTITY thì vẫn chọn dạng SEQUENCE vì hibernate hỗ trợ tốt nhất với dạng này.
  • Nếu database hỗ trợ IDENTITY thì chỉ nên dùng IDENTITY đùng sử dụng các loại khác
  • Không sử dụng loại TABLE trừ khi database của bạn không hỗ trợ “auto_increment” hoặc “sequence”
  • Không nên truyền id khi thực hiện insert trong hibernate nếu không hiệu năng chương trình của bạn sẽ có vấn đề
  • Sử dụng config “rewriteBatchedStatements” với mariadb, mysql để tăng hiệu năng của chương trình.

Bài viết gốc được đăng tải tại demtv.hashnode.dev

Xem thêm:

Xem thêm Việc làm IT hấp dẫn trên TopDev