ガラシのパルプンテ頼み

地方産限界エンジニアのグローバル独り言

【5分くらいでわかる】 enumの使い方 | Rails入門

enumとは

enumとは一つのカラムに指定した複数個の整数の定数に、それぞれ名前を割り当てるために使われるデータ型です。 広義の意味では enumeration: 列挙型 と訳されます。enumを使うメリットとしては以下のようなものが挙げられます。

  • 意味のある単語を用いてデータを操作できるため整数による操作に比べて意味を理解しやすい
  • 開発者が整数の定数とそれぞれの振る舞いとの組み合わせを覚えておく必要がなくなるため可読性とメンテナンス性が上がる
  • enumの定義により動的に定義されるヘルパーメソッドを使うことでコードの記述量を減らすことができる

それでは具体的な例を挙げながらRailsにおけるenumの使い方を追っていこうと思います。

DBにenumで管理したいカラムを作成する

投稿モデルを持つRailsアプリを例に考えてみます。投稿レコードは一件ずつ以下のような状態を持つこととします。

定数 名前 状態
1 draft 下書き
2 public 公開
3 private 非公開
4 trashed ゴミ箱

マイグレーションは以下の通りです。

# db/migrate/yyyymmddhhmmss_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :name, null: false
      t.text :description
      t.integer :status, null: false, default: 1

      t.timestamps
    end
  end
end

enumを定義する

rails db:migrateでマイグレーションを実行したら、以下のコード例のようにPostモデルにてenumメソッドを使いenumを定義します。 enumメソッドの第1パラメータにはカラムの属性名を渡し、第2パラメータにはpostのステータスとして利用したい名前のリストを渡します。

# app/models/post.rb

class Post < ApplicationRecord
  enum :status, { draft: 1, public: 2, private: 3, trashed: 4 }
end

enumのヘルパーメソッド

冒頭でenumを定義するメリットとして、enumの定義により動的にヘルパーメソッドが作成されることについて触れたかと思います。 以下にそのメソッドを使ってどのようなことができるのかをシーンに合わせて説明していきます。

ステータスの一覧、または一部を取得する

Post.statuses
#=> { "draft" => 1, "public" => 2, "private" => 3, "trashed" => 4 }

Post.statuses[:draft]
#=> 1

Post.statuses["private"]
#=> 3

特定のステータスを持つレコードを検索する(スコープとして用いる)

Post.private
#=> =>   Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."status" = $1  [["status", 3]]
[#<Post:0x0000aaaaeb408e90 id: 1, name: 'test_post', description: 'hoge', status: 3>]

ステータスの照合を行う

例えば post.status == 'draft' のような照合は以下のような?付きのメソッドで置き換えられる。

post.draft?
#=> true

post.public?
#=> false

ステータスの更新を行う

更新に関しても以下のようなコードは!付きのメソッドで置き換えることができる

post.status = 'public'
post.save
post.public!

【5分くらいでわかる】CarrierWaveでファイルをアップロードする方法 | Rails入門

CarrierWaveとは

CarrierWaveとは、Railsアプリケーションにファイルのアップロード機能を追加するためのgemです。 デフォルトの保存先はpublic/uploadsで、設定でS3などの外部ストレージへの保存も可能です。

GitHub github.com

CarrierWaveをローカルで使う

大まかな手順としては以下の通り。 それぞれ順を追って説明していきます。

  1. CarrierWaveをインストールする
  2. アップローダーを作成する
  3. アップロードファイル用のカラムを追加する
  4. アップローダーとカラムを紐づける
  5. アプリケーションから画像を登録する
  6. 画像を表示する

1. CarrierWaveをインストールする

$ gem install carrierwave

or Gemfileに以下を記述してbundle installを実行

gem 'carrierwave', '>= 3.0.0.beta', '< 4.0'

2. アップローダーを作成する

以下のコマンドの実行によりapp/uploaders/配下にAvatarUploaderクラスが作成されます

$ rails g uploader Avatar

アップローダーとは?

CarrierWave経由でアップロードするファイルの保存先、受け付ける拡張子の指定、 画像ならサイズや縦横比のフォーマットなどをあらかじめ指定するクラスです。 アップロード対象となるファイルの種別(画像、CSV、PDFなど)ごとに このアップローダーと後述のアップロード用カラムを用意する必要があります。 尚、アップローダークラスに storage :fileと記載することで、 public/uploads配下にファイルを保存することができます。

3. アップロードファイル用のカラムを追加する

次にアップロードしたファイルの情報を保存するカラムを追加します。

マイグレーションファイルを追加

$ rails g migration add_avatar_to_users avatar:string

マイグレーションを実行

$ rails db:migrate

なぜファイル名のみをDBに保持するのか?

上記で追加したavatarカラムには画像のバイナリではなく、ファイル名を保持します。 理由としては、単純にバイナリをそのまま保存するとサーバーの容量が圧迫されてしまうためです。 avatarカラムには、ファイルを特定するためのファイル名のみを保存して、表示時やダウンロード時に 「ファイルが格納されているパス + DBに保存されているファイル名」で表示を行います。

4. アップローダーとカラムを紐づける

アップロード機能を追加する対象モデルクラスで、3の手順で追加したavatarカラムと 2の手順で追加したAvatarUploaderクラスの紐付けを行います。

class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploader
end

これでファイルアップロード時にAvatarUploaderクラスが利用できるようになりました。

5. アプリケーションから画像を登録する

ユーザー登録画面のviewに記述されている、formにfile_fieldを追加してファイル選択ボックスを作成する。

<%= form.label :avatar %>
<%= form.file_field :avatar %>

users_controllerではストロングパラメーターにavatarカラムを追加することを忘れないようにしましょう。

def user_params
  params.require(:user).permit(:name, :age, :avatar)
end

ファイル選択ボックスから画像を選択し、登録を実行すると、paramsのavatarキーに選択したファイルの情報が入ります。 @original_filenameがavatarカラムに保存され、バイナリは「public/uploads/モデル名/画像のカラム名/id」配下に保存されます。

6. 画像を表示する

アバター画像を表示させるには、まずは画像が保存されている場所(パス)を取得する必要があります。

UserモデルにてAvatarUploaderクラスとavatarカラムを紐づけを行っているので、以下のAvatarUploaderクラスのメソッドを使って 簡単にアップロードしたファイルの情報を取得することが出来ます。

ファイルのURLを取得

userインスタンス.avatar.url

カレントパス取得

userインスタンス.avatar.current_path

ファイル名を取得

userインスタンス.avatar.avatar_identifier

viewでimage_tagと上記メソッドを活用することでアップロードしたファイルの表示が行えます。

<% if @user.avatar? %>
  <p>
    <strong>Avatar:</strong>
    <%= image_tag @user.avatar.url %>
  </p>
<% end %>

表示の仕組み

public以下に配置したファイルは、Railsのデフォルトでブラウザに直接送信されるので、 アドレスバーにpublic以下のパス(url)を指定する事で、その画像をブラウザで表示する事が出来ます。

【5分くらいでわかる】sortメソッドでマルチソートする方法 | Ruby入門

今回やりたいこと

  • sortのブロック内で複数キーを用いたマルチソートを実行したい
  • 項目にnilを含む場合は、nilの項目を末尾に追いやりたい
  • その上で特定のキーで昇順、降順を制御したい

sortの挙動

ブロックとともに呼び出された時には、要素同士の比較をブロックを用いて行います。ブロックに2つの要素を引数として与えて評価し、その結果で比較をします。 ブロックは <=> 演算子と同様に整数を返すことが期待されており、ブロックは第1引数が大きいなら正の整数、両者が等しいなら0、 そして第1引数の方が小さいなら負の整数を返さなければなりません。両者を比較できない時は nil を返却します。

docs.ruby-lang.org

Arrayの<=>メソッド

自身と other の各要素をそれぞれ順に <=> で比較していき、結果が 0 でなかった場合にその値を返却します。各要素が等しく、配列の長さも等しい場合には 0 を返却します。 各要素が等しいまま一方だけ配列の末尾に達した時、自身の方が短ければ -1 を、そうでなければ 1 を返却します。 other に配列以外のオブジェクトを指定した場合は nil を返却します。

docs.ruby-lang.org

  • sortのブロック内で配列同士の比較もできる。
  • 配列の先頭から比較して同値の場合には次の要素を検証していく
  • 同値じゃなければ1または-1を返す

idの降順でソート

  • 降順でのソートの場合はsortにブロックを渡して、二つ目のブロック変数を先にしてあげることで再現が可能です
customer.action_histories.sort |a, b| do 
    b.id <=> a.id
end

nameの昇順、idの降順でソート

  • 第一ソートキーであるnameで比較、そこで同値じゃなければ第二ソートキーは評価せず、右辺の大小に応じて並べ替えます
customer.action_histories.sort |a, b| do 
    [a.name, b.id] <=> [b.name, a.id]
end

日付(Date)の降順でソート

  • Dateクラスにも<=>メソッドが実装されているため考え方は配列と同様です
customer.action_histories.sort |a, b| do 
    b.action_datetime <=> a.action_datetime
end

ソート対象にnilが含まれていて、nilの項目は末尾にソート

  • nilが含まれている属性に対してnil?を実行してtrue/falseに応じて0か1を返します
  • nilの場合は0が返されて、それ以外では1が返される。降順でのソートのため0が返されたnilの項目は末尾に送られます
  • その後idがnilじゃないもの同士は1で同値となるので、第二ソートキーの昇順で並べ替えが行われます
customer.action_histories.sort |a, b| do 
    [b.id.nil? ? 0 : 1, a.action_datetime] <=> [a.id.nil? ? 0 : 1, b.action_datetime]
end

【5分くらいでわかる】ストロングパラメータ is 何 | Rails入門

ストロングパラメータとは

strong parametersは、Action ControllerのパラメータをActive Modelの「マスアサインメント」で利用することを禁止します(許可されたパラメータは除く)。 したがって開発者は、マスアップデートを許可する属性をコントローラで明示的に指定しなければなりません。 strong parametersは、ユーザーがモデルの重要な属性を誤って更新してしまうことを防止するためのより優れたセキュリティ対策です。

引用:Action Controller の概要 - Railsガイド

マスアサインメントとは

RailsにおけるDBの更新処理形で、複数のカラムを一括で指定することが可能です。 フォームから送られてきたパラメーターをひとつにまとめて、一度に保存できます。

User.create(name: 'garasi', age: 28, admin: true)

マスアサインメントの脆弱性

マスアサインメントはフォームから入力された複数の項目をキーバリューとして受け取って、モデル側でそれらをまとめて更新できる便利な機能である反面、 開発者側がなんの考慮もせずにフォームからの情報をマスアサインメントでモデルに渡してしまうと、悪意のあるパラメータを受け付けてしまう可能性があります。

例えばUserモデルにadminという管理者権限の有無を示すフラグが設けられており、アプリの開発者や管理者のみtrueにして、管理者用の画面へのアクセスを許可していたとしましょう。 もし悪意のある第三者がユーザー登録時に、Chromeのdevツールなどを用いてパラメータにadmin: true を付与してリクエストを送信してきた場合、 悪意のある第三者に管理者権限を持ったアカウントが発行されてしまい、アプリケーションが危険に晒されてしまう危険性があります。

改めてストロングパラメータとは

フォームから渡ってきたパラメータにコントローラー側であらかじめ許可する項目を指定して、 フォーム上から悪意のあるパラメータが送られてきた場合に開発者側で許可したカラム以外の更新を防ぐことを可能にしたもの。 要するに安全が保証されたパラメータをマスアサインメントでモデルに渡すためのRailsのセキュリティ機構と言えるでしょう。

具体的な使い方

一次元構造のハッシュ、二次元構造のハッシュでそれぞれ以下のように定義する必要があります

#一次元ハッシュの場合
params.permit(:キー名)

# 二次元ハッシュの場合
params.require(:モデル名).permit(:キー名)

パラメータを受け取るコントローラーのprivateメソッドとして定義することが多いです

 def create
    user = User.new(user_params) # =>{ name: "garasi", age: 28 }と同義
    if user.save
        redirect_to root_path, notice: '登録が完了しました'
    else
        render :new
    end
 end

 private
 # 例:名前と年齢以外を受け取らないため管理者権限など重要な項目を変更されることがない
 def user_params
  params.require(:user).permit(:name, :age)
 end

requireとpermit

コントローラー側で送られてきたパラメータを出力すると以下のようなハッシュが渡ってきていることがわかります。 以下の場合requireメソッドを使ってキーがuserの値のみを読み込み、その中で更にpermitメソッドにより許可する属性名を指定しています。

# Parameters
=> <ActionController::Parameters {
  "utf8"=>"", 
  "authenticity_token"=>"<token>", 
  "user"=>{"name"=>"garasi", "age"=>"29"}, 
  "commit"=>"送信", 
  "controller"=>"users", 
  "action"=>"create"
} permitted: false>

# controller
 def user_params
  params.require(:user).permit(:name, :age)
 end

これらを踏まえて再度ストロングパラメータの指定コードを見てみると以下のように読み取ることができます。 1. params.require(:user)ではparamsからキーがuserのバリューのみを受け取る 2. permit(:name, :age)をメソッドチェーンすることによってそのうちnameとageのカラムのみをモデルに受け渡すことを許可している

まとめ

  • モデルには複数の値を一括で受け取って更新できるマスアサインメントという更新処理系が存在する。
  • マスアサインメントには脆弱性があり、開発者がなにも考慮しないと悪意のある第三者に管理者権限を奪取されたり、アプリケーションのセキュリティ的に不都合な値を変更される可能性がある。
  • この脆弱性に対応するため、フォームからの受け取りを許可する項目をあらかじめ指定して安全な値のみをマスアサインメントでモデルに受け渡す必要がある。
  • この受け取る内容を制限したパラメータがストロングパラメータ。
  • ストロングパラメータはviewから送られてきたparamsにrequireメソッドとpermitメソッドで値を受け取るキーとバリューを指定することで作成できる。

【5分くらいでわかる】form_withの使い方とfrom_for、form_tagとの違いについて | Rails入門

form_withとは

form_withとは、railsで情報を送信するためのヘルパーメソッド。 form_withを使うことにより、簡単に入力フォームに必要なHTMLを作成することができます。

api.rubyonrails.org

特定のコントローラーで任意のデータを受け取りたい or 受け取ったデータを永続化しない

  • 特定の画面のフォームから送られてきた値を任意のコントローラーで利用したい場合
  • または関連するモデルが存在しない = 送られてきた値を特定のモデルに永続化しない場合

これらの場合は、以下のようにurlオプションからコントローラーを指定します。 必要に応じてmethodオプションからHTTPメソッド(=アクション)を指定することも可能です。

<%= form_with url: users_path, method: :get do |form| %>
  <!--フォーム内容 -->
<% end %>

生成されるHTML

<form action=”/users” method=”get”>
・・・
</form>

送られてきたデータをDBに保存して永続化したい

  • 関連するモデルが存在する = フォームから送られてきたparamsをDBに保存したい場合

この場合は、保存したい対象のモデルをmodelオプションに指定します。 methodオプションは指定がなかった場合POSTがデフォルトのHTTPメソッドとして選択されます。

<%= form_with model: User.new do |form| %>
  <%= form.text_field :name %>
  <%= form.number_field :age %>
  <%= form.submit %>
<% end %>

生成されるHTML

<form action=”/users” method=”post”>
    <input type="text" name="user[name]" id="user[name]">
    <input type="number" name="user[age]" id="user[age]">
    <input type="submit" name="commit" value="保存する" data-disable-with="保存する"> 
</form>

渡ってくるparams

params
=> <ActionController::Parameters {
  "utf8"=>"✓", 
  "authenticity_token"=>"<token>", 
  "user"=>{"name"=>"garasi", "age"=>"29"}, 
  "commit"=>"送信", 
  "controller"=>"users", 
  "action"=>"create"
} permitted: false>

modelオプションが指定されている場合のデフォルトの挙動について

  • 該当のモデルオブジェクトから特定したcontroller・アクションに対するPOSTリクエストの送信
  • 渡されたインスタンスが空ならPOST(create), IDを持っていればPATCH(update)がHTTPメソッドとして指定される(methodオプションで任意のHTTPメソッドを指定することも可能)
  • ActiveRecordと連携してモデル名とモデルの属性名を、htmlのinput要素にあるname属性として付与する

ネストしたモデルに対してデータを永続化したい

子要素のモデルに対してデータを永続化する場合の記述の仕方が若干変わります。 ある記事に対してユーザーがコメントを投稿するフォームを例にあげて考えてみましょう。 まずはコントローラーにてインタンス変数の定義を行います。

def new
  @article = Article.find(params[:article_id])
  @comment = Comment.new
end

def edit
  @article = Article.find(params[:article_id])
  @comment = Comment.find(params[:id])
end

コメントは必ずいずれかの記事に紐づいているので、どの記事に対するコメントなのかという情報が必要となります。 そのため@article = Article.find(params[:article_id])でコメントが行われた記事を特定します。 ビュー側では下記のようなイメージで、引数に配列を渡す渡すことでネストされた子要素に対するリクエストの送信が可能となります。

<%= form_with model: [@article, @comment] do |form| %>
  <%= form.text_field :text %>
  <%= form.submit %>
<% end %>

Rails 5.1以前のformヘルパー

form_tag

  • 関連するモデルが存在しない = 送られてきた値を特定のモデルに永続化しない場合

form_withでいうところのurlオプションを使用する際の要件に一致する場合、5.1以前ではform_tagを用いてこれを実現していました。 使い方としては第一引数に送信先のURL、第二引数にリクエスト時のHTTPメソッドを指定する形です。

<%= form_tag('/main', method: :post) do %> 
  <input type="text" name="nickname"> 
  <input type="submit"> 
<% end %>

api.rubyonrails.org

form_for

  • 関連するモデルが存在する = フォームから送られてきたparamsをDBに保存したい場合

form_withでいうところのmodelオプションを使用する際の要件に一致する場合、5.1以前ではform_forを用いてこれを実現していました。 使い方としては引数としてモデルのインスタンスを渡すのみ。form_forではFormBuilderオブジェクトのヘルパーメソッドを使用できるため、フィールドの属性とインスタンスのプロパティを適宜指定します。

<%= form_for(@user) do |f| %>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>

api.rubyonrails.org

まとめ

form_withについて

  • 関連するモデルが存在しない = 送られてきた値を特定のモデルに永続化しない場合は、urlオプションでコントローラーとアクションを直指定してリクエストを飛ばす

  • 関連するモデルが存在する = フォームから送られてきたparamsをDBに保存したい場合は、modelオプションに任意のモデルのインスタンスを渡して、ORマッピングに沿ったデータの永続化を行う

  • ネストした子要素のモデルに対してはmodelオプションに配列を渡すことでリクエストできる

form_withと旧form_for, form_tagとの関係

  • modelとurlの指定により用途の使い分けを可能にし、書き方を統一したのがform_with

  • Rails5.1以降のアプリケーションでは基本的にform_withで書くのが一般的

  • 昔のプロダクトでform_tag, form_forを利用している箇所があった場合に、出力されるhtmlがイメージできる程度にはなっておく必要がある

【5分くらいでわかる】formからネストしたハッシュの配列を送信する方法 | Rails入門

入門といいつつ今回は少し踏み込んだフォームの使い方について。 実務の要件ではform_withの枠組みから外れて、送るパラメータの構造を設計しなければならない場面に直面することもあります。 今回はそんな時に覚えておくとちょっと役立つかもしれないテクニックを3つほど紹介していきます。 この記事を最後まで読むことで、下記のようなパラメータを自在に設計できるようになります。

users: [
  {
   id: 10, name: "山田太郎", email: "test@email.com", phone_number: '024-111-11',
   action_histoires: [
    {id: 10, action_datetime: "2022-05-13T00:00:00", description: "外出"},
    {id: 20, action_datetime: "2022-05-13T00:00:00", description: "直帰"},
    {id: nil, action_datetime: "2022-05-13T00:00:00", description: "時間休 13:00~"},
   ],
  },
  {
   id: 20, name: "山田太郎", email: "test@email.com", phone_number: '024-111-11',
   action_histoires: [
    {id: 30, action_datetime: "2022-05-13T00:00:00", description: "外出"},
    {id: nil, action_datetime: "2022-05-13T00:00:00", description: "直帰"},
    {id: nil, action_datetime: "2022-05-13T00:00:00", description: "直帰"},
   ],
  },
]

この例はユーザーとそれに紐づくユーザーの行動履歴をネスト形式で送るという想定です。 usersという配列の中に各ユーザーの情報がハッシュとして格納されており、その中で更に関連する行動履歴をaction_historiesというハッシュの配列として持ち合わせています。このような構造のparamsを作るための方法を以下の順で追っていこうと思います。

  1. フォームからデータをハッシュ形式で送る方法
  2. フォームからデータを配列形式で送る方法
  3. フォームからデータをハッシュの配列形式で送る方法

データをハッシュ形式で送る

htmlのinputでname属性に user[name] のように指定すると、指定したパラメーター名でハッシュが作成されます。(ブラケット内のワードをキーに、inputに入力された値がバリューになる)

Rails(viewファイル)

<%= form.text_field 'user[name]' %> 
<%= form.email_field 'user[email]' %> 

form_withにモデルのインスタンスが指定されていれば以下と同義です

<%= form_with(model: User.new, local: true) do |form| %> 
    <%= form.text_field :name %> 
    <%= form.email_field :email %> 
<% end %>

form_withにmodelオプションが指定されている場合、formヘルパーの第一引数にモデルが持つ属性をシンボルで指定することが可能です。 このように指定した場合、復元されたhtmlでは モデル名[属性名] の形式のname属性がinputタグに自動でセットされます。

復元されるhtml

<input type="text" name="user[name]" value="山田太郎" >
<input type="email" name="user[email]" value="yamada@email.com" >

送信されるparams

user => {
  name: '山田太郎',
  email: 'yamada@email.com'
}

データを配列形式で送る

htmlのinputでname属性に user[] のように指定すると、同じnameを持つinput valueの配列が作成されます。

Rails(viewファイル)

<%= form.text_field 'user[]' %>
<%= form.email_field 'user[]' %> 

復元されるhtml

<input type="text" name="user[]" value="山田太郎" >
<input type="email" name="user[]" value="yamada@email.com" >

送信されるparams

user => ['山田太郎', 'yamada@email.com']

データをハッシュの配列形式で送る

上記二つを合わせるイメージ。htmlのinputでname属性に users[][name]のように指定すると、usersというキーに配列が作成され、その配列の中に複数のハッシュが内包される構造を作ることが可能です。

Rails(viewファイル)

<%= form.text_field 'users[][name]' %>
<%= form.text_field 'users[][email]' %>

復元されるhtml

<input type="text" name="users[][name]" value="山田太郎" >
<input type="email" name="user[][email]" value="yamada@email.com" >
<input type="text" name="users[][name]" value="佐藤次郎" >
<input type="email" name="user[][email]" value="sato@email.com" >

送信されるparams

users => [
    { name: '山田太郎', email: 'yamada@email.com' },
    { name: '佐藤次郎, email: 'sato@email.com' },
]

【5分くらいでわかる】ActiveRecordのdependentオプションについて | Rails入門

dependentとは

  • 親オブジェクトがdestroyされたとき、関連する子オブジェクトの扱いを制御するオプション

dependentオプションの種類

destroy

  • 親オブジェクトが削除されたら子オブジェクトも削除される
  • ActiveRecordを介して削除(コールバック処理が実行される)
  • クエリが親に紐づいている子の数だけ実行される
class User < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end

delete

  • 親オブジェクトが削除されたら子オブジェクトも削除される
  • SQLを直実行して削除(コールバック処理は実行されない)
class User < ActiveRecord::Base
  has_many :posts, dependent: :delete
end

delete_all

  • 親オブジェクトが削除されたら関連する子オブジェクトを全て削除する
  • SQLを直実行して削除(コールバック処理は実行されない)
class User < ActiveRecord::Base
  has_many :posts, dependent: :delete_all
end

nullify

  • 親オブジェクトのみが削除されて子オブジェクトをnilに更新する
class User < ActiveRecord::Base
  has_many :posts, dependent: :nullify
end

参考記事

railsguides.jp