railsのアソシエーションを学ぶ その2(多対多のアソシエーションと中間テーブル)

こんにちは!
前回の記事の続きです。

前回の記事はこちら↓
kojiprg.hatenablog.com


この記事では、ツイッターもどきを作成しています。
前回は、どのユーザが投稿したツイートかわかるように、ユーザとツイートのテーブルに1対多のアソシエーションを実装しました。
1対多の関係とは、以下のような関係でした。

  • ユーザ一人が、複数のツイートをできる
  • ツイートは、一人のユーザに属する

今回は、多対多と呼ばれるアソシエーションを使用して、いいね機能の実装をしていきます。

いいね機能の概要

まず、今回実装するいいね機能について定義します。

  1. 各ツイートにいいねボタンを表示する
  2. いいねボタンの横にそのツイートにいいねをしたユーザの数を表示する
  3. いいねボタンの色で、ログインしているユーザがどのツイートにいいねをしたかがわかる
  4. まだいいねをしていないツイートのいいねボタンを押すことで、そのツイートにいいねをすることができる
  5. いいね済みのツイートのいいねボタンを押すことで、そのツイートへのいいねを取り消すことができる

本家Twitterで、ハートをクリックすると、ハートがついたり消えたりする動作をイメージしています。
ただし、今回のアプリではJavaScriptによる非同期での更新機能は実装せずに、ボタンをクリックするたびに画面を更新させる仕様にします。

f:id:kojiprg:20201031104659g:plain
今回実装するいいね機能

いいねボタンをクリックすると、色が変化するのと、右隣にある数字が増減します。

テーブルの設計

上記機能を実現するためには、ユーザがどのツイートにいいねをしたかが分かるようなデータを保存する必要があります。
ここでは、どうやってテーブルに保存するかを考えます。

まず、ユーザテーブルに保存することを考えます。
ユーザは複数のツイートにいいねできるため、どのツイートにいいねしたかを記録するためには、ユーザテーブルにツイートの数だけカラムを用意しておく必要があります。
ツイートの数が、5個や10個くらいに制限されていればこれでもいいかもしれませんが、拡張性がありません。

次に、ツイートテーブルに保存することを考えます。
この方法でも、どのユーザがいいねをしたかを記録しておくために、ユーザごとにカラムを準備する必要があるので、拡張性がありません。

ユーザやツイートの数に合わせて、カラムを増やしていくのは無理があるので、レコードを増やす方向で考えます。
新しくテーブルを作成すればレコードは簡単に増やすことができます。
そこで、いいねテーブルを作成します。
このテーブルに、いいねしたユーザと、いいねされたツイートのidを保存していくことで、実現できそうです。

今回の状況をまとめます。

  • ユーザが複数のツイートにいいねができる
  • ツイートが複数のユーザにいいねされる

つまり、ユーザから見た時も、ツイートから見た時も相手が複数いる状況です。
このような関係を多対多の関係とよび、新しくテーブルを作ることで、テーブル同士の関係性を保存することができます。
このテーブルを中間テーブルと呼びます。

いいねテーブルを作成した後の、テーブル構成は以下のようになります。

f:id:kojiprg:20201030191739p:plain
中間テーブルとして、いいね(Favorites)テーブルを作成

references型でuserと、tweetのidを保存します。

機能の実装は以下のようにすればできそうです。

  • ツイートにいいねをしたユーザの数を表示する

→いいねテーブルで、調べたいツイートのtweet_idがいくつ保存されているかを調べる。

  • ログインしているユーザがどのツイートにいいねをしたかがわかる

→いいねテーブルで、調べたいツイートのtweet_idで検索をかけ、その中にログイン中のuser_idを含むものがあるかどうを調べる。

アソシエーションの実装

実装はポイントに絞って解説し、rails db:migrateなんかは端折ります。(読み直したら、もっと端折ってました。)
まず、モデルへのアソシエーションの記述です。
基本的には、1対多のアソシエーションと同じで、has_manyとbelongs_toを使用します。

  • favorite.rb
class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :tweet
end
  • user.rb
(省略)
  has_many :favorites, dependent: :destroy
(省略)
(省略)
  has_many :favorites
  has_many :users, through: :favorites
(省略)

ポイントはtweetモデルの記述です。

  has_many :users, through: :favorites

throughオプションを使用することで、ツイートにいいねをしたユーザの一覧をtweet.usersのような形で取得することができます。
流れにすると以下のようになります。
tweetのidを取得
→favoritesテーブルでtweet_idと一致するいいねの一覧を取得。これで、ツイートにいいねしたユーザのidが分かる
→usersテーブルからtweetにいいねしたユーザの情報が取得できる

サンプルコード

ビューとコントローラのコードです。

  • index.html.erb
(省略、以下メッセージ部分)
<div class="messages">
  <% @tweets.each do |tweet| %>
    <div class="message">
      <div class="message_head">
        <div class="message_user_name">
          投稿者:<%= tweet.user.user_name %>
        </div>
        <div class="message_created_at">
          <%= time_ago_in_words(tweet.created_at) %>
        </div>
        <% if tweet.users.ids.include?(current_user.id) %>
          <%= button_to "いいね", favorite_path(tweet.id), method: :delete, style: "background-color: pink; border-radius: 50vh;" %>
        <% else %>
          <%= button_to "いいね", favorites_path, params: {tweet_id: tweet.id}, style: "background-color: white; border-radius: 50vh;" %>
        <% end %>
        <div class="favorite_count">
          <%= tweet.favorites.size %>
        </div>
      </div>
      <div class="message_content">
        <%= simple_format(tweet.message) %>
      </div>
    </div>
  <% end %>
</div>
  • favorites_controller.rb
class FavoritesController < ApplicationController
  def create
    @favorite = Favorite.new(favorite_params)
    @favorite.save
    redirect_to root_path
  end

  def destroy
    @favorite = Favorite.find_by(user_id: current_user.id, tweet_id: params[:id])
    @favorite.destroy
    redirect_to root_path
  end

  private

  def favorite_params
    params.permit(:tweet_id).merge(user_id: current_user.id)
  end
end
  • tweets_controller.rb(indexアクション)
class TweetsController < ApplicationController
  def index
    @tweet = Tweet.new
    @tweets = Tweet.includes(:user, :users).reverse_order
  end
(省略)

ビューのいいねボタンの実装部分は以下です。

        <% if tweet.users.ids.include?(current_user.id) %>
          <%= button_to "いいね", favorite_path(tweet.id), method: :delete, style: "background-color: pink; border-radius: 50vh;" %>
        <% else %>
          <%= button_to "いいね", favorites_path, params: {tweet_id: tweet.id}, style: "background-color: white; border-radius: 50vh;" %>
        <% end %>

一行目のif文の条件文で、アソシエーションの記述を使用して、ツイートにいいねをしたユーザのidを取得しています。
ポイントは、idも複数形のidsになっている点と、
whereやfind_byを使用せずに、簡潔にかけている点です。
あとは、include?メソッドを使用して、その中にログイン中のユーザのidが含まれているかどうかを判定しています。
また、button_toのデフォルトのメソッドはpostのため、いいねを取り消すdestroyアクションを作動させる場合は、method: :deleteを指定しています。
微妙な点としては、直接favoriteのidを取得することが難しかったため、tweet.idをパスに放り込んでいます。
いいねコントローラの記述を見るとわかりますが、直接favoriteのidで検索するのではなく、find_byでuser_idとtweet_idを検索して削除しています。

    @favorite = Favorite.find_by(user_id: current_user.id, tweet_id: params[:id])

続いて、いいねされた数を表示する機能の実装部分です。

          <%= tweet.favorites.size %>

アソシエーションとsizeメソッドを使用することで、レコードの数を表示しています。
countでも同様のことができますが、SQL文が毎回発行されてしまいます。
sizeメソッドではすでにロード済みの場合、その結果を使用するためSQL文が発行されません。
今回は、ツイートコントローラで以下のように:usersをincludesで呼び出しています。

    @tweets = Tweet.includes(:user, :users).reverse_order

この時に、throughオプションによってfavoritesも同時にロードしているため、sizeメソッドを使用することで、SQL文の発行を回避できています。

まとめ

今回は多対多のアソシエーションと中間テーブルでした。
アソシエーションを使用することで、異なるテーブルの情報を簡単に取得することができました。
後半の実装部分はややパフォーマンスも意識してみたので、わかりにくくなっていたらすみません。
最後までありがとうございました!

railsのアソシエーションを学ぶ その1(導入、1対多のアソシエーション)

こんにちは!
10, 11月で、オンラインライブ2本、有観客ライブ2本、チケット取れたので、浮かれています。
楽しみをよりしっかり楽しめるように、インプット・アウトプットもしっかりしていきます。

今回は、モデル同志の関連づけであるアソシエーションについて書きます。
言葉だけだと分かりにくいことも多いと思うので、今回もミニアプリを作成していきます。
今回のミニアプリはツイッターもどきです。
要件をまとめます。

  • 短い文章(ツイート)を投稿できる
  • ログインしている人だけが、ツイートの投稿と表示ができる
  • ツイートの一覧が表示される
  • 誰のツイートか分かる
  • ツイートにいいねができる
  • フォローした人のツイートだけをみたい


そのために、以下の機能を実装します。

  • ユーザ登録機能
  • ツイート投稿機能
  • ツイート表示機能
  • いいね機能
  • フォロー機能

今回の記事では、ツイート表示機能までを実装します。
また、この時点では全員の投稿が表示される状態です。

なお、こちらの記事をオマージュさせていただきました。
qiita.com

概要

今回の記事では、テーブル設計をしながら、都度実装をしていきます。
実際は、出戻りを少なくするために、テーブル設計をきちんとしてから、実装に入ることになると思います。
ですが、本記事では以下の理由からテーブル設計と実装を並行して進めていきます。

  • 初心者に最初から完璧なテーブル設計は難しい(間違えるため結局出戻りする。それであれば、簡単なところから確実に機能を増やした方が学習効果はあると思う)
  • アソシエーションの説明に主眼をおいているため、必要なアソシエーションが出てきたタイミングで説明する方が分かりやすい

アソシエーションとは?

まず、アソシエーションとはモデルとモデルの関連づけのことです。
アソシエーションがあると便利な例を今回のアプリの機能から紹介します。
例えば、誰のツイートか分かるようにするためには、ツイート内容だけでなく、その投稿者の名前も表示すればよいです。
そこで、以下のようなテーブルを設計します。

f:id:kojiprg:20201014180024p:plain
UsersテーブルとTweewsテーブル(アソシエーションなし)

一行目がテーブル名、それ以外の行がカラムの情報を示しています。
単純に、ユーザの情報を保存するUsersテーブルと、ツイートの内容と誰の投稿かを保存するTweetsテーブルを作成しました。
なお、PKはPrimary Keyの略で、mysqlを使用している場合は、自動的にidに割り当てられます。
ここで、赤丸で囲ったカラムに注目してください。
Usersテーブルにも、Tweetsテーブルにもuser_nameのカラムがあります。
このテーブル設計では、ツイートを投稿する時に、メッセージだけでなく、投稿したユーザのユーザ名もTweetsテーブルに保存しています。
これは無駄です。
それだけでなく、不便でもあります。
例えば、user_name以外のユーザの情報をツイート表示時に使用したくなった場合を考えます。
上記の設計では、Tweetsテーブルに新しくカラムを増やして、ユーザの情報を保存し直すか、user_nameからUsersテーブルの該当する情報を検索する必要があります。
しかも、検索をかける場合にはuser_nameに対して一意性の制約をかけておいたり、検索速度を向上させるためにインデックスを貼るなどの工夫も必要です。
このような、無駄・不便を解消する方法がアソシエーションです。
以下に、アソシエーションをつけたテーブル設計を示します。

f:id:kojiprg:20201014184008p:plain
UsersテーブルとTweewsテーブル(アソシエーションあり)

Tweetsテーブルのuser_nameカラムをuser_idカラムに変更しました。
また、型をreferences型にし、オプションでforeign key: trueを設定します。
マイグレーションファイルでの書き方です。

t.references :user, foreign_key: true

まず、references型ですが、この型にすると上記のようにuserとするだけで、user_idカラムが作成され、UsersテーブルとTweetsテーブルが紐づけられます。
これとモデルでアソシエーションを宣言することにより、@tweetというインスタンスを作成しておいて、

@tweet.user.user_name

のように書くことで、対応するユーザ名を取得することができます。
もちろんユーザ名以外を取得することもできます。

次に、forein_key: trueですが、外部キー制約と呼ばれるものです。
この制約をかけることで、Tweetsテーブルへの保存時に該当するUserがいないと保存することができなくなります。
また、Userも、紐づけられているTweetがある場合に、そのUserの削除をすることができなくなります。
外部キー制約をかけておくことで、ツイートをしたユーザ名を表示→対応するユーザがいないためnilクラスを取得→エラーといったことが回避できます。
なお、外部キー制約をかけている場合に、紐づいているユーザを削除する方法は後述します。

ここまで、アソシエーションの必要性・有用性についてでした。

ユーザ登録機能の実装

ここからは、アプリの実装に入っていきます。
まず、ユーザ登録機能を実装します。
今回も、deviseを使用します。

ToDo

  • config/database.ymlから、encodingをutf8に変更
  • rails db:create
  • tweetsコントローラ+indexビューを作成
  • tweets#indexをトップページとするルーティング
  • deviseのgemを導入、インストール
  • devise経由でUserモデルを作成
  • マイグレーションファイルを編集して、user_nameカラムを作成
  • rails db:migrate
  • application_controller.rbで、deviseのストロングパラメタを設定して、user_nameカラムに保存できるようにする
  • application_controller.rbで、before_action :authenticate_user!を宣言し、未ログイン時は強制的にログインページに誘導する
  • ログインページと、ユーザ登録ページのビューを編集し、user_nameのテキストフィールドを作成する
  • トップページに、ログインしているユーザ名と、ログアウトボタンを作成する

書き出してみると結構ありますね。。。
あとは、user_nameにバリデーションをかけとくと良いと思います。

validates :user_name, presence: true

ちなみに、deviseのビューファイルは、下記のコマンドで作成できます。

rails g devise:views

上記の場合、全てのdeviseのビューフォルダが作成されてしまいます。
今回は、registrations(ユーザ登録用)とsessions(ログイン用)フォルダのビューしか編集しないため、次のようにオプションをつけても良いです。

rails g devise:views - v registrations sessions

ここまでの実装をgifにしました。

f:id:kojiprg:20201014210843g:plain
ユーザ登録機能

ツイート機能の実装

ようやく、アソシエーションを組んでいきます。
ツイート機能でのアソシエーションは「アソシエーションとは?」で書いた通りです。

Todo

  • Tweetモデル作成
  • マイグレーションファイルを編集し、rails db:migrate
  • モデルにアソシエーションの記述
  • ルーティング
  • tweetsコントローラにcreateアクションを追加
  • トップページにtweet投稿フォームを実装
  • トップページで、投稿済みのツイートが表示されるようにビューを編集

Tweetsテーブルと、Usersテーブルのアソシエーションを作るためのマイグレーションファイルです。

class CreateTweets < ActiveRecord::Migration[6.0]
  def change
    create_table :tweets do |t|
      t.string :message, null: false 
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end
t.references :user, foreign_key:true

これで、user_idカラムが作成されます。
ちなみに、短い文章の投稿ということでmessageカラムの型はstring(255文字まで)にしています。
通常、文章を保存する場合には、text型を使用します。

続いて、モデルにアソシエーションの記述をします。
考え方としては、userは複数のtweets(複数なので複数形にする)をすることができるので、
user has many tweets

一方、tweetの立場からすると、userに持たれている、つまりuserに所属しているので、
tweet belongs to user
になります。
これを、ほぼそのままモデルに書きます。

user.rb

has_many :tweets

tweet.rb

belongs_to :user

ナチュラルな英語っぽくかけるのが、ruby, railsの強みですね。

このような、ユーザ一人に対して、複数のツイートを結びつけるようなアソシエーションを1対多のアソシエーションと呼びます。
ちなみに、一人のユーザに対して、一つだけのデータを関連づけたい時(例えば、支払い方法とか)には、has_many :複数形の代わりに、has_one :単数形を使用します。

あとは、ビューとコントローラをよしなに書いてください。

ここまでの実装結果です。

f:id:kojiprg:20201014232525g:plain
ツイート機能

せっかくなので、ボタンにcssを適用したり、何分前に投稿されたかを表示できるようにしました。
参考までに、この時点でのビューとコントローラのコードです。
ポイントは、includesでN+1問題を回避していることですかね。

index.html.erb

<h1>トップページ</h1>
<% if user_signed_in? %>
  <div>ユーザ名: <%= current_user.user_name %></div>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% end %>

<%= form_with model: @tweet, url: tweets_path, method: :post, local: true do |f| %>
  <div>
    <%= f.text_area :message, placeholder: "メッセージを入力してください" %>
  </div>
  <%= f.submit "つぶやく", class: "button"%>
<% end %>

<div class="messages">
  <% @tweets.each do |tweet| %>
    <div class="message">
      <div class="message_head">
        <div class="message_user_name">
          投稿者:<%= tweet.user.user_name %>
        </div>
        <div class="message_created_at">
          <%= time_ago_in_words(tweet.created_at) %>
        </div>
      </div>
      <div class="message_content">
        <%= simple_format(tweet.message) %>
      </div>
    </div>
  <% end %>
</div>

tweets_controller.rb

class TweetsController < ApplicationController
  def index
    @tweet = Tweet.new
    @tweets = Tweet.includes(:user).reverse_order
  end

  def create
    @tweet = Tweet.new(tweet_params)
    if @tweet.save
      redirect_to root_path
    else
      render :index
    end
  end

  private

  def tweet_params
    params.require(:tweet).permit(:message).merge(user_id: current_user.id)
  end
end

depented: :destoryオプション

今回、foreign_key: trueの設定をしたため、ユーザだけを削除しようとするとエラーになります。
そこで、ユーザを削除すると同時に紐づいているツイートを全て削除するようにアソシエーションで宣言します。
方法は以下のように、user.rbを編集します。

has_many :tweets, dependent: :destroy

dependent: :destroyをつけるだけです。
なお、ユーザの削除のリンクは、以下のコードです。
パスの確認は、rails routesですね。

<%= link_to "ユーザを削除する!ツイートも消えます!", user_registration_path, method: :delete %>
f:id:kojiprg:20201014235837g:plain
user:testを削除すると、testが投稿したツイートも消える

まとめ

ここまでで、前半戦終了です。
今回のように一人のユーザが何回もツイートできる、そしてツイートの方から投稿したユーザの情報を得たい、という状況では、1対多のアソシエーションになります。
ポイントとしては、以下の2点です。

次回は、多対多のアソシエーションとして、いいね機能を、
自己結合型のアソシエーションとして、フォロー機能を実装します。

最後までお付き合いいただきありがとうございました!

その2の記事はこちらです↓
kojiprg.hatenablog.com

rubyで参照渡し的な挙動ができないか?

こんにちは!
先週は7ヶ月ぶりにライブに行ってきました。
LINE CUBE SHIBUYAでの講演でマスク着用、声出し厳禁でしたが、最高でした!
ドラムのキックがドスドスくる感じとか音圧、目の前にアーティストがいる感動は家では味わえないので、じわじわライブ開催が増えて欲しいです。
そして、ホール講演は楽でいいですね。
コインロッカーの確保も不要、座席指定なので開場時間前から並ぶ必要もない。
しかも、事前物販(オンライン)のみ!
引きこもり生活で体力も落ちているのでちょうどよかったです。
汗だくのライブハウスはまだまだ先なんだろうな。。。

今回のテーマ

値渡し・参照渡しの説明から入り、値渡ししか存在しないrubyで参照渡しのような挙動をさせるにはどうするかを書きます。
きっかけは、1月ほど前から、AtCoder競技プログラミングを始め、模範解答やアルゴリズムの参考コードを読むためにc++の勉強をし始めたことでした。
関数について学んでいる際に参照渡しというものが出てきたので、rubyでもできないかと思い調べてみました。
結論としては、rubyには参照渡しは存在しません。
ただ、参照渡しと同様の挙動をさせることはできます。
何を言っているのか、ちょっとよくわかりませんが、まずは参照渡しについてと、その対になる値渡しについて簡単に説明します。
個人的な要約も混じっているため、プログラミング言語内部の動きを正確に述べているわけではありませんが、概略は掴んでいると思います。

関数・メソッドへの引数の渡し方について_値渡しと参照渡し

値渡し・参照渡しと言うのは、関数(rubyではメソッド)への引数の渡し方の種類です。
内容についてはこちらの記事を参考にさせていただきました。
簡単にまとめると、以下のようになります。
なお、「参照」とは変数へのリンクだと思えば良いと思います。

  • 値渡し:引数に指定した変数の値をコピーして、関数内で使用。元の変数とは別にコピーが作成されるため、関数実行後でも引数に指定した変数の値は変化しない
  • 参照渡し:引数に指定した変数への参照を、関数内で使用。変数が持つ値を直接変更することになるため、関数実行後、引数に指定した変数の値は変化する

ポイントは、コピーを作成するかしないかです。
変数が変化しない・するというのは、上記の結果です。
言葉だけだと分かりにくいので、rubyで簡単なプログラムを書きました。

値渡しの例_その1(一般的な挙動)

def call_Taro(name)
  name = 'Taro'
  puts name
end

name = 'Jiro'
puts name

call_Taro(name)

puts name

このコードで実行していることは以下です。

  1. メソッドcall_Taroを定義。内容は、引数を'Taro'に変更して出力。
  2. 変数nameを初期値'Jiro'で定義。nameを出力。
  3. nameを引数にとり、call_Taroを呼び出し。
  4. 再度、nameを出力。

結果はどうなるでしょうか?
rubyは「値渡し」なので、メソッド内では変数のコピーに対して変更が加えられ、元の値は影響を受けず、以下のようになります。

Jiro
Taro
Jiro

値渡しの例_その2(破壊的メソッドを使用すると?)

続いて、ruby初心者がよくハマる?トラップです。
破壊的メソッド(大体「!」がついているメソッドです)であるupcase!を使用します。
upcase!は対象のstringを全て大文字に変換します。

def call_JIRO(name)
  name.upcase!
  puts name
end

name = 'Jiro'
puts name

call_JIRO(name)

puts name

この場合の結果を示します。

Jiro
JIRO
JIRO

ポイントは、最後の出力です。
なんと、'Jiro'ではなく、メソッドでの出力と同じ'JIRO'になっています。
この理由について、説明します。

破壊的メソッドを使用すると、オブジェクトの中身が変わる

rubyでは、変数それぞれにobject_idが割り当てられています。
ここでは、object_idが同じ変数は値を共有していると考えて大丈夫です。
(正確には、私も調べきれていません。。。)
rubyでの「値渡し」の際に、実はこのobject_idもコピーされて渡されています。
ここがポイントです。
上記の例_その1の場合、変数へ値を再代入するという、非破壊的メソッドを使用したため、このタイミングでobject_idが書き換わっています。
一方、例_その2の場合、いきなりupcase!という破壊的メソッドを使用したため、引数で指定した変数と同じobject_idの変数が書き換えられています。
object_idが同じ変数は値を共有しているため、値渡しであるにもかかわらず、変数の値が変化しています。
これについても、rubyプログラムで実際の挙動を確認していきます。

値渡しの例_その3(object_idを確認してみる_その1)

その1のコードを改変して、object_idの変化を見えるようにしました。

def call_Taro(name)
  puts "#{name} #{name.object_id}"
  name = 'Taro'
  puts "#{name} #{name.object_id}"
end

name = 'Jiro'
puts "#{name} #{name.object_id}"

call_Taro(name)

puts "#{name} #{name.object_id}"

結果(object_idの値は時と場合により変わります。)

Jiro 70252407078620
Jiro 70252407078620
Taro 70252407078420
Jiro 70252407078620

出力についての説明です。
1行目:メソッド実行前に初期化したnameを出力。
2行目:メソッド内で、呼び出した直後のnameを出力。この時点では、object_idが1行目と同一であることがわかる。
3行目:メソッド内で、nameに非破壊的変更を加えた後。object_idが変化している。
4行目:メソッド実行後、再度nameを出力。メソッド実行前のobject_idが呼び出されている。
このように、object_idもコピーされてメソッドに渡されていること、非破壊的メソッドを使用した場合に、object_idが変化していることがわかりました。

値渡しの例_その4(object_idを確認してみる_その2)

今度は、その2の改変版です。

def call_JIRO(name)
  puts "#{name} #{name.object_id}"
  name.upcase!
  puts "#{name} #{name.object_id}"
end

name = 'Jiro'
puts "#{name} #{name.object_id}"

call_JIRO(name)

puts "#{name} #{name.object_id}"

結果(object_idの値は時と場合により変わります。)

Jiro 70246049185640
Jiro 70246049185640
JIRO 70246049185640
JIRO 70246049185640

破壊的メソッドを使用した場合、object_idが変化していないことがわかります。(3行目)
そのため、4行目も'JIRO'に変化してしまいました。

値渡しの例_その5(破壊的メソッド後の非破壊的メソッド)

ちょっとトリッキーなパターンです。

def call_JIRO_TARO(name)
  name.upcase!
  puts name
  name = 'TARO'
  puts name
end

name = 'Jiro'
puts name

call_JIRO_TARO(name)

puts name

この場合、最後の出力は、'Jiro', 'JIRO', 'TARO'のどれになるでしょうか?
object_idの変化を考えればわかると思います。
答えは、こちら。

Jiro
JIRO
TARO
JIRO

'JIRO'でした。
メソッドの最初で、upcase!を呼び出しているので、この時点で'Jiro'から'JIRO'に変化します。
続いて、'TARO'の再代入ですが、これは非破壊的メソッドのため、object_idが変化し、'JIRO'に影響は及びません。
したがって、最終的な出力は'JIRO'になります。

一旦、まとめ

ここまでで、rubyにおいてメソッドへの引数の渡し方は値渡しであること、
object_idもコピーされて渡るため、最初に破壊的メソッドを使用すると、元の変数も変化してしまうこと、
の2点がお分かりいただけたでしょうか。
値渡しとしての挙動を担保したい場合は、最初に非破壊的メソッドを使用するように注意した方が良さそうです。

初心者がよくハマるトラップ

この後の内容とも関連しますが、配列が出てくると、ややこしいことになります。
実は、配列の要素の追加・削除や、変更は破壊的メソッドになっています。
そのため、配列の要素にメソッド内でアクセスすると、意図しない動作になることがあります。
次に示す例は、文字列の操作ですが配列の操作と同じ現象が起きています。

def call_taro(name)
  name[0] = 't'
  puts name
end

name = 'Taro'
puts name

call_taro(name)

puts name

意図としては、最後の出力は'Taro'になって欲しいです。
が、実際の結果は、'taro'になります。

Taro
taro
taro

はい、破壊的メソッドでした。
メソッドの最初に、配列自身の再代入など非破壊的メソッドを挟むことで、回避することができます。

def call_taro(name)
  name = name.dup
  name[0] = 't'
  puts name
end

name = 'Taro'
puts name

call_taro(name)

puts name

上記のコードでは、メソッドの最初にdupメソッドを使用してコピーを作成し、nameのobject_idを変更しています。
こうすることで、値渡しで予想される挙動になります。

Taro
taro
Taro

ruby再帰関数を書いてみる

ここから、後半戦です。
再帰関数を通して、rubyで参照渡し的に変数を変化させる方法を考えます。
再帰関数とは、関数の中で自身を呼び出す関数のことです。
深さ優先探索などのアルゴリズムで使用されます。
気になる方は調べてみてください。
今回は簡単な例として、下図に示す頂点1から10までの出発時刻と到着時刻を出力するプログラムを書きます。

f:id:kojiprg:20200930175335p:plain
1から10まで往復する際の、出発・到着時刻を各頂点ごとに出力する

ダメな例

まず、これまでの値渡しや参照渡しのルールを全て忘れて、それっぽいコードを書いてみます。

n = 10
departure = Array.new(n, 0)
arrival = Array.new(n, 0)
x = 0
time = 0

def count_time(departure, arrival, n, x, time)
  time += 1
  departure[x] = time

  if x < n - 1
    next_x = x + 1
    count_time(departure, arrival, n, next_x, time)
  end

  time += 1
  arrival[x] = time
end

count_time(departure, arrival, n, x, time)

n.times do |i|
  puts "#{i + 1} #{departure[i]} #{arrival[i]}"
end

各変数とメソッドの意味です。
n:頂点数
departure:出発時刻を格納する配列
arrival:到着時刻を格納する配列
x:現在の頂点番号 - 1(配列のindexが0から始まることに対応させるため)
time:現在時刻(0で初期化)
count_time:timeを更新しながら、departureに記録、次の頂点があれば次の頂点に対して再度自身を呼び出す、最後にarrivalに記録
少し、count_timeの挙動がわかりにくい方もいるかもしれません。
そういう方は、今一度図をみてください。
count_downの前半が下半分の矢印に相当します。
timeを増やして、次の頂点に進む動作です。
これを、頂点10に到着するまで繰り返すようにif文で指示しています。
一方、count_downの後半が上半分の帰りの矢印に相当します。
進んだ回数だけ、timeを更新しながら戻っていきます。
さて、結果はどうなるでしょうか。

1 1 2
2 2 3
3 3 4
4 4 5
5 5 6
6 6 7
7 7 8
8 8 9
9 9 10
10 10 11

残念な感じになっています。
原因は、timeが値渡しで渡されている点です。
本来、再帰呼び出ししているcount_downを抜けたタイミングでtimeは帰りの値に更新されていて欲しいのですが、値渡しのため、再帰前のtimeを保持しています。

n = 10
departure = Array.new(n, 0)
arrival = Array.new(n, 0)
x = 0
time = 0

def count_time(departure, arrival, n, x, time)
  time += 1
  departure[x] = time # <= ここのtimeと、

  if x < n - 1
    next_x = x + 1
    count_time(departure, arrival, n, next_x, time)
  end

  time += 1 # <= ここのtimeは、同じ値!
  arrival[x] = time
end

count_time(departure, arrival, n, x, time)

n.times do |i|
  puts "#{i + 1} #{departure[i]} #{arrival[i]}"
end

なので、timeをどうにかして更新する必要があります。
この方法をこの後、3種類示します。
その前に、もう一つ重要なポイントがあります。
それは、departureとarrivalの値が更新されていることです。
これは先ほどハマるポイントで紹介した、配列要素の変更が結果的に意図した動作を引き出しています。
つまり、今回departureもarrivalもその要素の変更という、破壊的操作しかしていないため、まるで参照渡しをしたかのように振る舞っています。
(あくまで、値渡しです。)

timeを更新する方法_その1(インスタンス変数を使用する)

方法その1はインスタンス変数の使用です。
簡単に説明すると、変数名の前に@をつけることで、スコープが拡大し、定義していないメソッド内でもその変数を使用することができるようになります。

n = 10
departure = Array.new(n, 0)
arrival = Array.new(n, 0)
x = 0
@time = 0

def count_time_v2(departure, arrival, n, x)
  @time += 1
  departure[x] = @time

  if x < n - 1
    next_x = x + 1
    count_time_v2(departure, arrival, n, next_x)
  end

  @time += 1
  arrival[x] = @time
end

count_time_v2(departure, arrival, n, x)

n.times do |i|
  puts "#{i + 1} #{departure[i]} #{arrival[i]}"
end

変更点は、timeを@timeにした点と、メソッドの引数からtimeを削除した点です。
こうすることで、各メソッドの中で同じ@timeが参照されるようになり、ちゃんと更新されていきます。
結果は、意図した通りになりました。

1 1 20
2 2 19
3 3 18
4 4 17
5 5 16
6 6 15
7 7 14
8 8 13
9 9 12
10 10 11

timeを更新する方法_その2(戻り値に指定する)

方法その2はメソッドの戻り値にtimeを指定することです。
メソッドの出口でtimeを戻り値に指定し、それを受け取ることで、timeを更新しながら帰ることができます。

n = 10
departure = Array.new(n, 0)
arrival = Array.new(n, 0)
x = 0
time = 0

def count_time_v3(departure, arrival, n, x, time)
  time += 1
  departure[x] = time

  if x < n - 1
    next_x = x + 1
    time = count_time_v3(departure, arrival, n, next_x, time)
  end

  time += 1
  arrival[x] = time
  return time
end

count_time_v3(departure, arrival, n, x, time)

n.times do |i|
  puts "#{i + 1} #{departure[i]} #{arrival[i]}"
end

count_timeの最後で、return timeとすることで、timeを戻り値に指定しています。
rubyでは自動的にメソッドの最後の行が戻り値になるため、returnを省略することが多いです。
今回は、戻り値であることを明示するため、記載しました。
再帰的にcount_timeを呼び出している箇所で、

    time = count_time_v3(departure, arrival, n, next_x, time)

とすることで、timeを更新しています。
動作がわかりにくい場合は、n=3くらいの条件で紙に書き出してみると分かるかもしれません。
結果は、インスタンス変数を使用した場合と同じになるので割愛します。

timeを更新する方法_その3(配列の要素変更が破壊的メソッドであることを悪用する)

最後の方法は、departureとarrivalが結果的に更新されていることから思いついた方法です。
明らかにinteger型であるtimeを、わざわざarray型で宣言するので、筋が悪いです。
おまけです。

n = 10
departure = Array.new(n, 0)
arrival = Array.new(n, 0)
x = 0
time = Array.new(1, 0)

def count_time_v4(departure, arrival, n, x, time)
  time[0] += 1
  departure[x] = time[0]

  if x < n - 1
    next_x = x + 1
    count_time_v4(departure, arrival, n, next_x, time)
  end

  time[0] += 1
  arrival[x] = time[0]
end

count_time_v4(departure, arrival, n, x, time)

n.times do |i|
  puts "#{i + 1} #{departure[i]} #{arrival[i]}"
end

まとめ

今回は、値渡しと参照渡しの違い、rubyでのメソッドの引数に指定した変数の挙動、参照渡しのような挙動をさせるにはどうするかという内容でした。
前半戦で様々なパターンを上げすぎて、後半戦の説明が薄い気もしていますが、メソッドで変数をいじる際に初心者がハマりそうなポイントは説明できたと思います。
(実際、ハマった結果が後半戦最初の、ダメな例です)
少々長くなってしまいましたが、最後までお付き合いいただきありがとうございました!

ActiveStorageのN+1問題

こんにちは!
毎週の更新を頑張りたいのですが、ブログ以外の進捗がメインなので、隔週に頻度を下げるかもしれません。。。
最近は、外で学習するときはゲーム音楽のサントラを聴いています。
ボーカル無い方が集中できる+戦闘曲はテンション上げられるのでおすすめです。
ちなみに今は、不思議の幻想郷シリーズのサントラです。
東方二次創作のローグライクゲームですね。

はい、本題!
今回は、やや調べるのに時間をかけてしまったActiveStorageのN+1問題を解消する方法を書きます。
ActiveStorageは画像データなどをデータベースに保存する時に使います。
N+1問題は、アソシエーションで関連づけたデータを呼び出す時にallを使用していると、元のデータへのアクセスに1 回、関連づけられたN個の要素を呼び出すためにN 回、合計(N+1) 回データベースにアクセスしてしまう問題です。
投稿サイトで投稿と投稿者名を表示する時などに発生します。
この場合は、投稿数をNとすると、投稿の取得に1回、投稿に関連づけられた投稿者名を投稿回数分取得しにいくのでN回、合計(N+1)回のデータベースへのアクセスとなります。
そのため、投稿が増えれば増えるほどデータベースへのアクセス回数が多くなり、ページの表示に負荷がかかってしまします。
解決方法は、includeメソッドを使用することです。
1回のアクセスで関連するデータを全て取得してくるようになるので、データベースへのアクセスは(1+1) 回ですみます。
ただし、ActiveStorageではincudesメソッドをそのままの形では使えません。。。
ではどうするか。結論としては、一次資料を見れば一発解決!でした。。。
ActiveStorageのReadmeに書いてあります
ほら。。。なんで気がつかなかったんだろう。。。

    # Use the built-in with_attached_images scope to avoid N+1
    @messages = Message.all.with_attached_images

これで解決なのですが、これでは芸が無いので実際にミニアプリを作成して挙動を確認してみます。
今回作成するミニアプリは、メッセージと複数枚の画像を同時に投稿できるミニアプリです。
トップページに、今までに投稿されたメッセージと画像が表示されます。
環境は、以下のとおりです。
ruby 2.6.5
rails 6.0.3.3
mysql使用

新規作成

rails _6.0.0_ new アプリ名 -d mysql

で新規作成します。

ActiveStorageの導入

続いて、ActiveStorageを導入します。

rails active_storage:include
rails db:migrate

これで、データベースにActiveStorageのテーブルが作成されました。
active_storage_attachmentsとactive_storage_blobsができているはずです。

f:id:kojiprg:20200916160953p:plain
ActiveStorageのテーブルは上の2つです。

messageモデルの作成

投稿されたメッセージを保存するために、messageモデルを作成します。

rails g model message

作成されたマイグレーションファイルを編集し、メッセージを保存するためのcontentカラムを追加します。

class CreateMessages < ActiveRecord::Migration[6.0]
  def change
    create_table :messages do |t|
      t.text :content, null: false # この行を追加

      t.timestamps
    end
  end
end

マイグレーションファイルを編集したら、

rails db:migrate

を忘れずに。

f:id:kojiprg:20200916162014p:plain
messagesテーブル作成。contentカラムが追加されている。

アソシエーションの追加

messageモデルとActiveStorageを関連付けます。
app/models/message.rbに以下の文言を追加するだけ。

has_many_attached :images

今回は複数枚の画像の保存ということで、has_many_attachedを使用しています。
一枚だけで良い場合は、has_one_attachedを使用してください。
これで、messageモデルに対してimagesを保存しようとすると、ActiveStorageに保存されるようになりました。

トップページの作成

コントローラとトップページのビューを作成します。

rails g controller messages index

これで、messagesコントローラと対応するindexビューが作成されます。
今回は、このindexビューをルートに設定します。
config/routes.rbを編集します。

Rails.application.routes.draw do
  root 'messages#index'
end

これで、サーバを立ち上げてローカルホストに接続すれば、以下の画面が表示されるはずです。

f:id:kojiprg:20200916163157p:plain
トップページへのアクセス確認。表示はこれから編集します。

トップページの編集(投稿機能と表示機能の実装)

routes.rb、messages_controller.rb、index.html.erbを編集します。

routes.rb

Rails.application.routes.draw do
  root 'messages#index'
  resources :messages, only: :create
end
messages_controller.rb

class MessagesController < ApplicationController
  def index
    @message = Message.new
    # まずは、何も考えずにallでデータベースから情報を取得します。
    @messages = Message.all
  end

  def create
    @message = Message.new(params_message)
    if @message.save
      redirect_to root_path
    else
      render :index
    end
  end

  private

  def params_message
    # images: []として配列であることを明示
    params.require(:message).permit(:content, images: [])
  end
end
index.html.erb

<h1>複数枚画像投稿</h1>
<%= form_with model: @message, url: messages_path, local: true do |f| %>
  <%= f.text_field :content %>
  <%# name: 'message[images][]'として、imagesを配列として明示 %>
  <%= f.file_field :images, name: 'message[images][]' %> 
  <%= f.file_field :images, name: 'message[images][]' %>
  <%= f.file_field :images, name: 'message[images][]' %>
  <%= f.submit %>
<% end %>
<h3>投稿されたコメントと画像</h3>
<% @messages.each do |message| %>
  <p><%= message.content %></p>
  <div class='image-box'>
    <% message.images.each do |image| %>
      <%= image_tag image, class: 'image' %>
    <% end %>
  </div>
<% end %>

だいぶ説明を端折っていますが、createアクションのルーティング、@messageに新規の投稿を格納、@messagesに今までの投稿を格納、あとは投稿フォームの作成をしています。
今回は省略していますが、バリデーションの設定もした方が良いです。
ポイントとしては、以下の2点です。

  1. まずは、何も考えずに@messages = Message.allで今までの投稿を取得します
  2. 複数の画像を保存するために、imagesをarray型で明示しています。

これで、トップページが完成しました。

f:id:kojiprg:20200916165433p:plain
完成したトップページ

メッセージと画像を投稿します。
2つのメッセージと、それぞれのメッセージに3枚ずつ合計6枚の画像を投稿しました。

f:id:kojiprg:20200916165609p:plain
メッセージ2つと6枚の画像

ログの確認

サーバを立ち上げているターミナルでログを確認していきます。
ちなみに、私はvscode上でターミナルを起動しています。
まずは、allで取得してきた場合。

f:id:kojiprg:20200916170010p:plain
allを使用した場合の、データベースへのアクセス。画像を1つずつ拾ってきています。

水色の箇所がデータベースにアクセスしている部分です。
メッセージの取得に1回、画像の取得に8回アクセスしています。
ActtiveStorageのテーブルへのアクセスは、まずactive_storage_attachmentsでどのblobを引き出せば良いか確認(1回)、active_storage_blobsからデータの取得(1つのメッセージにつき3枚の画像があるので3回)をしています。
今回はメッセージが2件なので、(1 回+3 回)/件 * 2件 = 8 回のアクセスになります。
メッセージと画像の投稿が増えていくにつれ、データベースへのアクセスが増えてしまうN+1問題です。
これを解消するのが、with_attached_(アソシエーションで設定した名前)です。
messages_controller.rbを以下のように編集します。

    # N+1問題解決版です。allをやめてwith_attached_imagesを使います。(allは残してMessage.all.with_attached_imagesとしてもokです)
    @messages = Message.with_attached_images

内部的には

includes(images_attachments: :blob)

を呼んできているので、上記のように書くこともできます。
お好みで使い分けてください。
この状態で、もう一度ログを見てみます。

f:id:kojiprg:20200916171620p:plain
with_attached_imagesを使用した場合。N+1問題が解消されてすっきりしました。

メッセージ、active_storage_attachments、active_storage_blobsに1回ずつ合計3回のアクセスになりました。
これで投稿が増えても安心です。

まとめ

ActiveStorageのN+1問題を解消する方法でした。
has_one_attachedの時にも、基本的には同様の方法で解消できます。
N+1問題は表には出てこない部分なので、意識していないと忘れてしまいがちです。
安易にallを使用しない、データベースへのアクセスが無駄に多く無いかログを確認するようにして気付けるようにしていきたいです。
最後までお付き合いいただきありがとうございました!

HEROKUでアプリケーションを公開する

こんにちは!
今回は、アプリケーションのデプロイについて書いていきます。
デプロイとは、作成したアプリケーションをweb上で公開することです。
通常、デプロイには、サーバやOS、データベースなどを準備する必要があります。
HEROKUはこれらのデプロイに必要なものを準備してくれるサービスです。
そして、HEROKUはアプリケーションのソースコードを管理するためのサービスであるgithubと連携しているため、
githubを通じてアプリケーションをHEROKUに送るだけでデプロイすることができます。
しかも、基本無料!月の利用時間に制限があるため複数のアプリを常時稼働はできない、30分でスリープするため初期のアクセスに数十秒かかるなど制約もありますが、個人で試しに使う分には無料枠でも問題ありません。
なお、今回の記事ではgithubは導入ずみ、、アプリもできている、普段使用しているデータベースはMySQLという前提です。
(完全に個人の都合)

HEROKUの登録

まずは、HEROKUでユーザ登録をします。
HEROKU
右上の新規登録をクリックするとフォームが出てくるので入力します。

f:id:kojiprg:20200902181253p:plain
HEROKUのユーザ登録画面

入力したメールアドレスにリンクが送られてくるので、そこからパスワードの設定をしたら完了です。

クレジットカードの登録

HEROKUのデフォルトのデータベースはPostgreSQLという種類です。
データベースの種類を変更するためには、クレジットカード情報を登録しておく必要があります。
ユーザ登録とログインが完了すると、アプリの管理ページに飛ばされると思います。
そこで、右上の忍者みたいな人をクリックするとメニューが開くので、「Account settings」をクリックします。

f:id:kojiprg:20200902184947p:plain
クレジットカード登録画面へ移動する

設定画面に移動したら、「Billing」を選択すると一番上に「Billing Information」と出てくるので、ここからクレジットカードの登録をおこなってください。

f:id:kojiprg:20200902185421p:plain
ここからクレジットカード登録

HEROKU CLIの導入

続いて、ターミナルでHEROKU関連のコマンドを実行できるようにするために、HEROKU CLIを導入します。
ターミナルで以下のコマンドを実行するだけです。

brew tap heroku/brew && brew install heroku

導入が完了したら、

heroku -v

と入力して、herokuのバージョンが表示されれば成功です。

ターミナルからHEROKUへのログイン

heroku login

として、ターミナルからHEROKUへログインします。
この時、

heroku: Press any key to open up the browser to login or q to exit: 

と表示されるので、適当にキーを入力するとブラウザが立ち上がりログイン画面になります。
ログインをクリックして、ログイン情報を入力すると右の画面のようになるので、ブラウザは閉じてしまって大丈夫です。

f:id:kojiprg:20200902190810p:plainf:id:kojiprg:20200902190824p:plain
ログイン画面とログイン完了画面

ターミナルでは以下のように表示されて、ログインに成功したことがわかります。

›   Warning: If browser does not open, visit
 ›   https://cli-auth.heroku.com/auth/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com

ブラウザを開かずに、ターミナルでログイン情報を入力したい場合には、

heroku login -i

としてください。同様にメールアドレスと、パスワードを入力すればログインできます。

アプリのディレクトリに移動して、HEROKU上でアプリケーション作成

cdコマンドでデプロイしたいアプリケーションのディレクトリに移動したら、次のコマンドでHEROKU上にアプリケーションを作成します。

heroku create アプリ名

データベースをMySQLに変更する

HEROKUにはアドオンと呼ばれる追加機能が豊富に用意されています。
MySQLを使えるようにするには、clearDBのアドオンを追加します。

heroku addons:create cleardb:ignite

アドオンが追加できました。
次のコマンドで、データベースのurlを確認します。

heroku config | grep CLEARDB_DATABASE_URL

#こんなのが返ってきます
CLEARDB_DATABASE_URL => mysql://adffdadf2341:adf4234@us-cdbr-east.cleardb.com/heroku_db?reconnect=true

このurlをHEROKU上で設定することで、MySQLのデータベースが使用できるようになります。
gemでmysql2を使用している場合は、mysqlをmysql2に書き換えて設定してください。

heroku config:set DATABASE_URL='mysql://adffdadf2341:adf4234@us-cdbr-east.cleardb.com/heroku_db?reconnect=true'

mysql2なら

heroku config:set DATABASE_URL='mysql2://adffdadf2341:adf4234@us-cdbr-east.cleardb.com/heroku_db?reconnect=true'

鍵の設定

もう少しで設定が完了です!
Railsでは外部に漏らしたくない情報を暗号化しています。
それを復号するためにmaster.keyというファイルがあるのですが、通常はgithubに上がらないようになっています。
そこで、このmaster.keyの中身を環境変数に入れておきます。
他にも、開発環境で設定した環境変数がある場合には、HEROKUでも設定しなおします。

heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

アプリケーションをgitからHEROKUへ

このコマンドを打てば、ローカルリポジトリのmasterブランチの内容がHEROKUへプッシュされます。
事前にcommitやpullを忘れないようにしましょう。

git push heroku master

データベースのマイグレーション

ついに、最後です!
あとはデータベースをマイグレーションしましょう。
通常のコマンドの前にheroku runをくっつけます。
これで、HEROKUにプッシュされたアプリケーションに対して、マイグレーションが実行されます。

heroku run rails db:migrate

できた〜!

アプリのurlを確認

デプロイしたアプリケーションのurlは次のコマンドで確認できます。

heroku apps:info

この一番下に、「Web URL」という項目があるので、そのurlにアクセスしてください。
デプロイしたアプリケーションがあるはずです。

アプリを更新したら。。。

今回は手動で更新する方法を紹介します。
と言っても、ターミナルからログインして、

git push heroku master

をするだけです。
データベースをいじっている場合は、マイグレーションも忘れずに実行してください。


最後までお付き合いいただき、ありがとうございました!

capybaraとrspecを使ったテストコード

こんにちは!
今回は趣向を変えて、ヲタクトークから始めます。
本日8/26は私が好きなバンド、PEDROの2nd フルアルバム「浪漫」の発売日です!
リード曲「浪漫」のMVを置いておきます。
www.youtube.com
初期の「自律神経出張中」みたいな、世の中にツバかけるような曲も好きですが、最近は「生活革命」のように"ふとした幸せ"みたいな曲が増えてて、好きが増しています。
「浪漫」は後者ですね。
早くライブに行きたい。。。

では、今回のコンテンツです。

本題と概要

最近Rspecでテストコードを書く練習をしています。
今回の記事では、アプリを使うときの一連の動作をテストコードで模した時に、私が気になったポイントを何点か紹介します。
なお、上記のテストは「結合テスト」とか、「統合テスト」とか、「エンドツーエンドテスト」などと呼ばれています。

rails g rspec:system hoges

みたいにするとRSpec

spec/system/hoges_spec.rb

を作ってくれます。(gem 'rspec-rails'がインストールされている前提です。)
テストコードで、ブラウザ操作を実行する時には、Capybaraというgemの機能を使っていくのですが、このメソッドの種類がかなりあります。
下記のサイトでほとんど網羅されているので、操作に対応するメソッドを忘れた時には、ここを調べています。
qiita.com

Basic認証の突破

Basic認証とは、あらかじめ設定したユーザ名とパスワードを要求するだけの簡単な認証機能です。
本番環境では使わないので、コメントアウトしておけば良いと思います。
が、どうしてもテストを突破したい場合は、下記のようにするとできます。

user_name = 'user_name'
password = 'password'
visit "http://#{user_name}:#{password}@#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}#{path}"

「user_name」と「password」には、自身で設定した変数や文字を入れてください。
毎回最初に必要になるので、サポートモジュールを使用して、メソッドにしておくと便利です。

deviseのログイン

deviseを使っていると、毎回ログイン処理が必要になります。
メソッドを自身で作っても良いのですが、公式ですでに準備されています。
How To: Test with Capybara · heartcombo/devise Wiki · GitHub
使い方を簡単にまとめると

spec/rails_helper.rb

にこう書きます。(RSpec.configure do~endは33行目くらいに準備されているので、do~endの中に記載します。)

RSpec.configure do |config|
  config.include Warden::Test::Helpers
  config.after :each do
    Warden.test_reset!
  end
end

これで、ログイン処理のメソッドlogin_asが使えるようになります。
また、各テストの最後にログイン状態のリセットを実施してくれます。
あとは、テストコードの最初で、login_asを使ってログインするだけです。

user = User.create!(email: 'test@example.com', password: 'f4k3p455w0rd')
login_as(user, scope: :user)

FactoryBotなどのあらかじめユーザ情報を作成しておくgemを使っていれば、もう少し簡単になります。

user = FactoryBot.create(:user)
login_as(user, scope: :user)

注意点は、あくまでプログラム上でログイン処理をするだけのため、ログイン状態でページの表示が変わるような場合、login_asだけでは表示が変わりません。

visit root_path

などで、一度画面のリロード・遷移をする必要があります。
login_as後に、visitすればokです。

データベースに保存した画像が表示されていることの確認

データベースに保存されている画像のurlをみてみると以下のように???の感じになっています。
img class="item-image" src="http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--531d820f23be7783715afe29f9c98a9d5f33caaa/output-image1.png"
have_selectorマッチャを使用することで、img要素の存在を確認することはできるのですが、どうせなら「output-image1.png」というファイル名の一致まで確認したいです。
以下のようにするとできます。

expect(page).to have_selector('img[src*="output-image1.png"]')

これは、「output-image1.png」と部分一致するsrc属性を持つimg要素が存在するかを確認しています。
=を*=にすることで、完全一致ではなく、部分一致となっています。
他にも、
^=とすることで前方一致、
$=とすることで後方一致の検索ができます。

最後までお付き合いいただき、ありがとうございました!

draw.ioを使ってER図を作ってみた

こんにちは!
今週は、データベースの設計をするにあたり、テーブル間の関係や保存すべき情報(エンティティ)を洗い出せているかを見やすくするために作成する、ER(Entity-Relationship)図というものについて書きました。
ER図の簡単な説明と、フリーのツールdraw.ioを使ってみた使い方や感想となっています。

ER図

まずは、ER図の簡単な説明です。
ERD(Entity-Relationship-Diagram)とも呼びます。
ER図は下の画像のような、テーブル名とカラムをまとめた箱とテーブル間の関係を示した線から構成されています。

f:id:kojiprg:20200819125448p:plain
ER図の例

なお、上の画像ではカラム名だけでなく、型や制約も一緒に記載しています。
データベースを作成するために必要な情報なので、ER図に含めた方が良いと思います。
また、カラム名の左側にPKやFKといった文字があるカラムがありますが、PKが主キーを、FKが外部キーを表しています。
主キーとは、テーブルに保存されているデータを一意に識別するためのカラムです。
通常は、idが主キーとして扱われていると思います。
外部キーは、他のテーブルと関連付けて扱うためのカラムです。
上の画像の例では、どの顧客の注文かを判断するために、customersテーブルと、ordersテーブルを関連づける必要があり、ordersテーブルのなかに外部キーとしてcustomers_idが含まれています。

テーブル間を結ぶ線の形で、一人の顧客に対して注文が複数存在できるのか、それとも注文は必ず一つなのかといった、数に関する関係を示しています。
1対多、1対1のアソシエーションと呼ばれる関係ですが、詳しい説明は割愛します。
上の画像の場合、一人の顧客に対して複数の注文が存在できるため、customersテーブル側は1を意味する縦棒が入っており、ordersテーブル側は0~複数を示す、丸とフォークのような枝分かれが書かれています。

draw.io

ここからは、実際にER図を作成するツールを使ってみたので、使い方や感想をまとめます。
ER図をフリーで作成できるソフトはいろいろありますが、今回はこちらの記事で紹介されていた、draw.ioを使ってみました。
まず、サイトにアクセスするとどこに保存するかを聞かれます。

f:id:kojiprg:20200819135558p:plain
drawの保存先

google driveやone drive以外にも、dropboxgithubも選べます。
ちなみに、デフォルトだと説明が英語で出てきます。(スクリーンショットを撮り忘れました。。。)
日本語への設定は、後述します。
保存場所を指定すると、新規作成か保存済みデータの編集かを聞かれるので、初めてなら新規作成を選びます。
すると、以下のようにテンプレートの一覧が出てきます。
ER図専用のツールではないので、いろいろ出てきます。

f:id:kojiprg:20200819135846p:plain
テンプレートの一覧

一番上のボックスでファイル名を作成します。
拡張子も変えられますが、後からpng出力などもできるのでデフォルトのままで良いと思います。
テンプレートは、白紙ファイルで問題ないですが、今回はERDで作成してみます。
すると、以下のような画面が開きます。

f:id:kojiprg:20200819140351p:plain
新規作成開始画面

左側にボックスなどの一覧、中央に編集シートがあり、右側で選択したボックスのプロパティなどを編集できます。
ER図のボックスや線は左側のERと書かれた場所に格納されています。
また、右上の地球マークをクリックすると言語の一覧が表示され、言語を変更することができます。
日本語は下から三番目くらいにあります。
よくある「japanese」ではなく、「日本語」で登録されています。

左側のERをクリックすると、下図のように一覧が表示されます。
たくさんありますが、基本的には左上角のTable1と下側に集まっている線しか使いません。
使いたい要素をクリックすることで、中央の編集シートに配置されます。

f:id:kojiprg:20200819141357p:plain:h400
ER図作成に使用できるボックスなど

要素を配置したら、ドラッグで好きな位置に配置することができます。
線の場合は、選択すると下図のように両端が青くなるので、そこをドラッグしてテーブルのボックスに近づけると接続させることができます。
パワポと同じですね。

f:id:kojiprg:20200819142220p:plain
選択された線は両端が青くなる。ここをドラッグ。

テーブル名や、カラム名の編集は、編集したいテーブル→編集したい名前とクリックで選択していきます。
テーブルの中にテキストボックスが複数配置されているような構造です。
テーブルやカラムを量産したいときは、command+Dで複製できます。
特にカラムを複製すると、下図のように勝手にテーブルのサイズも調整してくれるので便利です。

[f:id:kojiprg:20200819142354p:plain:]f:id:kojiprg:20200819142434p:plain
カラムの複製

あとは、pngなどへの書き出しですが、google driveの場合は、左上のファイルから、形式を指定してエクスポートで可能でした。

f:id:kojiprg:20200819143602p:plain
画像ファイルへの書き出し

以上、簡単に使い方をまとめてみました。
使ってみた感想ですが、テーブルやアソシエーションのための線を配置する機能と、そのみためを編集するための機能のみなので、直感的に使いやすいと思いました。
ユーザ登録も必要なく、無料で使用できるので、特殊な機能を必要とせず、すぐにER図を作成したいという方にはおすすめです。

最後までお付き合い、ありがとうございました!