Tạo api search sử dụng Grape api và Ransack gem

Đề bài: Tạo 2 model User và Address như schema bên dưới:

Viết một api tìm kiếm user theo các trường: full name, giới tính, email, địa chỉ đường, quận, thành phố

1. Giới thiệu

Để giải quyết bài toán trên, mình sẽ sử dụng 2 gem Grape APIRansack. Trong bài này mình chỉ tập trung giới thiệu về Ransack, Grape API mình sẽ dành cho bài khác

  1. Mô tả

    Ransack is a rewrite of MetaSearch created by Ernie Miller and developed/maintained for years by Jon Atack and Ryan Bigg with the help of a great group of contributors. Ransack's logo is designed by Anıl Kılıç. While it supports many of the same features as MetaSearch, its underlying implementation differs greatly from MetaSearch, and backwards compatibility is not a design goal. (Tiếng anh cho dễ hiểu nhé, mình dịch gà lắm)

  2. Cú pháp

    Cú pháp để gọi search Ransack:

    Model.ransack(query).result
    

    Vậy query này là gì, build ra sao?

    query là một Hash, tập hợp các key-value nhằm mục đích cung cấp cho Ransack các thông tin về trường cần tìm kiếm, dữ liệu cần tìm kiếm và điều kiện cần tìm kiếm

    Key trong query có cấu trúc như thế nào? Cấu trúc chung của các key có dạng <attribute_name>_<operator>.

    attribute_name build như sau

    • Nếu tìm kiếm các trường của Model thì attribute_name chính là tên các trường cần tìm kiếm.
    • Nếu cần tìm kiếm từ các trường của assocical liên kết trực tiếp (Không sử dụng through) thì attribute_name có dạng <assocical_name>_<attr_name_of_assocical>. VD: addresses_id, addresses_street...
    • Nếu cần tìm kiếm các trường từ assocical liên kết gián tiếp (though qua assocical khác) thì attibute_name có dạng <accocical_name_through><assocical_name_focus><attr_name_of_assocical_focus>. VD:
    class Author
        has_many :posts
        has_many :comments, through: posts
    end
    

    thì attibute_name sẽ là posts_comments_id, posts_comments_content...

    Có một key đặc biệt là m, key này có 2 value: or hoặc and. Mục đích của key-value này là nhóm các key-value còn lại theo điều kiện or hoặc and. Ví dụ như tìm người có tên chứa ký tự A hoặc có email là B...bla bla

    operator là các toán tử như cont(chứa), eq(bằng), gt(lớn hơn)... xem danh sách các toán tử ở đây

2. Thực hiện

  1. Cài đặt gems

    Thêm 2 dòng sau vào Gemfile và chạy bundle install để cài đặt chúng

    gem "ransack", github: "activerecord-hackery/ransack"
    gem "grape"
    
  2. Code

    Tạo 2 file migrate cho 2 model User và Address như sau và nhớ chạy rails db:migrate nhé:

    # db/migrate/20181009101901_create_user.rb
    
    class CreateUser < ActiveRecord::Migration[5.2]
      def change
        create_table :users do |t|
          t.string :first_name, null: false
          t.string :last_name, null: false
          t.string :email
          t.string :address
          t.datetime :date_of_birth
          t.string :phone
          t.integer :gender
    
          t.timestamps
        end
      end
    end
    
    # db/migrate/20181009104754_create_address.rb
    
    class CreateAddress < ActiveRecord::Migration[5.2]
      def change
        create_table :addresses do |t|
          t.references :user, foreign_key: true
          t.string :province
          t.string :district
          t.string :street
    
          t.timestamps
        end
      end
    end
    

    Tạo 2 model trên:

    # app/models/user.rb
    
    class User < ApplicationRecord
      has_many :addresses
    
      enum gender: [:male, :female]
    
      def full_name
        "#{last_name} #{first_name}"
      end
    end
    
    # app/models/address.rb
    
    class Address < ApplicationRecord
      belongs_to :user
    end
    

    Như đề bài yêu cầu search full name, do vậy ta cần ghép 2 cột last name và first name lại. Vì data user là người Việt nên mình ghép last name rồi tới first name nhé. Thêm đoạn code sau

    # app/models/user.rb
    
      ....
      ransacker :full_name, formatter: proc{|v| v.mb_chars.downcase.to_s} do |parent|
        Arel::Nodes::NamedFunction.new("lower",
          [Arel::Nodes::NamedFunction.new("concat_ws",
            [Arel::Nodes.build_quoted(" "), parent.table[:last_name], parent.table[:first_name]])])
      end
    

    Cách viết câu query search của Ransack rất dễ hiểu. Để search các trường trực tiếp từ model chỉ cần đặt theo cấu trúc <attr_name>_<operator> (operator như cont-chứa, eq-bằng...), search theo các trường từ assocical chỉ cần gắn thêm assocical name ở phía trước nữa là được. Nhiều khi gắn assocical name với attr name làm cho code thêm dài thườn thượt, Ransack hỗ trợ đặt alias cho chúng ta. Như ví dụ hiện tại sẽ thử luôn với assocical addresses

    # app/models/user.rb
    
      ...
    
      ransack_alias :addr_province, :addresses_province
      ransack_alias :addr_district, :addresses_district
      ransack_alias :addr_street, :addresses_street
    

    Tạo Api search user

    #app/api/v1/user_api.rb
    
    class V1::UserAPI < Grape::API
      resources :users do
        desc "Search user"
        params do
          optional :full_name, type: String
          optional :gender, type: String, values: User.genders.keys
          optional :email, type: String
          optional :street, type: String
          optional :district, type: String
          optional :province, type: String
        end
        get "search" do
          query = Hash.new.tap do |q|
            q[:full_name_cont] = params[:full_name] if params[:full_name].present?
            q[:gender_eq] = params[:gender] if params[:gender].present?
            q[:email_cont] = params[:email] if params[:email].present?
            q[:addr_street_cont] = params[:street] if params[:street].present?
            q[:addr_district_cont] = params[:district] if params[:district].present?
            q[:addr_province_cont] = params[:province] if params[:province].present?
          end
          users = User.includes(:addresses).ransack(query).result
          {result: Entities::V1::User.represent(users)}
        end
      end
    end
    
    
    
  3. Test

    Code xong rồi, test thôi, và đây là kq chạy test

    Tìm kiếm theo tên

    Tìm kiếm theo email

    Tìm kiếm theo giới tính

    Tìm kiếm theo tên đường

    Tìm kiếm theo tên quận

    Note: Nếu kèo source code mình về nhớ chạy rails db:seed để import data nhé

3. Tổng kết

Mình vừa hướng dẫn các bạn cách viết api search sử dụng gem Ransack và Grape api, source ở đây. Chúc các bạn thành công!