Đã đăng vào thg 12 21, 2018 10:13 SA 2 phút đọc
Tại sao chia sẻ điều này?
Phương
thức Chunk
của Laravel được sử dụng rộng rãi để phân chia các truy vấn lớn thành các phần truy vấn nhỏ hơn khi xử lý với một cơ sở dữ liệu lớn. Nhưng có một vấn đề nếu thuộc tính được sử dụng trong truy vấn sẽ bị thay đổi trong callback function. Nó rất dễ bị bỏ qua và do đó thường bị sử dụng sai.
Vấn đề ở đây là gì?
Vui lòng xem ví dụ dưới đây:
Giả định: tổng số người dùng chưa xử lý là 400 [Id là 1 - 400]
Vấn đề là: Chỉ có một nửa số người dùng được xử lý.
Phân tích
Mỗi chunk đang được fetch thông qua single query sử dụng kết hợp với limit và offset. Chúng ta hãy xem chức năng chunk sẽ hoạt động như thế nào.
Ban đầu, người dùng chưa được xử lý là 400 [Id 1 - 400], đối với truy vấn chunk thứ nhất:
select * from `users` where `processed` = 0 limit 100 offset 0
và 100 người dùng đầu tiên Ids [1-100] được xử lýSau lần chunk thứ nhất, người dùng chưa được xử lý là 300 [Id 101 - 400], truy vấn chunk thứ 2:
select * from `users` where `processed` = 0 limit 100 offset 100
và 100 người dùng tiếp theo với Ids [201-300] được xử lýSau lần chunk thứ 2, người dùng chưa được xử lý là 200 [Id 101 - 200 và 301 - 400], đối với truy vấn đoạn thứ 3:
select * from `users` where `processed` = 0 limit 100 offset 200
và không có 1 người dùng nào được xử lý
Kết quả là: 200 người dùng được xử lý, nhưng 200 người dùng vẫn chưa được xử lý.
- Giải pháp
Bạn nên sử dụng phương thức chunkById
thay cho phương thức chunk
trong tình huóng này, chúng có vai trò tương tự như nhau. Sự khác biệt duy nhất là cách xây dựng truy vấn. ChunkById
sử dụng 'id' và 'limit' trong khi chunk
sử dụng 'limit' và 'offset'. Code sửa đổi sẽ như sau:
Khi sử dụng chunkById
, một câu truy vấn chunk sẽ giống như dưới đây:
Truy vấn chunk thứ nhất: select * from `users` where ``processed` = 0 and `id` > 0 order by `id` asc limit 100
Truy vấn chunk thứ 2: select * from `users` where ``processed` = 0 and `id` > 100 order by `id` asc limit 100
Truy vấn chunk thứ 3: select * from `users` where ``processed` = 0 and `id` > 200 order by `id` asc limit 100
Truy vấn chunk thứ 4: select * from `users` where ``processed` = 0 and `id` > 300 order by `id` asc limit 100
Do đó, kết quả tìm nạp sẽ là 1-100, 101- 200, 201 -300, 301- 400 theo trình tự và tất cả người dùng đều được xử lý.
Bài viết được sưu tầm và dịch từ: //engineering.carsguide.com.au/chunk-be-careful-b19c8197dc4d
All rights reserved
Facade DB
Trong Laravel, khi muốn lấy dữ liệu hoặc xây dựng query thì chúng ta có thể sử dụng facade DB. Khi đi kèm với table
thì facade này sẽ trả về instance của query builder thuộc về bảng mà chúng ta gọi đến. Ví dụ như DB::table['posts']
thì sẽ tạo ra một instance của query builder thuộc về bảng posts
. Với việc trả về instance của query builder như vậy, thì
dev có thể thêm nhiều câu query nữa vào trong cái instance đó như là lọc bởi lượt người xem lớn hơn hoặc bằng năm, sắp xếp theo cột title theo thứ tự Z-A, .... Và cuối cùng, chúng ta sẽ có được kết quả khi gọi đến hàm get
, hàm này sẽ trả về instance là kiểu Collection và dev có thể viết function hoặc dùng những hàm có sẵn trong Collection để làm những việc mình mong muốn như map data, duyệt từng record.
Vấn đề
Với facade DB thì mọi chuyện sẽ khá là dễ dàng và vui
vẻ. Như vậy, nếu muốn lấy tất cả dữ liệu của bảng posts
thì chúng ta sẽ gọi facade DB như sau DB::table['posts']->get[]
. Nice, chỉ với một dòng đơn giản như vậy, mà chúng ta đã có thể lấy hết tất cả dữ liệu của bảng post, sau đó làm gì tùy thích. Chúng ta có thể duyệt từng record, map, filter, tính sum, count, vân vân và mây mây. Tuy nhiên, khi gọi như vậy, dữ liệu sẽ được load vào memory một lúc. Thử tưởng tượng, nếu dữ liệu không phải là vài chục, vài trăm, vài ngàn mà là vài chục ngàn, vài trăm ngàn hay có
thể lên đến 1 triệu thì sao? Thì chúng ta sẽ tốn khá là nhiều memory để load và nếu memory không đủ thì ngủm củ tỏi. Vậy có cách nào để cải thiện không?
Chunk
Giải pháp cũng khá đơn giản, nếu như mà dữ liệu nhiều quá, load một lần như vậy hao tốn memory quá thì chúng ta sẽ load từ từ, từ từ mà tốn chứ không tốn một lần quá nhiều. Laravel cung cấp cho một giải pháp với tên gọi là Chunk. Ý tưởng khá đơn giản, có một trăm ngàn dữ liệu, thay vì load hết vô memory thì chia nhỏ ra. Có thể chia nhỏ ra thành 100 records mỗi lần, như vậy cứ load 100 records, xong giải phóng dữ liệu rồi sau đó tiếp tục load 100 records tiếp theo. Như vậy, memory chúng ta cần sử dụng sẽ ít hơn khá là nhiều. Thay vì cần dùng 8GB RAM thì với chunk đôi khi chúng ta chỉ cần dùng đến tầm 4GB hoặc 5GB RAM là đẹp.
Cách dùng chunk cũng khá đơn giản, viết câu query builder, gọi hàm chunk, pass số lượng record muốn chunk và closure để thực hiện công việc cần thiết vào. Lấy ví dụ mình muốn lấy tất cả post, chunk 100 records mỗi lần và in ra title của post đó thì mình sẽ làm như thế này:
DB::table['posts']->chunk[100, function [$posts] {
foreach[$post as $post] {
echo $post->title;
}
}
Ngoài ra, chúng ta cũng có thể dừng chunk lại nếu có gì đó sai sai. Giả sử đến record thứ 1000 mình dừng chunk thì mình chỉ cần return false.
DB::table['posts']->chunk[100, function [$posts] {
if [$post->id >= 1000] {
return false;
}
}
Nếu mình cần update dữ liệu trong database thì cân nhắc dùng chunkById sẽ an toàn hơn vì phương thức này sẽ dựa vào primary key của record để mà chunk
Bonus thêm streaming lazy
Ngoài chunk ra, thì
còn có streaming lazy, nó cũng hoạt động na ná. Với phương thức này thì nó sẽ thực thi những câu query ở trong những chunks. Chỉ cần thêm phương thức lazy vào là được và khi thêm lazy thì sẽ trả về kiểu LazyCollection
do đó bạn có thể dùng những phương thức của LazyCollection
để thao tác với dữ liệu như each
, map
, .... Cũng như chunk, nếu cần update dữ liệu trong DB thì nên cân nhắc dùng lazyById
hoặc lazyByIdDesc
, laravel sẽ tách kết quả dựa trên primary key của record đó.