判定ロジックはモデルに書こう!!

目次

判定ロジックの例

例えば、掲示板アプリで、「rails勉強中!」のような投稿があった場合。

投稿した人だけが、編集したり、削除したりするような機能をつけたい!(投稿していない人が勝手に編集・削除できないようにする)という場合があると思う。

これを実現するためには、「投稿した人のidと今ログインしている人のidが一致」すればOK。

これが判定ロジックの一例。

これを普通に書くと

例えば、以下のようなviewファイルの記述になる。

<%= if @post.user_id == current_user.id %>
  <%= link_to '編集', post_path(@post)
  <%= link_to '削除', post_path(@post), method: :delete %>
<% end %>

ただし、これだと長ったらしい。

加えて、例えば10箇所に判定ロジックを使用していた場合。判定ロジックの内容を変更しようとすると、10箇所全てを直す必要があるので、ものすごく大変!

だったら、モデルに判定ロジックを書こう!

なので、基本的に自分で作成する判定ロジックについてはモデルに書いた方がスマート(仮に10箇所で使ってたとしても、判定ロジックの中身だけ、つまり1箇所だけの変更でOK!)

ということで、今回の例で判定ロジックを書くと、以下の通り。

まず、モデル内に以下記述。

def own?(object)
  id == object.user_id
end

そして、ロジックの呼び出しは以下の通り。

<% if current_user.own?(@post) %>
  <%= link_to '編集', post_path(@post)
  <%= link_to '削除', post_path(@post), method: :delete %>
<% end %>

これで、最初のコードと同じ結果が実現できる。

コードの解説

さて、先ほどのコードを噛み砕いて、理解していきます。

モデル内の判定ロジックは厳密に言うと、self.idを省略している書き方

なので、省略しないで書くと

def own?(object)
  self.id == object.user_id
end

なので、self.idはレシーバーのid。言い換えると、呼び出し元のオブジェクトのidということになる。

もっと簡単にいうと、.own?の左側の部分のid

()内については引数。なので、以上まとめると

なので、

<% if current_user.own?(@post) %>

という、コードについては

current_user.id == @post.id

の判定を行なっているという意味になります!

CarrierWaveを使った画像のアップロード機能作成について

目次

CarrierWaveの導入方法

画像のアップロード機能を作成したい時はCarrierWaveという便利なgemを使うと比較的簡単に実装できる。

導入のためには以下のをgemfileに記述し、実行。

gem 'carrierwave'

実装方法

まず、アップローダーを作成。そのためには以下を実行。

rails g uploader Image

すると、アップローダーファイル(/uploaders/image.rb)が出来上がるので、以下記述。

class ImageUploader < CarrierWave::Uploader::Base
  # アップロードした画像の保存先パスを指定
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
  # デフォルト画像の設定
  def default_url
    'ファイル名.jpgなど'
  end
end

次に画像のカラムを設定。例えば、投稿データを扱うpostsテーブルがあった場合、imageカラムを作成するマイグレーションファイルを作成し、実行。(注意:あくまで、画像名のカラムなので、string型等にする

rails g migration AddImageToPosts image:string

ただし、これだけではダメで、アップローダーを使いますよ!という宣言をする必要がある

そのためにはPostモデルに以下を記述。

class Board < ApplicationRecord
  mount_uploader : image, ImageUploader
  # 以下省略
end

コントローラーの実装。今回は単純な画像登録だけの場合。

class PostsController < ApplicationController
  def new
    @post = Post.new
  end
                      
def create @post = Post.new(post_params) if @post.save redirect_to posts_path, success: '投稿しました' else flash.now[:danger] = '投稿失敗' render :new end end    private     def post_params params.require(:post).permit(:image) end end

最後に、view側でデータを渡すためのフォーム実装。

<%= form_with model: @post, local: true do |f| %>
  <%= f.label :image %>
  <%= f.file_field :image %>
  <%= f.submit class: 'btn' %>
<% end %>

これで、データのアップロード機能実装は完了!

アップロードした画像の表示方法

image_tageに以下のような記載をすればOK!

<%= image_tag @post.image.url %>
ちなみに、このコードについては単純に画像を出力するだけでなく、画像の有無の判定をしてくれる。

言い換えると、@post.imageが存在すればその画像を、なければ設定したデフォルト画像を出力のように、条件分岐も含めたコードを1行で実現している(ありがたい)

その他:画像の形式を制限したい時

CarrierWaveには便利機能がいくつかあるが、例えばファイルの種類を制限したい時。

具体的にはjpgとpngだけにしたい、みたいな場合。

アップローダーファイルに以下を記述すればOK!

class ImageUploader < CarrierWave::Uploader::Base
  def extension_whitelist
    %w(jpg png)
  end
end

アソシエーションとN+1問題について

目次

アソシエーションとは

簡単にいうと、複数のテーブルを紐付けること。

例えば、ユーザー(users)のテーブルには「鈴木」と「高橋」というデータがある。

投稿(posts)のテーブルには「好きな食べ物は寿司」「趣味は車」「趣味はピアノ」「趣味は車」「よくジムで筋トレする」みたいなデータがある。

この時に「鈴木」が「投稿したもの」と、「高橋」が「投稿したもの」に分けたい、これが紐付け。

例えば今回のケースだと、「鈴木」が投稿したのは「好きな食べ物は寿司」「趣味はピアノ」、「高橋」が投稿したのは「趣味は車」「よくジムで筋トレする」みたいな感じ。

具体的にどうする?

上記の例を考えるとき。

「鈴木」が投稿したのは「好きな食べ物は寿司」「趣味はピアノ」みたいに、1人のユーザー対し、複数の投稿が存在するという関係になる。

端的にいうと、1対多数(親と子)の関係であり、これを定義してあげればよい。

そのためには、親のモデル(今回はUserモデル)に以下を記述。

 has_many :posts, dependent: :destroy
# 紐づく子のモデル名については複数形で
# また、dependent: :destroyとすることで、親のデータが消えると、一緒に子のデータも消えるようになる。

子のモデル(今回はPostモデル)に以下を記述。

 belongs_to :user
# 紐づく親のモデル名については単数形で。

ただし、データベース上での紐付けも必要

上記だとモデル同士が紐づいているイメージ?なので、データベース上でテーブル同士の紐付けをする必要がある。

最初の例を考えると、鈴木の投稿がuser_id:1、高橋の投稿がuser_id:2みたいにしてあげれば良い。

すると、「好きな食べ物は寿司」のuser_id:1、だからこれは鈴木の投稿!みたいな判別ができる。

これを言い換えると、紐付けの関係を作るためには多数(子)に、親モデル_idを付与してあげると良いということ。

そのためには以下2つの方法が考えられる。

1. そもそも、モデルを作る際に定義してあげる。

今回の場合はPostモデルを作る際に、以下のような記述を行う。

rails generate model posts(モデル名) title:string body:text user:references

上記の記載例で、Postモデルのほか、postsテーブルを作成するマイグレーションファイルも作成される。

そしてマイグレーション ファイルに、titleカラム、bodyカラムだけでなく、user:referencesとすることで、user_idカラムが作成される記述が出来上がる。

2. 後から定義する

その場合は関連付けを規定する、マイグレーションファイルを作成する。以下、マイグレーションファイル作成の記載例。

rails generate migration AddUserIdToPosts 

作成されたマイグレーションファイルに以下を記述し、実行

 class AddUserIdToPosts < ActiveRecord::Migration[5.2]
  def change
    add_reference :posts, :user, foreign_key: true
  end
end

以下、結果はモデルを作る際に定義した場合と同様。

N+1問題とは?

アソシエーションを行った際、必ず考慮するべきなのが、N+1問題。

例えば、投稿を繰り返しで全部表示したい時。

一旦、@posts = Post.all みたいに投稿データをインスタンス変数に代入する。

そして、繰り返しの処理をして、表示するためには以下の通り。

<%= @posts.each do |post| %>
    post
<% end %>

これで、見た目は問題ないけど、実は中身ではとんでもないことが起きているって話!

実は@posts = Post.all の時点。

ここでは、当然postsテーブルからデータを引っ張ってきている(1回目、これが+1の部分。

しかし、SQLさんは「あれ、postsテーブルの中にuser_id: 1 がある…。あっ、そういえばusersテーブルと紐づいてたじゃん!よし、usersテーブルのuser_idが1のレコードを確認しよう!」となる(2回目)。

そして、SQLさんは「あれ、よく見ると postsテーブルの中にuser_id: 2 もある!?user_idが2のレコードも確認しなきゃ…」となる(3回目)。

つまり、余計なお世話だよ!みたいなことをしている。(もちろん、投稿に紐づいている投稿者の名前を表示したい時などはusersテーブルの参照も必要。とはいえ、user_idごとに参照するんじゃなくて、usersテーブルをまとめて見てくれよ…、見たいな感じ)

なので、なんと @posts = Post.all では3回もSQLが動いているということ!

言い換えると、たった1回の処理なのに、postsテーブルに1回、usersテーブルに2回、計3回アクセスしていて、余計な動作をしているということ。

まとめると、テーブルとテーブルが紐づいているとき。そのテーブルを参照する際、そのテーブルを1回、もう片方のテーブルを紐づいているid数(n個)だけ参照しているということ。

これの何が問題かというと、必要以上にSQLが発行され、特にもデータの量が増えるほど、めちゃくちゃアプリの動作が重くなってしまう可能性がある。

N+1問題の解決方法

解決するにはincludesメソッドを使用すればOK。

先ほどの例だと以下のように記述。

@posts = Post.all.includes(:user)

Bootstrapのスタイルを用いた、フラッシュメッセージ

目次

導入方法

application_controller.rbに以下のような記述

add_flash_types :success, :info, :warning, :danger

それで?何が違うの?

railsのデフォルトではフラッシュメッセージは「notice」と「alert」が指定されている。

なので、2色のパターンのフラッシュメッセージしか表示できない

ところが、成功した時は「青」、失敗した時は「赤」、警告したい時は「黄色」にしたい時なども当然考えられる。

なので、Bootstrapのスタイルを用いることで、それが実現するということ。

add_flash_types :success, :info, :warning, :danger

そのため、上記の記述はsuccess(緑)、info(青)、warning(黄)、danger(赤)という4色のパターンのフラッシュメッセージを使用できるようにしたということ!(もちろん、状況に合わせて使いたい色を指定してあげれば良い)

ちなみに、4色のイメージは以下の通り。

https://s3-ap-northeast-1.amazonaws.com/runteq-production/uploads/document_image/name/3/flash_keys.png

使い方

使い方については通常のフラッシュメッセージと同様。

以下は緑色で「ログイン成功!」というフラッシュメッセージを書く場合の例

redirect_to login_path, flash: { success: 'ログイン成功!' }

ただし、以下のようにflash: {}を省略することも可能!

redirect_to login_path, success: 'ログイン成功!' 

HTTPはステートレス←つまり、どういうこと?

目次

HTTPについて考えてみる

基本的にHTTPはステートレス。つまり、記憶力が超悪い、おバカさん。

簡単に言うと、1回ごとに忘れる。つまり、毎回教えてあげないといけないと言うこと。

なので、ログインしてる状態と言うのはログインしている状態でページ移動や何かのアクション(買い物かごへの追加とか)したりしているわけではなく、毎回ページ移動やアクションごとに、今はこいつだよ!と教えているようなイメージ。この時に使われるのが、セッションだったり、クッキーだったりする。

例で考えてみる

例えば、今、鈴木というアカウントでログインしていたとする。

その時、ページ移動したら、「あれ、今誰だっけ?」となるので、「今、鈴木と言うアカウントですよ」と教えている。

他にも例えば、買い物かごにりんごを追加しようとすると、「あれ、今誰だっけ?」となるので、「今、鈴木と言うアカウント」がりんごを追加だよ、と教えてる。

その後に、今度はみかんを追加しようとすると、また「あれ、誰だっけ?」となるので、「今、鈴木と言うアカウント」にみかんを追加だよ 、と教えている。 以下、繰り返し。

別の言い方をすると?

これを別の言い方にすると、まずログインする。その時にセッションIDに鈴木のidをいれる。

買い物かごにりんごを追加しようと、クッキーに保存されているセッションIDも一緒に送信される。

送られてきたセッションIDから、「今のユーザは鈴木だ!」と判断し、鈴木のデータにりんごを追加を保存する。

今度はみかんを追加しようとすると、またクッキーに保存されているセッションIDも一緒に送信される。

送られてきたセッションIDから、「今のユーザは鈴木だ!」と判断し、鈴木のデータにみかんを追加を保存する。以下、繰り返し。

つまり、毎回セッションIDを送って、そのIDを元にユーザを毎回特定しているということ。

具体例で考える

もっとわかりやすい例で言うと、ログインしているかでヘッダーを切り替える時とか。

あれはログインしている状態によって、切り替えていると言うよりかは、毎回毎回表示される度に「あれ、今ログインしているっけ?」みたいな状態で、毎回「今、してるよ!だから、している場合の表示はこっちだよ!」みたいなことをしている。

CRUDとresourcesメソッドについて

目次

そもそもCRUDって何?

webアプリケーションには基本的に、Create(作成)、Read(読み込み)、Update(更新)、Delete(削除)の4つ主要機能がある場合がほとんど。

例えば基本的な掲示板アプリで、CRUD機能を考えてみると

  1. 投稿の一覧を表示する

  2. 各投稿の詳細を表示する

  3. 新規投稿画面を表示する

  4. 新規作成の処理をする

  5. 更新画面を表示する

  6. 更新の処理をする

  7. 投稿を削除する

7つのアクションが必要となる。

resourcesメソッド

さて、上記7つのアクションに対するルーティングを設定する時、

get 'index', to: 'posts#index'
get 'new', to: 'posts#new'
# 以下省略

のように、7つ設定するのは面倒臭いし、何よりスマートじゃない。

ということで、resourcesメソッドの出番!

resourcesメソッドを使うことで

  1. index(一覧表示)

  2. show(詳細表示)

  3. new(新規作成画面表示)

  4. create(新規作成処理)

  5. edit(更新画面表示)

  6. update(更新処理)

  7. destroy(削除)

ルーティングパターンを一気に作成してくれる!

resourcesメソッドの使い方

例えば、posts_controllerに対するアクションを設定する時。routes.rbに以下を記述。

resources posts

この記述だけで、なんと7つのルーティングパターンが生成させるということ!

ちなみに、今回はindex、new、createアクションだけでいんだよな〜、みたいな時。

そんな時は以下のような記述でOK。

resources :posts, only: %i[index new create]