Truy vấn dữ liệu với ActiveRecord - Truy vấn với quan hệ belongs_to

Chúng ta sẽ bắt đầu bằng cách thảo luận về các kỹ thuật nâng cao để truy vấn các đối tượng trong quan hệ belong_to.

Các model mẫu

Nếu bạn đang theo dõi loạt bài này, bạn có thể có các ActiveRecord model và các quan hệ giữa chúng. Để làm mẫu cho bài viết, tôi sẽ sử dụng một ví dụ khá đơn giản và quen thuộc:

class Person < ActiveRecord::Base
  belongs_to :role
end

class Role < ActiveRecord::Base
  has_many :people
end

Trong suốt chuỗi bài viết này, bạn sẽ thấy ví dụ về cách sử dụng các phương thức của ActiveRecord để truy vấn cơ sở dữ liệu của bạn:

Person.all

đồng thời với đó, chuỗi truy vấn SQL sinh ra:

SELECT "people".*
FROM "people";

và kết quả dữ liệu trả về:

id name role_id
1 Wendell 1
2 Christie 1
3 Eve 2

Như bạn có thể thấy, ứng với mỗi people trong bảng có một role_id, tương ứng với khóa chính của một hàng trong bảng role. Ngoài ra, mỗi role có một name và một cột boolean là billable.

Tìm tất cả những people thuộc về một role billable

Thử thách đầu tiên của chúng tôi: Tìm tất cả những people thuộc về một role billable.

Thách thức này, và hầu hết các thách thức trong khóa học này, có thể dễ dàng được giải quyết bằng phương thức khá cũ của Ruby là Enumerable, nhưng mục tiêu của chúng tôi là để tìm hiểu làm thế nào để truy vấn được cơ sở dữ liệu thông qua các phương thức của ActiveRecord. Điều này sẽ thu được nhiều hiệu quả hơn kiểu như:

# This works, but is not optimal
Person.all.select { |person| person.role.billable? }

Hầu hết chúng ta đã làm như vậy trước đây, nhưng nó không thực sự là tối ưu do một vài lý do:

  • Có thể ứng dụng truy vấn vào cơ sở dữ liệu hàng trăm, hàng ngàn hoặc hàng triệu lần (đối với mỗi people, hệ thống đang thực hiện một truy vấn đến bảng role).
  • Ứng dụng lấy nhiều dữ liệu từ cơ sở dữ liệu hơn mức nó thực sự cần (chúng ta không thực sự quan tâm về các thuộc tính khác của role, chúng ta chỉ muốn có một danh sách chọn lọc của people).
  • Ứng dụng sử dụng quá nhiều bộ nhớ tạm ActiveRecord để lưu đối tượng Role trong khi không thực sự cần thiết.

Kết nối các bảng sử dụng phương thức join

Công cụ đầu tiên chúng ta sẽ tìm hiểu để cải thiện tình trạng này là phương thức join của ActiveRecord. Phương thức này cho phép chúng ta báo với ActiveRecord để thực hiện một lệnh join SQL trên các quan hệ của nó:

Person.all.joins(:role)

và câu lệnh SQL sẽ được tạo ra như sau:

SELECT "people".*
FROM "people"
INNER JOIN "roles"
  ON "roles.id" = "people"."role_id";

và dữ liệu trả về như sau:

id name role_id id name billable
1 Wendell 1 1 Developer t
2 Christie 1 1 Developer t
3 Wendell 2 2 Manager f

Dữ liệu có sẵn và dữ liệu trả về

Các dữ liệu trên bên phải trong dữ liệu trả về là từ bảng role, và đã được join với bảng people (map giữa khóa chính và khóa ngoại).

Những cột dữ liệu có sẵn trong cơ sở dữ liệu được truy vấn lại, nhưng sẽ không bị gửi lại ứng dụng để tạo ra các đối tượng ActiveRecord.

Điều này giúp cải thiện tốc độ truy vấn, thời gian xử lý và sử dụng bộ nhớ: tốt hơn nhiều so với giải pháp cũ của Ruby.

Lọc dữ liệu với phương thức where

Hiện tại chúng ta đã có các cột của role đã được join với các cột của people, chúng ta có thể lọc danh sách này theo cách mà chúng ta muốn bằng cách sử dụng phương thức where của ActiveRecord:

Person.all.joins(:role).where(roles: { billable: true })

và câu lệnh SQL sẽ được tạo ra như sau:

SELECT "people".*
FROM "people"
INNER JOIN "roles"
  ON "roles.id" = "people"."role_id"
WHERE "roles"."billable" = 't';

và dữ liệu trả về như sau:

id name role_id id name billable
1 Wendell 1 1 Developer t
2 Christie 1 1 Developer t

Ổn rồi! Chúng ta đã đạt được mục tiêu đã đặt ra: ứng dụng chỉ truy vấn đúng 1 lần vào cơ sở dữ liệu, và chỉ tạo ra các đối tượng ActiveRecord mà chúng ta cần - hai đối tượng People và không có đối tượng Role nào được tạo ra.

Chuẩn hóa SQL của ActiveRecord

Chúng ta sử dụng Postgres cho các ví dụ này, SQL là chuẩn trên cơ sở dữ liệu quan hệ nhưng cũng không phải hoàn toàn như vậy. Đặc biệt, trong ví dụ này, 't' có nghĩa là true, nhưng cơ sở dữ liệu khác Postgres có thể có cú pháp hơi khác chút.

May mắn thay, ActiveRecord sẽ xử lý các tiểu tiết phiền phức này cho bất cứ cú pháp SQL mà cơ sở dữ liệu đang dùng yêu cầu.

Tách các quan hệ bằng phương thức merge

Tất cả những gì chúng ta vừa làm có logic như thuộc tính billable chỉ tồn tại trong Role chứ không phải là trong People.

Đầu tiên, hãy định nghĩa một phương thức trả về các mối quan hệ mà chúng ta muốn:

class Role < ActiveRecord::Base
  def self.billable
    where(billable: true)
  end
end

Và bây giờ, khi chúng ta truy vấn people, chúng ta có thể sử dụng phương thức merge của ActiveRecord để tận dụng mối quan hệ này:

Person.joins(:role).merge(Role.billable)

Phương thức này tạo ra câu truy vấn SQL giống như trong giải pháp đầu tiên, nhưng có nâng cao hơn là tách các quan hệ rõ ràng hơn. Lưu ý rằng chúng ta vẫn cần phải join với bảng role, nếu không phương thức merge sẽ báo lỗi như sau:

PG:UndefinedTable: ERROR: missing FROM-clause entry for table "roles"
LINE 1: SELECT * FROM "people" WHERE "roles"."billable" = 't'
                                     ^
: SELECT * FROM "people" WHERE "roles"."billable" = 't'

Hoàn thiện giải pháp

Bây giờ chúng ta có thể đóng gói giải pháp hoàn chỉnh trong phương thức:

class Person < ActiveRecord::Base
  def self.billable
    joins(:role).merge(Role.billable)
  end
end

Vậy là xong! Bây giờ chúng ta có thể truy vấn các people billable một cách rõ ràng và hiệu quả :

Person.billable

câu lệnh SQL sẽ được tạo ra như sau:

SELECT "people".*
FROM "people"
INNER JOIN "roles"
  ON "roles.id" = "people"."role_id"
WHERE "roles"."billable" = 't';

và dữ liệu trả về như sau:

id name role_id id name billable
1 Wendell 1 1 Developer t
2 Christie 1 1 Developer t

Đây chính là cách truy vấn với quan hệ belong_to. Chúc các bạn thành công.

Các bạn có thể theo dõi các bài khác trong loạt bài trên:

Truy vấn dữ liệu với ActiveRecord

Truy vấn dữ liệu với ActiveRecord - Phần giới thiệu

All Rights Reserved