Một số thay đổi trong Active Record của Rails 6

1. ActiveRecord::Relation#pick

Trước Rails 6 Pluck được sử dụng để lấy ra một mảng của attribute value khi query, giúp giảm step loop qua từng phần tử trong mảng ActiveRecord

Pluck.first sẽ lấy ra phần tử đầu tiên của mảng.

User.limit(1).pluck(:name).first
# SELECT "users"."name" FROM "users" LIMIT ? [["LIMIT", 1]]
# => "David"
User.where(id: 1).pluck(:name).first
# SELECT "users"."name" FROM "users" WHERE "users"."id" = $1
# => "David"

Rails 6 Pick được sử dụng thay Pluck.first với mục đích là lấy ra phần tử đầu tiên của mảng

User.pick(:name)
# SELECT "users"."name" FROM "users" LIMIT ? [["LIMIT", 1]]
# => "David"

User.where(id: 5).pick(:name, :city)
# SELECT "users"."name", "users"."city" FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]]
# => ["Naiya", "Goa"]

2. ActiveRecord::Base.create_or_find_by/!

create_or_find_by rails 6 là method thay cho find_or_create_by khi nó thay đổi thứ tự từ select->insert thành insert->select để tránh race condition khi sử dụng find_or_create_by trong các ứng dụng cần high scale.

Nếu một bản ghi đã tồn tại với các ràng buộc duy nhất, sẽ đưa ra một ngoại lệ giống việc sử dụng find_by. Giả sử có 2 bảng với ràng buộc như sau

class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :title, index: { unique: true }

      t.timestamps
    end
  end
end
class Post < ApplicationRecord
  validates :title, presence: true
end

Sử dụng create_or_find_by

Raise ngoại lệ với create_or_find_by!

3. Enum

Enum được sử dụng nhiều để đặt tên cho scope. Trong Rails 6 để đặt tên cho scope với ý nghĩa lấy ra phủ định ta có thể sử dụng not_*

class Feed < ActiveRecord::Base
  enum status: %i[ active pending trashed ]
end

Feed.not_active # => where.not(status: :active)
Feed.not_pending  # => where.not(status: :pending)
Feed.not_trashed # => where.not(status: :trashed)

4. update_columns

update_columns trong Rails được sử dụng để update record mà bỏ qua các điều kiện (validate) của thuộc tính.

Rails 6 khi sử dụng update_columns cho thuộc tính không tồn tại sẽ raise lên lỗi ActiveModel::MissingAttributeError.

Ở các phiên bản rails 4 hay rails 5 thì trong trường hợp này sẽ raise lên lỗi ActiveRecord::StatementInvalid

# Rails 6
> User.first.update_columns(office_email: '[email protected]')

SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]

Traceback (most recent call last):
        1: from (irb):1
ActiveModel::MissingAttributeError (can't write unknown attribute `office_email`)
# Rails 5.2
> User.first.update_columns(office_email: '[email protected]')
SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
UPDATE "users" SET "office_email" = $1 WHERE "users"."id" = $2  [["office_email", "[email protected]"], ["id", 1]]

=> Traceback (most recent call last):
        1: from (irb):8
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column "office_email" of relation "users" does not exist)
LINE 1: UPDATE "users" SET "office_email" = $1 WHERE "users"."id" = $2
                           ^
: UPDATE "users" SET "office_email" = $1 WHERE "users"."id" = $2

5. delete_by and destroy_by

Rails có find_or_create_by, find_by và các phương thức tương tự để tìm giá trị đầu tiên và tạo nếu không tìm thấy bản ghi khớp với các tham số đã cho.

Rails bị thiếu tính năng tương tự để xóa/hủy một record phù hợp điều kiện, ở các bản Rails trước chỉ hỗ trợ phương thức destroy và delete_all để xóa tất cả các record.

Sử dụng destroy và delete_all để xóa toàn bộ bản ghi phù hợp điều kiện cho trước
  User.find_by(email: "[email protected]").destroy
  User.where(email: "[email protected]", rating: 4).destroy_all

  User.find_by(email: "[email protected]").delete
  User.where(email: "[email protected]", rating: 4).delete_all

Rails 6 đã bổ sung việc xóa/hủy một record được tìm thấy với điều kiện cho trước với 2 phương thức là delete_by and destroy_by

Cú pháp:
model_name.delete_by(attr_name: 'value')
model_name.destroy_by(attr_name: 'value')
Sử dụng destroy_by và delete_by để xóa bản ghi phù hợp điều kiện cho trước
  User.destroy_by(email: "[email protected]")
  User.destroy_by(email: "[email protected]", rating: 4)

  User.delete_by(email: "[email protected]")
  User.delete_by(email: "[email protected]", rating: 4)

6. ActiveRecord::Relation#touch_all

Trong Rails, phương thức touch được sử dụng để update trường updated_at từ thời gian mặc định để current time. Không có validate và chỉ có các callback after_touch, after_commit, and after_rollback được thực hiện.

Rails 6 đã thêm phương thức touch_all trong ActiveRecord::Relation để touch vào nhiều bản ghi cùng một lúc.

Trước Rails 6, cần lặp lại trên tất cả các bản ghi bằng cách sử dụng một trình vòng lặp. Kết quả sẽ trả về một mảng ActiveRecords.

# Rails 5.2
> User.count
SELECT COUNT(\*) FROM "users"
=> 3

> User.all.touch_all
=> Traceback (most recent call last):1: from (irb):2
NoMethodError (undefined method 'touch_all' for #<User::ActiveRecord_Relation:0x00007fe6261f9c58>)

> User.all.each(&:touch)
SELECT "users".* FROM "users"
begin transaction
  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2019-04-20 16:23:31.388028"], ["id", 1]]
commit transaction
begin transaction
  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2019-04-20 16:23:31.418028"], ["id", 2]]
commit transaction
begin transaction
  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2019-04-20 16:23:31.518028"], ["id", 3]]
commit transaction

=> [#<User id: 1, name: "John", email: "[email protected]", rating: 2, created_at: "2019-04-10 16:09:29", updated_at: "2019-04-20 16:23:31">, #<User id: 2, name: "Marry", email: "[email protected]", rating: 4, created_at: "2019-04-10 16:09:43", updated_at: "2019-04-20 16:23:31">, #<User id: 3, name: "Poll", email: "[email protected]", rating: 4, created_at: "2019-04-10 16:09:45", updated_at: "2019-04-20 16:23:31">]

Với Rails 6 thì phương thức touch_all sẽ trả về số lượng của ActiveRecords

# Rails 6
> User.count
SELECT COUNT(*) FROM "users"
=> 3

> User.all.touch_all
UPDATE "users" SET "updated_at" = ?  [["updated_at", "2019-04-20 16:10:40.490507"]]
=> 3

Ngoài ra, touch_all còn lấy custom_time hoặc tham số khác ở các cột như ở ví dụ dưới

User.all.touch_all(time: Time.new(2019, 4, 12, 1, 0, 0))
User.all.touch_all(:created_at)

Cảm ơn các bạn đã theo dõi đến đây!!!

Nguồn tham khảo: https://www.botreetechnologies.com/blog/notable-activerecord-changes-in-rails-6-part-1 https://www.botreetechnologies.com/blog/notable-activerecord-changes-in-rails-6-part-2