includes và joins trong Rails
Bài đăng này đã không được cập nhật trong 3 năm
Có lần người bạn hỏi tôi về sự khác nhau về cách dùng giữa includes và joins, ngẫm lại thì cũng thấy có chút dễ nhầm lẫn nên tôi quyết định trình bày ra một số ý để chia sẻ với các bạn.
Sự giống nhau?
joins và includes nói nôm na và với cách nhìn qua bên ngoài đều là liên kết hai bảng có association với nhau. Và khi in ra kết quả trên màn hình console thì có thể mọi người sẽ nhìn nhận dưới góc độ là một mảng hoặc một Active Record mà không thấy sự khác nhau là mấy giữa hai kết quả trả về. Vậy thực sự thì includes và joins có khác nhau trong cách dùng và kết quả trả về.
Sự khác nhau giữa Includes và Joins?
Câu trả lời đầu tiên: chắc chắn có sự khác nhau giữa includes và joins.
Khái niệm quan trọng nhất cần phải hiểu khi sử dụng includes và joins đó là: cả hai đều có trường hợp sử dụng tối ưu. Includes sử dụng eager loading trong khi joins sử dụng lazy loading, cả hai đều rất mạnh mẽ nhưng rất dễ bị lạm dụng để làm giảm hoặc quá mức performance.
Trước hết, ta hãy xem lại mô tả includes method trong tài liệu Ruby on Rails: With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries. được hiểu rằng, số lượng câu queries được giảm thiểu khi dùng includes method. Hiểu theo cách khác, khi đang query một table có associate với một table khác, cả hai tables được loaded vào bộ nhớ và làm giảm số lượng câu queries yêu cầu xuống. Trong ví dụ dưới đây, ta sẽ tương tác đến những companies có associates với active Person record:
@companies = Company.includes(:persons).where(:persons => { active: true } ).all
@companies.each do |company|
company.person.name
end
Với vòng lặp cho mỗi company và in ra tên của person, chúng ta lại trở về cách tương tác với person name sau mỗi lần query. Tuy nhiên, khi sử dụng includes, nó đã được load từ trước đến associate với bảng person, do đó block code trên chỉ chạy với một lần query.
Vậy điều gì sẽ xảy ra nếu tôi tương tác đến tất cả các phần tử từ bảng person, nhưng không xuất ra bất cứ phần tử nào person? Nó sẽ bắt đầu trở nên overkill loading (nôm na là quá tải) đến associated table. Đó là khi joins tỏa sáng. Nếu ta tiếp tục với ví dụ bên trên, ta sẽ thấy vì sao nhiều người cảm thấy dễ bị nhầm lẫn khi sử dụng joins và includes, dưới đây là một số thay đổi:
@companies = Company.joins(:persons).where(:persons => { active: true } ).all
@companies.each do |company|
company.name
end
Trực quan nhất là cách thay thế includes với joins, tuy nhiên về mặt nguyên lý hoạt động vẫn còn ẩn giấu nhiều điều. Phương thức joins sử dụng lazy loads database, nhưng chỉ load các thuộc tính từ bảng Company vào bộ nhớ, còn Person thì không yêu cầu. Vì vậy chúng ta không load data dư thừa vào bộ nhớ khi không cần thiết. Dù vậy, khi chúng ta muốn sử dụng đến data của bảng Person thì cần phải một mảng tương đương, và nó sẽ yêu cầu thêm các truy vấn cơ sở dữ liệu.
Dưới đây là một số thống kê
Giả sử không dùng includes để truy vấn dữ liệu,
def index
@shippings = Shipping.active.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: @shippings }
end
end
Đây là mà hình queries mà tôi đã chụp lại, rất nhiều queries (N+1 queries)
ActiveRecord: 262.2ms
Chính bởi không sử dụng includes làm cho số lượng queries tăng lên rất nhiều, thời gian cũng khá lớn, điều đó làm cho hệ thống chậm đi một cách đáng sợ. Thay vì thế, tôi dùng includes:
def index
@shippings = Shipping.active.includes(:zones, :tiers).all
respond_to do |format|
format.html # index.html.erb
format.json { render json: @shippings }
end
end
thời gian được rút ngắn quá nhiều: 2.8ms Đó chỉ là một ví dụ nhỏ về sự cần thiết của includes trong sử dụng truy vấn dữ liệu.
Tóm lại
- includes để loading associations.
- joins để join các bảng(?!). Nó sẽ lấy một đoạn sql hoặc tên associations theo nhiều cách khác nhau và join các bảng có liên quan.
Bài viết còn nhiều thiếu sót mong bạn đọc gần xa góp ý và xây dựng (bow).
All rights reserved