Giap Hiep

I'm Giap Hiep

I'm a web developer, a gymer. I enjoy share something i know that help people's work!
Giap Hiep

Những thay đổi trong Rails 6? (Phần II - New Methods)

Bài viết trước Những thay đổi trong Rails 6? (Phần I - New Methods) mình có tổng hợp những method thay đổi trong rails 6. Tuy nhiên vẫn còn khá nhiều method mà Rails 6 đã bổ sung vào bản update này nên mình viết tiếp bài này để tổng hợp thêm cho đầy đủ hơn.

1. Relation#extract_associated

Trước đây, nếu chúng ta muốn trích xuất các record từ một ActiveRecord::Relation, chúng ta sử dụng preloadcollect

Ví dụ, chúng ta muốn lấy ra thông báo của vài users. Câu truy vấn sẽ như sau:

Rails 5.2

> User.where(gender: "male").preload(:notifications).collect(&:notifications)

User Load (1.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`gender` = 0
UserNotification Load (1.4ms)  SELECT `user_notifications`.* FROM `user_notifications` WHERE `user_notifications`.`user_id` IN (1, 2, 4)
Notification Load (1.1ms)  SELECT `notifications`.* FROM `notifications` WHERE `notifications`.`id` IN (1, 2, 3)

Rails 6

#extract_associated được giới thiệu như một cách viết ngắn gọn cho preloadcollect.

Chúng ta thay câu truy vấn ở trên như sau:

User.where(gender: "male").extract_associated(:notifications)

2. Array#extract!

Rails 6 thêm Array#extract loại bỏ và trả về giá trị thỏa mãn điều kiện truyền vào.

Ví dụ:

> numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9]
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9]

> odd_numbers = numbers.extract! { |number| number.odd? }
=> [1, 3, 5, 7, 9, 9]

> numbers
=> [0, 2, 4, 6, 8]

3. ActiveRecord::Relation#touch_all

Trước khi bắt đầu, chúng ta cùng tìm hiểu touch method làm gì? touch được sử dụng để cập nhật update_at bằng thời gian hiện tại. Nó cũng mất thời gian tùy chỉnh hoặc các cột khác như parameters.

Rails 6 thêm touch_all vào ActiveRecord::Relation để sinh ra nhiều records trong một lần chạy. Trước Rails 6, chúng ta cần lặp lại tất cả records bằng cách sử dụng một vòng lặp để đạt được kết quả mong muốn.

Theo dõi ví dụ dưới đây:

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", "2020-03-03 08:15:10.231211"], ["id", 1]]
commit transaction
begin transaction
  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2020-03-03 08:15:10.231211"], ["id", 2]]
commit transaction
begin transaction
  UPDATE "users" SET "updated_at" = ? WHERE "users"."id" = ?  [["updated_at", "2020-03-03 08:15:10.231211"], ["id", 3]]
commit transaction

=> [#<User id: 1, name: "Sam", created_at: "2019-03-05 16:09:29", updated_at: "2019-03-05 17:45:51">, #<User id: 2, name: "John", created_at: "2019-03-05 16:09:43", updated_at: "2019-03-05 17:45:51">, #<User id: 3, name: "Mark", created_at: "2019-03-05 16:09:45", updated_at: "2019-03-05 17:45:51">]

Điều này dẫn đến N + 1 query

Rails 6.0

> User.count
   (0.9ms)  SELECT COUNT(*) FROM `users`
=> 3

> User.all.touch_all
  User Update All (5.8ms)  UPDATE `users` SET `users`.`updated_at` = '2020-03-03 08:15:10.231211'
=> 3

So sánh với ví dụ với touch ở trên, kết quả này dùng 1 query thay vì N + 1 query.

touch_all trả về số lượng bản ghi

touch_all cũng thay đổi thời gian hay các cột khác nhau làm tham số

Tóm lại: touch_all nhanh và đơn giản hơn.

4. Hash#deep_transform_values và Hash#deep_transform_values!

Ruby 2.4, giới thiệu Hash#transform_valuesHash#transform_values! để tranform giá trị của hash. Phương thức này trả về một hash mới với tất cả các giá trị được convert bởi điều kiện cho trước trong block.

  • Hash#transform_values trả về kết quả là một hash thỏa mãn điều kiện cho trước.
  • Hash#transform_values! trả về kết quả là một hash thỏa mãn điều kiện cho trước và cập nhật hash đó.

Ví dụ:

Hash#transform_values

> hash_numbers = {a: "1.2", b: "1.32", c: "5"}
=> {:a=>"1.2", :b=>"1.32", :c=>"5"}

> hash_numbers.transform_values {|a| a.to_i}
=> {:a=>1, :b=>1, :c=>5}

> hash_numbers
=> {:a=>"1.2", :b=>"1.32", :c=>"5"}

Hash#transform_values!

> hash_numbers = {a: "1.2", b: "1.32", c: "5"}
=> {:a=>"1.2", :b=>"1.32", :c=>"5"}

> hash_numbers.transform_values! {|a| a.to_i}
=> {:a=>1, :b=>1, :c=>5}

> hash_numbers
=> {:a=>1, :b=>1, :c=>5}

Ví dụ với nested object với hash và array:

hash_numbers = {a: 1.2, b: 1.32, c: [5, 6.4]}
=> {:a=>1.2, :b=>1.32, :c=>[5, 6.4]}

> hash_numbers.transform_values {|a| a.to_i}
NoMethodError: undefined method `to_i' for [5, 6.4]:Array
Did you mean?  to_s
               to_a
               to_h

Như chúng ta thấy, trong ví dụ thứ 3, khi chúng ta có một nested object với hash và array, thì nó không hoạt động theo cách mà chúng ta mong muốn.

Rails 6 thêm phương thức Hash#deep_transform_valuesHash#deep_transform_values!

  • Hash#deep_transform_values trả về kết quả là một hash thỏa mãn điều kiện cho trước.
  • Hash#deep_transform_values! trả về kết quả là một hash thỏa mãn điều kiện cho trước và cập nhật hash đó.

NOTE: 2 phương thức này chỉ khả dụng trên rails 6.0 bạn có thể kiểm tra trên source code của Rails:
Rails 6.0 , Rails 6.0.2.1

Ví dụ:
Hash#deep_transform_values

> hash_numbers = {a: 1.2, b: 1.32, c: [5, 6.4]}
=> {:a=>1.2, :b=>1.32, :c=>[5, 6.4]}

> hash_numbers.deep_transform_values {|a| a.to_i}
=> {:a=>1, :b=>1, :c=>[5, 6]}

> hash_numbers
=> {:a=>1.2, :b=>1.32, :c=>[5, 6.4]}

Hash#deep_transform_values!

> hash_numbers = {a: 1.2, b: 1.32, c: [5, 6.4]}
=> {:a=>1.2, :b=>1.32, :c=>[5, 6.4]}

> hash_numbers.deep_transform_values! {|a| a.to_i}
=> {:a=>1, :b=>1, :c=>[5, 6]}

> hash_numbers
=> {:a=>1, :b=>1, :c=>[5, 6]}

5. ActionDispatch::Request::Session#dig

Rails 6 thêm phương thức dig cho ActionDispatch::Request::Session. Nó hoạt động giống với Hash#dig Nó trích xuất dữ liệu lồng nhau bởi các keys khi chúng ta muốn truy cập dữ liệu hay nested hashes trong session của mình.

Trước đây, chúng ta có thể làm bằng cách chuyển đổi session sang một hash và sau đó gọi Hash#dig.

Ví dụ:

Rals 5.2

>> session[:user] = {first_name: "Ánh", last_name: "Nguyễn Nhật", email: "abc@example.com", book: {title: "Làm bạn với bầu trời", author: "NNA"}}

=> {:first_name=>"Ánh", :last_name=>"Nguyễn Nhật", :email=>"abc@example.com", :book=>{:title=>"Làm bạn với bầu trời", :author=>"NNA"}}

>> session.to_hash

=> {"session_id"=>"5fe8cc73c822361e53e2b161dnnbb0e47", "_csrf_token"=>"gyFd5nEEkFvWTnl6XeVbJ7kljhL923hJt8PyHVCH/DA=", "return_to"=>"http://localhost:3000", "user"=>{:first_name=>"Ánh", :last_name=>"Nguyễn Nhật", :email=>"abc@example.com", :book=>{:title=>"Làm bạn với bầu trời", :author=>"NNA"}}}


>> session.to_hash.dig("user", :book, :title)

=> "Làm bạn với bầu trời"

Rails 6

>> session[:user] = {first_name: "Ánh", last_name: "Nguyễn Nhật", email: "abc@example.com", book: {title: "Làm bạn với bầu trời", author: "NNA"}}

=> {:first_name=>"Ánh", :last_name=>"Nguyễn Nhật", :email=>"abc@example.com", :book=>{:title=>"Làm bạn với bầu trời", :author=>"NNA"}}

>> session.dig("user", :book, :title)

=> "Làm bạn với bầu trời"

6. Relation#pick

Trước đây, việc lấy giá trị đầu tiên của một cột từ một tập các record khá cồng kềnh.

Ví dụ chúng ta muốn lấy title của tất cả các article có thể loại với id là 2

> Article.where(category_id: 2).limit(1).pluck(:title).first
  SELECT `articles`.`title` FROM `articles` WHERE `articles`.`category_id` = 2 LIMIT 1

=> "Gem Pagy - Làm thế nào để custom HTML pagination trên RoR application?"

Rails 6 thêm ActiveRecord::Relation#pick để cung cấp một cách ngắn hơn để lấy gía trị đầu tiên

> Article.where(category_id: 2).pick(:title)
   SELECT `articles`.`title` FROM `articles` WHERE `articles`.`category_id` = 2 LIMIT 1

=> "Gem Pagy - Làm thế nào để custom HTML pagination trên RoR application?"

Phương thức này áp dụng limit(1) cho quan hệ trước khi chọn giá trị đầu tiên. Vì thế nó rất hữu ích khi quan hệ đã được thu lại thành một hàng.

Ta cũng có thể select nhiều giá trị của nhiều cột.

> Article.where(category_id: 2).pick(:title, :content)
  SELECT `articles`.`title`, `articles`.`content` FROM `articles` WHERE `articles`.`category_id` = 2 LIMIT 1
=> ["Gem Pagy - Làm thế nào để custom HTML pagination trên RoR application?", "Pagy là gem dùng để phân trang"]

7. ActionController::Parameter#each_value

Rails 6 thêm each_value trên ActionController::Parameter

Nó khác với Hash#each_value ở chỗ nó chuyển tất cả các giá trị hash thành đối tượng ActionController::Parameters trước khi đưa các giá trị thành block, thay vì chỉ trả về giá trị.

Ví dụ:
Trước tiên chúng ta định nghĩa ActionController::Parameters cho user như sau:

params = ActionController::Parameters.new({
  name: {
    first: "Mai",
    last: "Dao"
  },
  gender: "Female",
})

=> <ActionController::Parameters {"name"=>{"first"=>"Mai", "last"=>"Dao"}, "gender"=>"Female"} permitted: false>

Bây giờ chúng ta thêm một đối tượng hash khác vào params:

params[:socials] = {
  twitter: "https://twitter.com/DaoMai",
}

=> {:twitter=>"https://twitter.com/DaoMai"}

Bây giờ chúng ta sử dụng với ActionController::Parameters#each_value:

params.each_value do |value|
  puts value.inspect
end

<ActionController::Parameters {"first"=>"Mai", "last"=>"Dao"} permitted: false>
"Female"
<ActionController::Parameters {"twitter"=>"https://twitter.com/DaoMai"} permitted: false>

Như bạn thấy ở trên socials trong params là một Hash. Nhưng trong each_value block nó được convert sang ActionController::Parameters

8. ActiveRecord::Relation#annotate

Rails thêm phương thức annotatecho phép tạo comment trong câu lệnh SQL.

Ví dụ:

name = "Na"
=> "Na"

> User.where("first_name LIKE ?", name).annotate("users with first name starting with #{name}").to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL AND (first_name LIKE 'Na') /* users with first name starting with Na */"

Trên đây là một số thay đổi mình tổng hợp lại. Hy vọng sẽ giúp ích cho các bạn. (bow