Tăng hiệu suất insert hoặc update khối lượng lớn dữ liệu với gem activerecord-import trong Rails

1. Vấn đề đặt ra

Giả sử bạn có file dữ liệu chứa khoảng 1000 đối tượng bản ghi cần insert vào hệ thống hoặc update lại nếu đã tồn tại bản ghi. Nếu bạn thực hiện insert hoặc update từng bản ghi, mỗi lần như vậy bạn phải kết nối với Database do ActiveRecord trong rails không hỗ trợ insert hàng loạt record mà phải thực hiện tuần tự. Điều này làm cho chức năng của bạn thực hiện mất nhiều thời gian và giảm hiệu suất của ứng dụng vì phải sinh ra 1000 câu lệnh SQL để thực hiện việc ghi dữ liệu.

2. Giải pháp

ActiveRecord-import là một thư viện dùng để insert một số lượng lớn các bản ghi vào trong cơ sở dữ liệu sử dụng ActiveRecord. Sử dụng ActiveRecord-import cho phép sử dụng 1 câu lệnh insert cùng lúc nhiều values, điều này giúp bạn cải thiện được suất hiệu của chức năng này.

3. Cài đặt

Khai báo gem file

# Gemfile
gem "activerecord-import", "~> 0.15.0"

Sau khi đã thêm gem, thực hiện lệnh bundle install để cài gem

4. Sử dụng insert

Import sử dụng Columns và Arrays

Phương thức import có thể lấy một array các tên cột và array gồm các array khác. Mỗi array con đại diện cho một bản ghi và danh sách các giá trị theo thứ tự như các cột đã chỉ định. Đây là cơ chế import nhanh nhất và cũng nguyên thủy nhất.

columns = [ :name, :address ]
values = [ ["Vendor1", "Hanoi"], ["Vendor2", "Danang"] ]

Vendor.import columns, values

Import sử dụng Hashes

Phương thức import có thể nhận một array gồm các hash. Các keys tương ứng với tên cột trong cơ sở dữ liệu.

values = [{name: "Vendor1", address: "Hanoi"}, {name: "Vendor2", address: "Danang"}]

Vendor.import values

Import sử dụng Hashes và chỉ rõ tên Column

Phương thức import có thể lấy một array các tên cột và một array các đối tượng hash. Các tên cột được sử dụng để xác định các trường cần import vào database. Ví dụ sau sẽ chỉ nhập sách với trường name

vendors = [ 
  {name: "Vendor1", address: "Hanoi"},
  {name: "Vendor2", address: "Danang"}
]
columns = [ :name ]

Vendor.import columns, vendors

# Kết quả insert trong table vendors
# name    | address
#---------|--------
# Vendor1 | NULL   
# Vendor2 | NULL

Import sử dụng ActiveRecord models

Phương thức import có thể lấy một array các object model. Các thuộc tính sẽ được lấy ra từ mỗi model bằng cách so khớp với các thuộc tính của model

vendors = [ 
  Vendor.new(name: "Vendor1", address: "Hanoi"),
  Vendor.new(name: "Vendor1", address: "Hanoi")
]

Vendor.import vendors

Import sử dụng ActiveRecord Models và chỉ rõ tên Column

Phương thức import có thể lấy một array các object model. Các tên cột được sử dụng để xác định các trường cần import vào database.

vendors = [ 
  Vendor.new(name: "Vendor1", address: "Hanoi"),
  Vendor.new(name: "Vendor1", address: "Hanoi")
]
columns = [:name]

Vendor.import columns, vendors

# Kết quả insert trong table vendors
# name    | address
#---------|--------
# Vendor1 | NULL   
# Vendor2 | NULL

5. Sử dụng update

Trong một số trường hợp bạn muốn cập nhật lại thông tin nếu bản ghi đã tồn tại thì sử dụng phương thức on_duplicate_key_update, bằng cách chỉ rõ các colunm thuộc tính mà bạn cần cập nhật lại dữ liệu vào database. Ví dụ bên dưới sẽ cập nhật thông tin address của các bản ghi vendor trong trường hợp đã trùng name

vendors_update = []
list_name_vendor.each do |name_vendor|
	vendor = Vendor.find_by name: name_vendor
    if vendor.present?
    	vendor.address = "Address update"
        vendors_update << vendor
    end
end

# MySQL version
Vendor.import vendors_update, on_duplicate_key_update: [:address]

# PostgreSQL version
Vendor.import vendors_update, on_duplicate_key_update: {conflict_target: [:id], columns: [:address]}

# PostgreSQL shorthand version (conflict target must be primary key)
Vendor.import vendors_update, on_duplicate_key_update: [:address]

6. Một số tùy chọn

Thiết lập validate khi sử dụng

# Thực hiện import nhưng bỏ qua check validate trong model
Vendor.import columns, values, validate: false

#Import có thực hiện check validate trong model
Vendor.import columns, values, validate: true

# Mặc định nếu không khai báo thì sẽ thực hiện check validate trong model
Vendor.import columns, values

Thiết lập số lượng bản ghi cần import cho mỗi câu lệnh

Trong trường hợp các bản ghi quá lớn ,để tránh trường hợp quá tải chúng ta có thể sử dụng batch_size để điều khiển số lượng bản ghi được import : Ví dụ sử dụng 2 câu SQL để import 20 bản ghi vendors

Vendor.import vendors, batch_size: 10

Cùng xem kết quả SQL trong trường hợp này:

Callback trong activerecord-import

Có một lưu ý khi bạn sử dụng activerecord-import khi import dữ liệu nó sẽ bỏ qua các method callback liên quan đến create, update và delete bản ghi đó. Sỡ dĩ nó bỏ qua callback là vì với một khối lượng lớn bản ghi được import vào thì việc truy cập ActiveRecord trong bộ nhớ là không cần thiết. Tuy nhiên, trường hợp bạn muốn gọi callback thì có thể tham khảo ví dụ sau:

vendors.each do |vendor|
  vendor.run_callbacks(:save) {false}
  vendor.run_callbacks(:create) {false}
end
Vendor.import(vendors)

Thao tác này sẽ chạy before_create và before_save callbacks cho mỗi bản ghi vendor. Đối số false truyền vào đây mục đích để ngăn chặn việc thực thi after_save, điều này sẽ không có ý nghĩa trước khi nhập hàng loạt.

7. Kết luận

Bài viết này giới thiệu cho các bạn về gem activerecord-import hi vọng sẽ giúp ích được các bạn trong một vài trường hợp import dữ liệu lớn trong Rails. Nguồn tham khảo: https://github.com/zdennis/activerecord-import