SQL文を読み解こう②(Pundit, Simple_form,FormObject,scope)

SQL文を読み解こう①の続きです。

 

  1. simple_formって何
  2. いやpluckって何
  3. controllerの挙動どうなってるん?
  4. Punditて何 ⇦ここから!
  5. FormObjectて何
  6. ほんでscopeて何なんw

4. Punditて何

Punditとは

管理権限に基づいて、ユーザーにできることを制限するためのgem

Policyと言うクラスが自動で生成され、例えばCRUD処理の末尾に?をつけることで、ユーザーのidによってviewに編集機能や削除機能を出し入れすることができます。

今回はArticlePolicyと言うクラスが作られています。ArticlePolicyはApplicationPolicyのメソッドを継承しているため、def edit?にtrueを記述するだけで、編集権限を付与することができます。

# Policy (punditを使って権限を管理するクラス)
class ArticlePolicy < ApplicationPolicy
  def edit?
    true
  end
end

class ApplicationPolicy
 //セッター
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope
    end
  end
end

例えば、userが管理者(admin)であれば編集画面を表示させ、そうでない場合は、表示させたくないと言う処理を記述したいときは以下の通りにするだけで大丈夫です。

tbody
        - @articles.each do |article|
          tr
            td = article.id
            td
              div = article.title
              div
                - if policy(article).edit?
                  => link_to edit_admin_article_path(article.uuid), class: %w[btn btn-default btn-xs btn-flat]
                    i.fa.fa-edit
                    '
                    | 編集

policy(article).edit?は、ArticlePolicyのedit?メソッドに該当する? a.k.a あなた編集権限ある? a.k.a あんたのuuidにadminという、enumで設定した管理者権限が付与されてはります?

と言う意味になります。

なんで= f.input :tag_idsとすると、article_tagsとtag_idがjoinされるの?

実は前もって定義されているんですね。

まず、formにバリデーションを効かせるために、FormObjectという概念が使われているんです。

5. FormObjectて何

FormObjectとは

Ruby on Railsデザインパターン「設計手法」の1つです。

元々modelにバリデーション等様々な処理が書かれているものを、form層に切り出すことで多くのmodelで同一の処理を使えるなど、拡張性の高いコードを実現させることができるもの。

今回は下記のように、FormObjectが使われている。(tag_idの部分だけを抜粋)

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :tag_id, :integer

 def search
  relation = relation.by_tag(tag_id) if tag_id.present?
  relation
end 

end

今回のように複数の値に対してバリデーションを効かせたい場合、あちこちにバリデーションを効かせるロジックを書いてしまうと、どのコードが実際に動いているのかわからなくなってしまいますよね。

僕は実際に開発現場に入ったことはないですが、ロジックを一つの場所に集約させておくことで、後任の人が「なんかこのコード臭うぞ?」と思うことを幾らか防げるみたいなんです。

技術負債を防ぐってやつですね。

フォームに複数の値を保存させたいときはActiveModelを継承させる下記コードを追加します。

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes

これで、ActiveRecord特有のメソッドである、validateや〇〇.all,〇〇.find_by等が使えるようになりました。

6. ほんでscopeて何なんw

by_tag(tag_id)

ここで謎のコードby_tagが追加されています。引数にtag_idをとっていますね。これが先に述べた、= f.input :tag_idsとすると、article_tagsとtag_idがjoinされる理由になります。

これ、article.rbでscopeを使って定義されているんですね。

scopeとは?

スコープでは、where、joins、includesなど、これまでに登場したすべてのメソッドを使用できます。どのスコープメソッドも、常にActiveRecord::Relationオブジェクトを返します。

(https://railsguides.jp/active_record_querying.html)

事前にarticle.rbで下記のように定義しておくことで、他のモデルやコントローラで呼び出した時にコードをスッキリさせることができます。

article.rb

**scope :by_tag, ->(tag_id) { joins(:article_tags).where(article_tags: { tag_id: tag_id}) }**

さて、上記のようなプロセスを踏むことで、冒頭に記載したSQL文が発行されるに至るんですね。

生成されるSQL

SELECT "taxonomies"."id" 
FROM "taxonomies" 
INNER JOIN "article_tags" 
ON "taxonomies"."id" = "article_tags"."tag_id" 
WHERE "taxonomies"."type" IN ('Tag') 
AND "article_tags"."article_id" = ?  [["article_id", 21]]

たった数行のSQL文を理解するために下記のようなことを学ぶことができました。

・INNER JOIN句

・IN句

・Pluckメソッド

・Pundit gem

・Simple_form gem

・FormObject

・scope

今後も学習を積み重ねていきたいです。

Form Object という選択肢を検討してみる

Active Record クエリインターフェイス - Railsガイド

pluck | Railsドキュメント

SQLでIN句を使おう!基本からサブクエリ活用方法まで一覧紹介 | 侍エンジニアブログ

【Rails】簡単にformが作成できるsimple_form - おぴよの気まぐれ日記

GitHub - heartcombo/simple_form: Forms made easy for Rails! It's tied to a simple DSL, with no opinion on markup.