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ó.
1. sequence vs auto_increment
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.
auto increment
Cách sử dụng auto_increment
createtableuser(idint auto_increment
primary key,namevarchar(255)null,
age intnull);insertintouser(age,name)values(104,'mai4')insertintouser(age,namevalues(103,'mai3')SELECT*FROMuser;+----+------+------+| id | name | age |+----+------+------+|1| mai4 |104||2| mai3 |103|+----+------+------+2 rows inset(0,00 sec)
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
CREATESEQUENCEserialSTARTWITH1INCREMENTBY10;insertintouser(age,name,id)values(104,'mai4',nextval(serial));insertintouser(age,name,id)values(103,'mai3',nextval(serial));SELECT*FROMuser;+----+------+------+| id | name | age |+----+------+------+|1| mai4 |104||11| mai3 |103|+----+------+------+2 rows inset(0,00 sec)
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.
@PutMapping("/multi/user")String insertUser(){List<User> savedUser =newArrayList<>();for(int i =0; i <5; i ++){Useruser=newUser();intindex= count.addAndGet(1);user.setName("demtv"+index);user.setAge(index);
savedUser.add(user);}
repository.saveAll(savedUser);return"done";}
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.
Đầ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.
3. strategy = GenerationType.IDENTITY
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.
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:insertintouser(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:insertintouser(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:insertintouser(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:insertintouser(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:insertintouser(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"]]}
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 strategyGenerationType.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 =newArrayList<>();for(int i =1; i <6; i++){Useruser=newUser();user.setId(1000* i);intindex= count.addAndGet(1);user.setName("demtv"+index);user.setAge(index);
savedUser.add(user);}
repository.saveAll(savedUser);return"done";}
{"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:insertintouser(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_ fromuser 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:insertintouser(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_ fromuser 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:insertintouser(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_ fromuser 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:insertintouser(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_ fromuser 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:insertintouser(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ờ.
Hibernate:select next_val as id_val fromserialforupdate{"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:updateserialset 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 fromserialforupdate{"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:updateserialset 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:insertintouser(age,name,id)values(?,?,?)Hibernate:insertintouser(age,name,id)values(?,?,?)Hibernate:insertintouser(age,name,id)values(?,?,?)Hibernate:insertintouser(age,name,id)values(?,?,?)Hibernate:insertintouser(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
3.2 Không sử dụng strategy
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.
Hibernate:select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ fromuser 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_ fromuser 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_ fromuser 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_ fromuser 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_ fromuser 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:insertintouser(age,name, id)values(?,?,?)Hibernate:insertintouser(age,name, id)values(?,?,?)Hibernate:insertintouser(age,name, id)values(?,?,?)Hibernate:insertintouser(age,name, id)values(?,?,?)Hibernate:insertintouser(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.
4. Bulk insert with mariadb
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.
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.
5. Kết luận
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.