+5

Custom Ransack Matcher

Ransack hay ActiveRecord sử dụng abstract syntax tree (AST) để compose query thay vì build query trực tiếp. AST sẽ bao gồm các node (Arel::Nodes::Node) chứa thông tin để tạo thành từng SQL fragment. Mỗi node được tạo nên bởi hai thành phần chính là attributepredicate. Hiểu được điều này, chúng ta có thể chuyển hầu hết các case sử dụng query thuần sang AST thông qua việc định nghĩa các node tương ứng.

Ransack matcher

Để hiểu hơn về cách mà ransack hoạt động, chúng ta sẽ phân tích các thành phần trong một matcher thông qua ví dụ sau:

User.ransack(first_name_cont_any: "Rya").result.to_sql

# SELECT "users".* FROM "users"  WHERE ("users"."first_name" LIKE '%Rya%')

Ở đây matcher là first_name_cout_any trong đó:

  • first_name: là attribute, hay là column trong table
  • cont: là predicate, xác định condition sẽ được sử dụng
  • any: là biến thể của predicate. Phần này là optional, tuỳ thuộc predicate có hỗ trợ hay không.

Ransack sẽ parse matcher thành các phần như ở trên, sau đó sẽ tạo node tương ứng trong AST. Vì vậy, nếu muốn thêm một custom matcher, ta sẽ cần define đầy đủ các thành phần của nó.

Custom ransacker

Thực tế, không phải lúc nào yêu cầu cũng chỉ là search theo một attribute như trong ví dụ trên. Trường hợp cần kết hợp nhiều attributes khác nhau trong một điều kiện search, đó sẽ là lúc ransacker được sử dụng. Hãy cùng xem ví dụ sau:

User.where("CONCAT_WS('', first_name, last_name) LIKE '%Rya%'").to_sql

# SELECT "users".* FROM "users" WHERE (CONCAT_WS('', first_name, last_name) LIKE '%Rya%')

Nếu muốn sử dụng ransack trong trường hợp này, ta cần định nghĩa một ransacker trong model User:

ransacker :full_name do
  Arel.sql("CONCAT_WS(' ', first_name, last_name)")
end

Sau đó, có thể kết hợp ransacker ở trên với các predicates mặc định để search như bình thường:

User.ransack(full_name_cont: "Rya").result.to_sql

# SELECT "users".* FROM "users" WHERE (CONCAT_WS(' ', first_name, last_name) LIKE '%Rya%')
User.ransack(full_name_matches: "Rya").result.to_sql

# SELECT "users".* FROM "users" WHERE (CONCAT_WS(' ', first_name, last_name) LIKE 'Rya')

Như vậy, ransacker ở đây chính là cách chúng ta định nghĩa attrbiute của một Ransack matcher.

Custom predicate

Ở trên, chúng ta vừa sử dụng ransacker để định nghĩa thành phần attribute trong một Ransack matcher. Trong trường hợp muốn tạo một custom predicate, bạn cần phải thực hiện các bước sau:

Thêm một predicate mới

Để thêm một predicate mới, ta có thể thêm method vào module Arel::Predications:

module Arel
  module Predications
    def prefix_admin other
      self.eq("[admin] #{other}")
    end
  end
end

Ở trên, chúng ta vừa thêm predicate prefix_admin có chức năng tự động thêm [admin] vào phía trước search value (other). Nghĩa là, nếu bạn nhập Rya thì search value thực tế sẽ là [admin] Rya. Mỗi một predicate sẽ cần phải trả về một Arel::Nodes::Note, chi tiết các node và cách sử dụng bạn có thể xem ở đây.

Khai báo predicate với Ransack

Trước khi có thể sử dụng, predicate cần phải được khai báo với Ransack. Tại đây bạn cũng có thể thêm các config khác liên quan đến format hay validate:

Ransack.configure do |config|
  config.add_predicate("perfix_admin",
    arel_predicate: "perfix_admin",
    formatter: proc {|v| v.titleize},
    validator: proc {|v| v.present?},
    compounds: false
  )
end

Trường hợp muốn sử dụng các predicates mặc định, bạn chỉ cần khai báo với Ransack predicate cần dùng:

Ransack.configure do |config|
  config.add_predicate("upcase",
    arel_predicate: "matches",
    formatter: proc {|v| v.upcase},
    validator: proc {|v| v.present?}
  )
end

Chi tiết các predicates mặc định bạn có thể tham khảo ở đây.

Sử dụng custom predicate

Bây giờ ta có thể sử dụng predicate vừa tạo kết hợp với các attributes mặc định hoặc với ransacker vừa định nghĩa ở trên:

User.ransack(first_name_prefix_admin: "Rya").result.to_sql

# SELECT "users".* FROM "users" WHERE "users"."first_name" = '[admin] Rya'
User.ransack(full_name_prefix_admin: "Rya").result.to_sql

# SELECT "users".* FROM "users" WHERE (CONCAT_WS(' ', first_name, last_name) = '[admin] Rya')
User.ransack(first_name_upcase: "rya").result.to_sql

# SELECT "users".* FROM "users" WHERE "users"."first_name" LIKE 'RYA'

Custom predicate compouds

Mặc định option compounds khi định nghĩa predicate là true. Nghĩa là bạn có thể sử dụng predicate kết hợp với biến thể như all, any. Việc cần làm là thêm predicate của các biến thể tương ứng. Dưới đây là ví dụ:

module Arel
  module Predications
    def concat others
      self.in(others.flatten)
    end

    def concat_any others
      self.in_any(others.flatten)
    end
    
    def concat_all others
      self.in_all(others.flatten)
    end
  end
end

Ở trên chúng ta thêm predicate concat và các biến thể concat_any, concat_all của nó.

Tiếp theo chúng ta sẽ khai báo predicate này với Ransack.

Ransack.configure do |config|
  config.add_predicate("concat",
    arel_predicate: "concat",
    formatter: proc {|v| v.split(",").map(&:strip)},
    validator: proc {|v| v.present?},
    compounds: true,
    type: :string
  )
end

Ở phần formatter ta biến đổi search value từ một chuỗi thành một mảng. Và bây giờ ta có thể search theo các cách sau:

User.ransack(id_concat: "1,2").result.to_sql

# SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2)
User.ransack(id_concat_any: "1,2").result.to_sql

# SELECT "users".* FROM "users" WHERE ("users"."id" IN (1) OR "users"."id" IN (2))
User.ransack(id_concat_all: "1,2").result.to_sql

# SELECT "users".* FROM "users" WHERE ("users"."id" IN (1) AND "users"."id" IN (2))

Conclusion

Vừa rồi chúng ta đã cùng nhau tìm hiểu về cách màn Ransack hoạt động cũng như việc custom các thành phần của nó. Hi vọng bài viết sẽ hữu ích và có thể giúp bạn phần nào trong việc giải quyết các vấn đề lên quan đến chức năng search hay query trong Rails.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.