Preload, Eagerload, Includes and Joins

MỞ ĐẦU

Rails với ActiveRecord giúp đỡ cho lập trình viên rất nhiều trong việc truy xuất dữ liệu từ cơ sở dữ liệu quan hệ, đặc biệt là trong trường hợp cần lấy dữ liệu từ các bảng liên kết với nhau bằng việc cung cấp các method tiện ích. Trong Rails có các method như là preload, eager_load, includes, referencesjoins. Các method này đều có cùng mục đích là load data từ các bảng có quan hệ, tuy nhiên mỗi cách lại có cách thức xử lý khác nhau và phù hợp cho các trường hợp khác nhau. Vì vậy trong bài viết này sẽ giới thiệu các method trên, ưu nhược để có thể ứng dụng phù hợp trong từng hoàn cảnh.

Chuẩn bị model

# app/models/student.rb
class Student < ActiveRecord::Base
  has_many :courses
  has_many :subjects
end

# app/models/course.rb
class Subject < ActiveRecord::Base
  belongs_to :student
  has_many :courses
end

# app/models/subject.rb
class Course < ActiveRecord::Base
  belongs_to :student
  belongs_to :subject
end
class CreateStudents < ActiveRecord::Migration[5.0]
  def change
    create_table :students do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateSubjects < ActiveRecord::Migration[5.0]
  def change
    create_table :subjects do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateCourses < ActiveRecord::Migration[5.0]
  def change
    create_table :courses do |t|
      t.string :name
      t.integer :subject_id
      t.integer :student_id
      t.timestamps
    end
  end
end

def self.setup
  Student.delete_all
  Subject.delete_all
  Course.delete_all

  s = Student.create name: 'Duy'
  sub_ruby = Subject.create name: 'ruby'
  sub_rails = Subject.create name: 'rails'
  sub_ruby = Subject.create name: 'ruby'
  sub_js = Subject.create name: 'js'
  
  s.courses.create! name: 'ruby is awesome', subject_id: sub_ruby.id
  s.courses.create! name: 'rails is awesome', subject_id: sub_rails.id
  s.courses.create! name: 'JavaScript is awesome', subject_id: sub_js.id

  s = Student.create name: 'X'
  s.courses.create! name: 'Javascript is awesome', subject: Subject.create(name: 'Js')

  s = Student.create name: 'User Y'
end

Preload

Preload load dữ liệu quan hệ qua nhiều câu query riêng rẽ. Ví dụ

Student.preload(:courses).to_a

# =>
SELECT "students".* FROM "students"
SELECT "courses".* FROM "courses"  WHERE "courses"."student_id" IN (1, 2, 3, 4, 5)

Đây chính xác cũng là cách thức hoạt động default của includes. Cũng bởi preload luôn sinh ra 2 câu lệnh riêng rẽ để load data, nên ta ko thể sử dụng bảng courses trong điều kiện WHERE được

Student.preload(:courses).where("courses.name='ruby tut'")
# =>
SQLite3::SQLException: no such column: courses.name:
SELECT "students".* FROM "students"  WHERE (courses.name='ruby tut')

Includes

Includes cũng load data bằng 2 câu query riêng biệt như preload, tuy nhiên includes thông minh hơn. Không như preload chết trong trường hợp điều kiện WHERE tham chiếu tới bảng quan hệ, includes xử lý như sau:

Student.includes(:courses).where("courses.name='ruby tut'")
# =>
SELECT "students"."id" AS t0_r0, "students"."name" AS t0_r1, "courses"."id" AS t1_r0,
       "courses"."name" AS t1_r1,
       "courses"."student_id" AS t1_r2, "courses"."subject_id" AS t1_r3
FROM "students" LEFT OUTER JOIN "courses" ON "courses"."student_id" = "students"."id"
WHERE (courses.name = "ruby tut")

Như ta thấy, includes chuyển từ 2 câu query thành 1 câu duy nhất dùng LEFT OUTER JOIN và có thể sử dụng được câu lệnh WHERE bình thường. Như vậy trong trường họp default, đon giản thì includes vẫn sử dụng 2 câu query, tuy nhên nếu ta muốn ép includes chỉ sử dụng 1 câu query duy nhất, lúc đó cần sử dụng references

Student.includes(:courses).references(:courses)
# =>
SELECT "students"."id" AS t0_r0, "students"."name" AS t0_r1, "courses"."id" AS t1_r0,
       "courses"."name" AS t1_r1,
       "courses"."student_id" AS t1_r2, "courses"."subject_id" AS t1_r3
FROM "students" LEFT OUTER JOIN "courses" ON "courses"."student_id" = "students"."id"

Eager load

earger_load thì load data association trong một câu lệnh query duy nhất sử dung LEFT OUTER JOIN - cũng chính là những gì includes thực hiện khi điều kiện where tham chiếu tới thuộc tính của bảng courses

Student.eager_load(:courses)
# =>
SELECT "students"."id" AS t0_r0, "students"."name" AS t0_r1, "courses"."id" AS t1_r0,
       "courses"."name" AS t1_r1,
       "courses"."student_id" AS t1_r2, "courses"."subject_id" AS t1_r3
FROM "students" LEFT OUTER JOIN "courses" ON "courses"."student_id" = "students"."id"

Joins

Join load association data sử dụng INNER JOIN

Student.joins(:courses)
# =>
SELECT "students".* FROM "students" INNER JOIN "courses" ON "courses"."student_id" = "students"."id"

Trong câu query trên thì không lấy dữ liệu từ bảng courses nên sẽ gây ra hiện tượng trùng lắp data.

#<Student id: 9, name: "Duy">
#<Student id: 9, name: "Duy">
#<Student id: 9, name: "Duy">
#<Student id: 10, name: "X">

Để tránh duplicate, ta dùng thêm từ khóa distinct

Student.joins(:courses).select('distinct students.*').to_a

Và nếu cần sử dụng attributes của bảng courses, ta phải khai báo trong câu lệnh select

records = Student.joins(:courses).select('distinct students.*, courses.name as courses_name').to_a
records.each do |student|
  puts student.name
  puts student.courses_name
end

KẾT LUẬN

Trên đây mình đã tổng hợp và giới thiệu một số cách load data association khi làm việc với ActiveRecord trong Rails. Hy vọng bài viết giúp đỡ được phần nào trong quá trình phát triển phần mềm của các bạn!!!

Nguồn tham khảo

  1. Blog.bigigbinary.com
  2. http://api.rubyonrails.org/v5.1/classes/ActiveRecord/Base.html