Preload, Eagerload, Includes and Joins
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, ...
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, references và joins. 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
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
- Blog.bigigbinary.com
- http://api.rubyonrails.org/v5.1/classes/ActiveRecord/Base.html