Tìm hiểu preload, eager_load, includes, references, and joins in Rails

Việc lấy dữ liệu bằng My Sql mà yêu cầu cần liên kết từ hai bảng trở lên hẳn là quá quen thuộc với lập trình viên chúng ta. Tuy nhiên, mỗi lập trình viên lại thường phát triển ứng dụng của mình trên một framework nhất định tương ứng với ngôn ngữ được sử dụng. Mỗi một framework đó lại cung cấp những method query ngắn gọn, tiện lợi giúp lập trình viên dễ ràng hơn trong việc viết code.

Tuy vậy, việc không nắm rõ cách thức hoạt động, ưu điểm, nhược điểm của những method đó sẽ dẫn đến việc sử dụng sai lầm, tốn tài nguyên cũng như vấn đề performance của ứng dụng. Cụ thể, trong framework Ruby on Rails cung cấp những method query preload, eager_load, includes, references, joins, mỗi một phương pháp có cách hoạt động phù hợp với từng mục đích khác nhau.

Chuẩn bị dữ liệu

Thiết kế DB và dữ liệu giả như bên dưới.

class CreateStudents < ActiveRecord::Migration[5.0]
  def change
    create_table :students do |t|

      t.string :name
      t.integer :room_id
      t.timestamps
    end
  end
end
class Student < ApplicationRecord
  belongs_to :room, class_name: Room
end

class CreateRooms < ActiveRecord::Migration[5.0]
  def change
    create_table :rooms do |t|

      t.string :name
      t.timestamps
    end
  end
end
class Room < ApplicationRecord
  has_many :students
end

(1..3).each do |c_room|
  class_room = Room.create(name: "Class #{c_room}")
  (1..5).each { |p_id| class_room.students.create(name: "Students #{c_room}-#{p_id}") }
end

Chúng ta sẽ thử lần lượt các method trên rails console để biết được cách thức hoạt động và tốc độ xử lý của chúng.

Preload

Test với câu lệnh:

2.2.0 :006 > Room.preload(:students)

Cho kết quả như sau:

Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
Student Load (0.2ms)  SELECT "students".* FROM "students" WHERE "students"."room_id" IN (1, 2, 3)
 => #<ActiveRecord::Relation [#<Room id: 1, name: "Class 1", created_at: "2016-09-27 03:26:55", updated_at: "2016-09-27 03:26:55">, #<Room id: 2, name: "Class 2", created_at: "2016-09-27 03:26:56", updated_at: "2016-09-27 03:26:56">, #<Room id: 3, name: "Class 3", created_at: "2016-09-27 03:26:57", updated_at: "2016-09-27 03:26:57">]>

Các bạn có thể thấy rõ câu lệnh preload luôn luôn sinh ra hai câu query:

  • Truy vấn lấy ra tất cả dữ liệu trong bảng room.
  • Truy vấn lấy ra tất cả dữ liệu của bảng students với điều kiện của bảng rooms đã truy vấn ở trên.

Includes

So với preload thỳ dùng includes sẽ thông minh hơn nhiều.

  • Cách 1: Nếu chỉ dùng includes không thỳ hoạt động không khác gì preload.
2.2.0 :014 > Room.includes(:students)
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
  Student Load (0.1ms)  SELECT "students".* FROM "students" WHERE "students"."room_id" IN (1, 2, 3)
 => #<ActiveRecord::Relation [#<Room id: 1, name: "Class 1", created_at: "2016-09-27 03:26:55", updated_at: "2016-09-27 03:26:55">, #<Room id: 2, name: "Class 2", created_at: "2016-09-27 03:26:56", updated_at: "2016-09-27 03:26:56">, #<Room id: 3, name: "Class 3", created_at: "2016-09-27 03:26:57", updated_at: "2016-09-27 03:26:57">]>
  • Cách 2: Dùng includes kết hợp với điều kiện ở bảng liên kết.
2.2.0 :015 > Room.includes(:students).where(students: {name: "1"})
  SQL (6.7ms)  SELECT "rooms"."id" AS t0_r0, "rooms"."name" AS t0_r1, "rooms"."created_at" AS t0_r2, "rooms"."updated_at" AS t0_r3, "students"."id" AS t1_r0, "students"."name" AS t1_r1, "students"."room_id" AS t1_r2, "students"."created_at" AS t1_r3, "students"."updated_at" AS t1_r4 FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id" WHERE "students"."name" = ?  [["name", "1"]]
 => #<ActiveRecord::Relation []>

Các bạn có thể thấy hai câu query giờ chỉ còn 1 với LEFT OUTER JOIN việc này có lợi là giảm thiểu lượng query tới server giúp server giảm tải.

References

Chú ý là chỉ dùng kết hợp được với includes giúp cho includes tạo ra query giống với eager_load.

Ví dụ:

2.2.0 :024 > Room.includes(:students).where({name: "1"}).references(:students)
  SQL (0.2ms)  SELECT "rooms"."id" AS t0_r0, "rooms"."name" AS t0_r1, "rooms"."created_at" AS t0_r2, "rooms"."updated_at" AS t0_r3, "students"."id" AS t1_r0, "students"."name" AS t1_r1, "students"."room_id" AS t1_r2, "students"."created_at" AS t1_r3, "students"."updated_at" AS t1_r4 FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id" WHERE "rooms"."name" = ?  [["name", "1"]]
 => #<ActiveRecord::Relation []>

Eager_load

Những đặc điểm nổi bật của eager_load là:

  • Chỉ tạo ra một query với LEFT OUTER JOIN
  • Join trước, rồi sau đó query theo điều kiện của bảng references
  • Cách hoạt động giống với includes + references.

Xét ví dụ sau để thấy những điểm giống và khác nhau.

2.2.0 :025 > Room.eager_load(:students)
  SQL (0.3ms)  SELECT "rooms"."id" AS t0_r0, "rooms"."name" AS t0_r1, "rooms"."created_at" AS t0_r2, "rooms"."updated_at" AS t0_r3, "students"."id" AS t1_r0, "students"."name" AS t1_r1, "students"."room_id" AS t1_r2, "students"."created_at" AS t1_r3, "students"."updated_at" AS t1_r4 FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id"
 => #<ActiveRecord::Relation [#<Room id: 1, name: "Class 1", created_at: "2016-09-27 03:26:55", updated_at: "2016-09-27 03:26:55">, #<Room id: 2, name: "Class 2", created_at: "2016-09-27 03:26:56", updated_at: "2016-09-27 03:26:56">, #<Room id: 3, name: "Class 3", created_at: "2016-09-27 03:26:57", updated_at: "2016-09-27 03:26:57">]>
2.2.0 :026 > Room.eager_load(:students).where(name: 'Student 1')
  SQL (0.1ms)  SELECT "rooms"."id" AS t0_r0, "rooms"."name" AS t0_r1, "rooms"."created_at" AS t0_r2, "rooms"."updated_at" AS t0_r3, "students"."id" AS t1_r0, "students"."name" AS t1_r1, "students"."room_id" AS t1_r2, "students"."created_at" AS t1_r3, "students"."updated_at" AS t1_r4 FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id" WHERE "rooms"."name" = ?  [["name", "Student 1"]]
 => #<ActiveRecord::Relation []>

Sự khác nhau giữa includes và eager_load

Chúng ta xét ví dụ dưới đây để thấy sự khác nhau này.

2.2.0 :027 > Room.includes(:students).each{|room| puts room.students.map(&:name).join(',') }
  Room Load (46.2ms)  SELECT "rooms".* FROM "rooms"
  Student Load (0.2ms)  SELECT "students".* FROM "students" WHERE "students"."room_id" IN (1, 2, 3)
Students 1-1,Students 1-2,Students 1-3,Students 1-4,Students 1-5
Students 2-1,Students 2-2,Students 2-3,Students 2-4,Students 2-5
Students 3-1,Students 3-2,Students 3-3,Students 3-4,Students 3-5
 => [#<Room id: 1, name: "Class 1", created_at: "2016-09-27 03:26:55", updated_at: "2016-09-27 03:26:55">, #<Room id: 2, name: "Class 2", created_at: "2016-09-27 03:26:56", updated_at: "2016-09-27 03:26:56">, #<Room id: 3, name: "Class 3", created_at: "2016-09-27 03:26:57", updated_at: "2016-09-27 03:26:57">]
2.2.0 :028 > Room.eager_load(:students).each{|room| puts room.students.map(&:name).join(',') }
  SQL (0.3ms)  SELECT "rooms"."id" AS t0_r0, "rooms"."name" AS t0_r1, "rooms"."created_at" AS t0_r2, "rooms"."updated_at" AS t0_r3, "students"."id" AS t1_r0, "students"."name" AS t1_r1, "students"."room_id" AS t1_r2, "students"."created_at" AS t1_r3, "students"."updated_at" AS t1_r4 FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id"
Students 1-1,Students 1-2,Students 1-3,Students 1-4,Students 1-5
Students 2-1,Students 2-2,Students 2-3,Students 2-4,Students 2-5
Students 3-1,Students 3-2,Students 3-3,Students 3-4,Students 3-5
 => [#<Room id: 1, name: "Class 1", created_at: "2016-09-27 03:26:55", updated_at: "2016-09-27 03:26:55">, #<Room id: 2, name: "Class 2", created_at: "2016-09-27 03:26:56", updated_at: "2016-09-27 03:26:56">, #<Room id: 3, name: "Class 3", created_at: "2016-09-27 03:26:57", updated_at: "2016-09-27 03:26:57">]

Hai cách đều cho kết quả giống nhau, tuy nhiên cách thức truy vấn và tốc độ xử lý thỳ khác nhau hoàn toàn. Tùy từng mục đích sẽ chọn được cách thức phù hợp. Về phía mình vẫn nghĩ rằng includes sẽ mềm dèo và tốt hơn eager_load.

Joins

Những đặc điểm chủ yếu của joins:

  • Chỉ sinh ra một câu query duy nhất.
  • Khác với eager_load dử dụng LEFT OUTER JOIN joins sử dụng INNER JOIN
  • Joins sẽ sinh ra những bản ghi giống nhau bị lặp lại.

Xét câu truy vấn sau để thấy sự khác nhau.

2.2.0 :002 > Room.joins(:students).count
   (0.2ms)  SELECT COUNT(*) FROM "rooms" INNER JOIN "students" ON "students"."room_id" = "rooms"."id"
 => 15
2.2.0 :003 > Room.eager_load(:students).count
   (0.2ms)  SELECT COUNT(DISTINCT "rooms"."id") FROM "rooms" LEFT OUTER JOIN "students" ON "students"."room_id" = "rooms"."id"
 => 3

Kết Luận

Chắc hẳn các bạn cũng đã có cái nhìn cơ bản về sự khác nhau của những phương pháp trên và có sự đánh giá, so sánh cơ bản về chúng. Mỗi một phương pháp có những đặc điểm riêng phù hợp với từng mục đích sử dụng. Hy vọng thông qua bài viết các bạn có thể lựa chọn được phương pháp phù hợp nhất với mình.

_Thanks you for reading!!!