20200528のRailsに関する記事は21件です。

[Rails]N+1は悪!発生したらとりあえず解消せよ!!という考えは危険

N+1

それは諸悪の根源!パフォーマンスの敵!!
見つけたらすぐに撃退すべき悪しき存在です!!!

と思われている方が多いと思います。

実際その通りで、ネットで調べてもN+1を解消するノウハウが溢れています。

基本的にはノウハウ通りに修正すれば良いのですが、まれにN+1は解消しない方が良いパターンもあるので具体例を交えて紹介します。

この記事で話したいこと

この記事で話したいことは、なぜN+1は直すべきなのか?ということです。
N+1はあまりに有名すぎて直すノウハウはたくさんありますが、なぜ直すべきなのかが忘れられている感があります。
1つ言っておきたいのは、N+1を直すのはクエリー発行数を減らしたいからではないです。パフォーマンスを改善したいからです!
言い換えるとクエリー数が減ってもはフォーマンスが改善しないのであればN+1を直す必要はないのです。

N+1とは

最初に典型的なN+1を復習しておきましょう。

下記のモデルを使って説明します。

  • ユーザー(user)は複数の記事(articles)を持っている
  • 記事には複数の写真(images)を添付することができる
class User
  has_many :articles
end

class Article
  belongs_to :user
  has_many :images
end

class Image
  belongs_to :article
end

このモデルを使って特定ユーザーの記事の一覧を取得するAPIを考えてみます。
レスポンスは下記の通り

{
  articles: [
    id: 1
    body: "hogehoge"
    images: [
      {
        id: 1
        alt: "alt"
        src: "https://example.com/hoge1.img"
      }
    ]
  ]
}

これをみんな大好き(?)Jbuilderを使うと下記のような実装になると思います。

class ArticlesController
  def index
    # 更新日の降順に10件取得(using kaminari)
    @articles = Articles.where(user_id: params[:user_id]).order(updated_at: :desc).page(params[:page]).per(10)
  end
end

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      json.array!(article.images) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

上記を実行するとどうなるでしょうか?
下記のようにimagesテーブルを取得するSQLがarticlesの数だけ発行されます。

-- articles取得は1クエリー
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

-- articlesの数だけimages取得クエリーが発行される
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10

これを解消するための機能がRailsには備わっています。
それがpreload, eager_load, includesですね。
これらの細かい使い方はこの記事では省略しますが、今回はpreloadを使ってN+1を解消します。

class ArticlesController
  def index
    # preload(:images)追加
    @articles = Articles.where(user_id: params[:user_id]).preload(:images).order(updated_at: :desc, id: :desc).page(params[:page]).per(10)
  end
end
-- articles取得は1クエリー
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

-- images取得も1クエリー
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

クエリーは発行するコストが高いのでクエリー数を減らすことでパフォーマンスを大幅に改善することができます。
今回の例でも11クエリーが2クエリーと大幅に減っています。

N+1撃退完了!!

N+1を解消させない方が良いパターン

上記の例で無事にN+1が解消しましたが、もしimageを大量に持っているユーザーがたくさんいるとしたらこの対応は本当に良かったのでしょうか?
極端な例になってしまいますが、1つのarticlesあたり平均1,000枚のimageが紐づいている場合を考えてみましょう。

この場合、上記のレスポンスのままだと1,000 * 10 = 10,000のimageを返すことになってしまい、レスポンスが巨大になりすぎてパフォーマンスが劣化します。
そこで大抵の場合は記事一覧ではimageの一部(先頭5件だけとか)を返却する仕様に変更しようとかなるわけです。

では仕様変更してみましょう。
コントローラーはそのままでjbuilderの箇所を変更してみました。

# articles/index.json.jbuilder
json.articles do
  json.array!(@articles) do |article|
    json.id article.id
    json.body article.body
    json.images do
      # 最初の5件だけ取得
      json.array!(article.images.first(5)) do |image|
        json.id image.id
        json.alt image.alt
        json.src image.src
    end
  end
end

これで実行すると無事レスポンスのimageはarticleごとに5件までになります。
めでたしめでたし...とはなりません!!!!

どこが問題でしょうか?
クエリーを見ても1つ前の例と同じ2クエリーしか発行されていません。

-- articles取得は1クエリー
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

-- images取得も1クエリー
SELECT `images`.* FROM `images` WHERE `images`.`article_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

問題点は分かりましたか?
今回の例では前回N+1を解消したimagesのクエリーが問題になります。
今回の前提条件で1つのarticlesに1,000枚のimageが紐づいているとしています。
ということは、このimagesのクエリーでは10,000枚のimageオブジェクトが取得されていることになります。
ActiveRecordは便利な反面、オブジェクトのサイズがとても大きいです。そのオブジェクトを10,000個生成したらかなりのメモリを消費します。
しかも今回は先頭の5枚しか使いません。

ではpreloadを外してN+1解消前の状態に戻すとどうなるでしょうか?

-- articles取得は1クエリー
SELECT `articles` FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`updated_at` DESC LIMIT 10

-- articlesの数だけimages取得クエリーが発行される
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 1 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 2 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 3 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 4 limit 5
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 5 limit 5
...
SELECT `images`.* FROM `images` WHERE `images`.`article_id` = 10 limit 5

imageのクエリーはarticlesの数だけ発行されるように戻ってしまいますが、それぞれ5件しか取得しないのでActiveRecordオブジェクトの生成数は10,000個から50個に大幅に減少します。

実行環境の性能に左右されるので絶対とは言えませんが、N+1を解消するよりメモリーを節約した方がパフォーマンスが上がることはよくあります。
(※実際にどちらの対応が適切かは実行環境と同等の環境でパフォーマンス検証しないと分かりません)
もしN+1は悪!絶対に直さないといけないと言う考えがあると、上記のようにimagesのクエリーが発行されているのをみるとpreloadをつけてしまい、メモリ使用量を大幅に上げてパフォーマンス劣化を招いてしまうかもしれません。

最後に

N+1はとても有名で目につきやすく、しかもRailsだとpreloadなどを付けるだけでさくっと解消させることができるため、深く考えずにとりあえず解消させるということがよくあると思います。
ただ今回の例のようにhas_manyを先読みしておく場合はどれくらいの件数が見込まれるかを考えてから実装するようにしましょう。
件数を考慮せず全部取得してしまうとメモリー不足を引き起こしてパフォーマンス劣化や最悪の場合にはサーバーをフリーズさせてしまう可能性があります。

あと、最初にも少し書きましたがN+1を直す目的をきちんと認識しましょう。
N+1を直した時にクエリーが減ったことを確認して対応完了にすることはありませんか?
N+1を解消するのは発行クエリーを減らすためではなくパフォーマンスを改善させるためです。
ただクエリーが減ったことだけを確認するのではなく、きちんとパフォーマンスが改善していることを確認するようにしましょう。

パフォーマンスを改善させるという観点で考えるとN+1の他にもメモリー利用量やループなど処理回数の削減など様々な観点があります。
N+1はあくまでパフォーマンス改善の1つの手であるということを忘れず、他の観点も頭に入れておくようにしましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]strftimeあれこれ

前置き

現在Railsでマッチングアプリ作成中ですが、strftimeをよく使うので、すぐ見れるよう書き方をまとめておきます。

strftimeって?

日時データを好きなようにビューに表示出来るメソッドです。

やってみよう

今回はcreated_atでやっていきます。

デフォルトだとこうですね。後ろの+0900は使わなさそうですね。
2020-05-22 16:11:01 +0900

created_at.strftime("%Y-%-m-%-d %-H:%M")

# 表示 2020-5-28 20:25

こんな風に文字列変換出来ます。
適当に記号並べているわけではありません。笑

下記に書式を一部添付しておきます。
もっと知りたい方はこちらにあります。
引用元pikawaka

書式 返り値 説明
%Y 2019 西暦を4桁の数で取得する※西暦1年は「0001」
%m 01 月を必ず2桁の数字で取得する(01-12)
%-m 1 月を1桁か2桁の数字で取得する(1-12)
%d 01 日付を必ず2桁で取得する(01, 02 ... )
%-d 1 日付を1桁か2桁で取得する(1, 2 ... 10, 11 ...)
%H 12 24時間制の時刻を必ず2桁で取得する(00-23)
%-H 12 24時間制の時刻を1桁か2桁で取得する(0-23)
%I 12 12時間制の時刻を1桁か2桁で取得する(1-12)
%M 00 分を取得する(00-59)
%S 00 秒を取得する(00-60)※60はうるう秒

ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリで初めて自作ヘルパーを使ってリファクタリングした話

はじめに

今までRuby on Railsでアプリケーションを作成していて
ふと気になったことがありました。

コントローラー作成時に毎回ついてくるコイツ
〇〇_helper.rb
このファイル何に使うんだろ...。
ヘルパーって書いてあるからヘルパーメソッドに関係しているのかな...??

この時、私は
"わざわざ同時に作成される物だからきっと便利なファイルに違いない"
と、興味の向くままにQiitaで解説記事などを調べてみました。

結論

自作のヘルパーメソッドを記述するファイルのようです。
(自作ヘルパー == カスタムヘルパー と言うらしい)
ヘルパーメソッドは今日の今日までGemに用意されているモノしか使用したことがなかったので
これは便利そうだ、と直感で思いました。

さっそく使ってみた

他の方のQiita記事を参考にさせていただき
今回は個人開発したアプリのコードリファクタリングに使用してみました。
コントローラー内のリファクタリング内容を本記事の内容にします。

環境

Rails 5.2.3
Ruby 2.5.1

使い方

まずはリファクタリングをするコントローラーを見ていきましょう。

users_controller.rb
class UsersController < ApplicationController

~省略~

  private
  def set_user
    @user = User.find(params[:id])
  end

  def check_user
    redirect_to new_user_registration_path, alert: 'ログインまたは新規登録をお願いします' unless current_user.id == @user.id
  end
end

check_userというメソッド内のunless以下がイケてないので
ヘルパーメソッドを作成してリファクタリングします。


helpers/users_helper.rb
module UsersHelper

  def current_user?(user)
    current_user.id == user.id
  end

end

current_user?というヘルパーメソッドを作成しました。
このメソッド名および内容はけっこう定番のようです。


helpersというディレクトリ直下に
application_helper.rbというファイルもありますが
これは複数のコントローラーやビューに適用させるヘルパーメソッドを管理するファイルのようです。
※今回は未使用


さっそく作成したヘルパーメソッドをコントローラー内に適用させましょう。

users_controller.rb
class UsersController < ApplicationController
  include UsersHelper

~省略~

  private
  def set_user
    @user = User.find(params[:id])
  end

  def check_user
    redirect_to new_user_registration_path, alert: 'ログインまたは新規登録をお願いします' if !current_user?(@user)
  end
end

説明

コントローラー内でhelperファイルの自作ヘルパーを使う際は
2行目のようにincludeで読み込み必要があるそうです。
※筆者はコントローラー名とヘルパーファイル名が対応してるから勝手にやってくれるんやろうな
と、Rails特有の処理が入ると勝手に思い込んでいましたが記述が必要なようです。


リファクタリング内容ですが
set_userで定義された@userを引数に渡して
先ほど作成したcurrent_user?メソッドを使用しました。

また、unlessという記述もif !~という記述に変更しました。
これで少しはコードがスッキリしました。




同じような内容でposts_controllerもリファクタリングします。

posts_controller.rb
class PostsController < ApplicationController


  def edit
    redirect_to new_user_registration_path, alert: 'ログインをお願いします' unless current_user.id == @a_post.user_id
  end

  def destroy
    redirect_to new_user_registration_path, alert: 'ログインをお願いします' unless current_user.id == @a_post.user_id
    if @a_post.destroy
      redirect_to root_path, notice: '投稿を削除しました'
    else
      render :show, notice: '投稿が削除できませんでした'
    end
  end


  private

  def set_post
    @a_post = Post.find(params[:id])
  end
end

こちらもunless以下の記述を修正していきます。

まずはヘルパーメソッドを作成。

helpers/posts_helper.rb
module PostsHelper

  def current_user_post?(a_post)
    current_user.id == a_post.user_id
  end

end

これをコントローラーに適用していきます。

posts_controller.rb
class PostsController < ApplicationController
  include PostsHelper

  def edit
    redirect_to new_user_registration_path, alert: 'ログインをお願いします' if !current_user_post?(@a_post)
  end

  def destroy
    redirect_to new_user_registration_path, alert: 'ログインをお願いします' if !current_user_post?(@a_post)
    if @a_post.destroy
      redirect_to root_path, notice: '投稿を削除しました'
    else
      render :show, notice: '投稿が削除できませんでした'
    end
  end

  private

  def set_post
    @a_post = Post.find(params[:id])
  end
end

こちらもヘルパーを読み込んでリファクタリングしたら完了です。

最後に

今回はコントローラーに絞って記事を書きましたが自作ヘルパーはもちろんビューにも使用できます。
また、ビューに使用する場合はコントローラーと違って、読み込みの記述が必要ないみたいですね。
私もこのあとビューファイルのコードをリファクタリングしようと思います。

どこか不備がありましたらコメントにてご指摘いただけると幸いです。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人的に頻繁に使ったRSpec集

RSpecを書いているうちに、「これとよく似た比較書いたけどどのプロダクトのどのSpecだっけ?」って探して時間を取られた事があったので、ここに記事としてまとめておきます。未来の自分と他の開発者の方に役立つ事を願って。

エラーログが出力されている事を確認。

it 'エラーログが出力される事' do
    expect(Rails.logger).to receive(:error).with(/code: 400/)
    # 以下、テストしたい処理を書く
end

ポイントとしては

  • expectをitの一番最初に書く。こうしないとログの出力メソッドがreceiveによって捕捉されません。
  • withの中に、含まれていてほしいメッセージを書く。そのまま書くと完全一致にならないとパスしないので、スラッシュで囲むことで、特定のメッセージを含んでいた場合のケースを書くことができます。いわゆる正規表現。

参考にしたサイト

エラーログが出力されていない事を確認。

it 'エラーログが出力されない事' do
    expect(Rails.logger).to receive(:error).exactly(0).times
    # 以下、テストしたい処理を書く
end

先程の例の応用で、「正常に処理が完了するケースの場合はエラーログが出ないはずだ。」も調べたかった。
receiveメソッドにつなぐ形で回数を指定できるので、.exactly(0).timesでメソッドが呼ばれていない事を確かめました。
参考にしたサイト

よくRubyやRailsを書く人からしてみれば「当たり前じゃねぇか!」って言われるかもしれませんが、私がここ最近初めて本格的にRailsを使った開発にアサインされて、戸惑いながらも開発していた時に得たものなので、優しい目で見ていただけますと幸いでございます。
使えそうなものが出てきたら追記していこうと思います。

ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby 自作クラスの配列をソートする

Ruby 自作クラスの配列をソートする

100日後に1人前になる新人エンジニア(8日目)です

あれ自作したクラスの入った配列ってどうやってソートするんだっけ...
って今日ちょっと詰まってしまったのでメモとして残しておきます。

自作のクラス

user.rb
class User
  attr_accessor :name, :age, :height

  def initialize(name, age, height)
    @name = name
    @age = age
    @height = height
  end
end

user1 = User.new("foo", 15, 190)
user2 = User.new("bar", 80, 150)
user3 = User.new("baz", 28, 168)
users = [user1, user2, user3]

こんな感じで自作のユーザークラスを作りそれらをusersという配列に入れる
このusersの並べ替えについて今日は考える

とりあえずソートしてみる

ruby.rb
users.sort
#エラーが発生
#`sort': comparison of User with User failed (ArgumentError)

上のように怒られます。
Userを比較することに失敗しました。
オブジェクトそのままの比較はできないですね。

ということでオブジェクトの属性で比較します。
そのためにはsortメソッドではなくsort_byを使います
年齢で比較する

ruby.rb
users = users.sort_by{|user|user.age}  #昇順
p users.map{|user|"#{user.name},#{user.age},#{user.height}"}
#出力["foo,15,190", "baz,28,168", "bar,80,150"]

users = users.sort_by{|user|user.age}.reverse  #降順
#出力["bar,80,150", "baz,28,168", "foo,15,190"]

こんな感じで自作クラスの属性ごとにソートすることができました。
降順にしたいときはreverseメソッドを使えば簡潔になります。
またsort_byメソッドももっと簡単にかけるので、

ruby.rb
users = users.sort_by(&:height)

この一行でusersの配列に入ったuserを身長昇順に並び替えられます。
Rubyてすごいよね。

本日はここまでです。
みてくださった方ありがとうございました。

1人前のエンジニアになるまであと93日

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails初心者必見】Ruby on Railsでversion指定をしてWebアプリを作成する方法

開発環境

ruby 2.6.4

Rails 6.0.2.2

やりたいこと

Rails初心者にとって開発環境を整えるのは難しいですよね
環境構築で挫折してほしくないんです!

そこで今回は環境設定の一部ではありますが、Railsのバージョン指定して新規プロジェクトを作成していきます

versionを指定して新規プロジェクトを作成する

ステップ1. 特定のRailsのバージョンをインストール

$ gem install rails -v 6.0.2.2

ステップ2. インストールしたRailsを確認

$ gem list rails

ステップ3. インストールしたRailsのバージョンを指定して新規プロジェクトを作成

$ rails _6.0.2.2_ new twinz_blog
# twinz_blogの箇所にはアプリ名を入れてください

すごく簡単にできましたね!

参考文献

【Rails初心者必見】Railsのversionを指定して新規プロジェクトを作成する方法
https://www.twinzlabo.com/rails_version_project/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails初心者必見】Ruby on Railsでversion指定をしてWebアプリを新規作成する方法

開発環境

ruby 2.6.4

Rails 6.0.2.2

やりたいこと

Rails初心者にとって開発環境を整えるのは難しいですよね
環境構築で挫折してほしくないんです!

そこで今回は環境設定の一部ではありますが、Railsのバージョン指定して新規プロジェクトを作成していきます

versionを指定して新規プロジェクトを作成する

ステップ1. 特定のRailsのバージョンをインストール

$ gem install rails -v 6.0.2.2

ステップ2. インストールしたRailsを確認

$ gem list rails

ステップ3. インストールしたRailsのバージョンを指定して新規プロジェクトを作成

$ rails _6.0.2.2_ new twinz_blog
# twinz_blogの箇所にはアプリ名を入れてください

すごく簡単にできましたね!

参考文献

【Rails初心者必見】Railsのversionを指定して新規プロジェクトを作成する方法
https://www.twinzlabo.com/rails_version_project/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】deviseでユーザー詳細ページを作成する【お手軽実装】

はじめに

Ruby on Railsでユーザー詳細ページを作成する方法を記事にしました。

ソースコードはこちらのGitHubに載せておきます。

前提

  • ユーザーログイン機能にはgemの「devise」を使用
  • Userモデルの作成は本記事の対象外です。ユーザーモデルがある前提で話を進めます。もし作成していなければこちらの記事でハングオンしていただけると幸いです。
  • ユーザーモデル追加カラムは「username」「profile」「image」の3つ

開発環境

内容 バージョン
Ruby 2.5.1
Ruby on Rails 5.2.4.2
bundler 2.1.4
MySQL 5.6.47

手順❶:コントローラーの作成

ユーザー用のコントローラーを作成していきます。

$ rails g controller users

手順❷:ルーティングに記述を追加

ルーティングにユーザーの記述をしていきます。

routes.rb
resources :users,only: [:show]

今回使うアクションはshowアクションだけなので、ひとまず上記の記述でいきます。

手順❸:コントローラーに記述を追加

users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

end

MySQLからidごとにユーザーの情報をもってくるような形で記述を追加。

手順❹:ビューの作成 & 記述

まずはビューファイルを作成します。以下コマンドをターミナルに打ち込んでください。

$ touch app/views/users/show.html.erb

ではビューに記述を加えていきます。今回は一丁前にBootstrapを使用していきます。

users/show.html.erb
<div class="card text-center" style="width: 18rem; margin:20px auto;">
<%= image_tag @user.image.url ,class:"card-img-top",size:"300x300" ,style:"border-radius:50%;"%>
  <div class="card-body">
    <h5 class="card-title"><%= @user.username %></h5>
    <% if @user.profile.present? %>
    <p class="card-text"><%= @user.profile %></p>
    <% else %>
    <p class="card-text">まだプロフィールは作成していません</p>
    <% end %>
  </div>
</div>

ユーザー情報が少なく、サイドバーも編集ボタン等もないのでかなり質素ですが、基本形はできあがりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fullcalendarのイベントを特定の条件のイベントだけ表示させる方法

Qiita初投稿です。よろしくお願いします。
fullcelndarのイベントをユーザー詳細画面でそのユーザーのみのイベントで表示させる時に、苦戦したので備忘録として投稿させていただきます。

準備

本記事は特定のイベントの表示を目的にしているため、準備は自分が参考にした記事を貼らせていただきますので、参考にしていただけたらと思います。この記事の通りに実装してもらったらカレンダーは表示できると思います。
参考記事

目的

では、本来の目的であるユーザーのみのイベントを表示させていきます。

実装

ログイン機能の実装

Gemfile
gem 'devise'
ターミナル
$ bundle install
$ rails g devise:install
$ rails g devise User
$ rails db:migrate
$ rails g devise:views

これでユーザーの新規登録ができるようになりました。

コントローラ

userのコントローラを作ります。

ターミナル
$ rails g controller users show

コントローラにイベントを取り出す記述をします。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    @events = @user.events
  end
end

ユーザー詳細画面にカレンダーの記述をします。

app/views/users/show.html.erb
<div id="calendar"></div>

これで表示されるはず、、ですが恐らくカレンダーだけ表示されてイベントがうまく表示されていないと思います。
原因はjson.jbuilderファイルにあります。

json.jbuilderはどこを経由してデータを取得しているか

これを理解する必要があります。index画面でカレンダーを表示できたときのターミナルを見てみると

ターミナル
Processing by EventsController#index as JSON

とあります。
こちらから指示していないのにEventsControllerのindexアクションを探しに行っているのはなぜでしょう?
実は参考記事の中で指定していました。

application.js
$(function(){
 ・・・
  $('#calendar').fullCalendar({
  events: '/events.json'
});

events: '/events.json'という記述で上記のターミナルのような結果になっていました。
今はUsersControllerのshowアクションを探して欲しいので、以下の手順が必要になります。

1. index.json.jbuilderをshow.json.jbuilderに変更し、usersディレクトリ内に移す。

そのまんまです。中身を少し弄ります。

2. application.jsの記述をusers/show.html.erbに移す。

app/views/users/show.html.erb
<div id="calendar"></div>
<script>
$(function () {

          function eventCalendar() {
              return $('#calendar').fullCalendar({});
          };
          function clearCalendar() {
              $('#calendar').html('');
          };
          $(document).on('turbolinks:load', function () {
          eventCalendar();
          });
          $(document).on('turbolinks:before-cache', clearCalendar);

          $('#calendar').fullCalendar({
              events: '/users/<%= @user.id %>',
              //カレンダー上部を年月で表示させる
              titleFormat: 'YYYY年 M月',
              //曜日を日本語表示
              dayNamesShort: ['', '', '', '', '', '', ''],
              buttonText: {
                  prev: '前月',
                  next: '次月',
                  prevYear: '前年',
                  nextYear: '翌年',
                  today: '今日',
                  month: '',
                  week: '',
                  day: ''
              },
              timeFormat: "HH:mm",
                //イベントの色を変える
              eventColor:'#63ceef',
              //イベントの文字色を変える
              eventTextColor: '#000000',
          });
});
</script>

ユーザー詳細画面で表示させたいため、events: '/users/<%= @user.id %>'と記述しました。これでCustomersControllerのshowアクションを呼ぶことができます。カレンダーの表示はできましたか?

まとめ

gemの動きだけではうまく表示できないので、jsonの知識が少し必要でした。
ご不明な点があれば、コメントしていただけたらと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データの並び替え降順、昇順/Railsでまとめてみた

投稿したデータや記事の並び替えを変える際に、descが降順?降順?で毎回ググって調べていたのでまとめました。

board.rb
class Board < ApplicationRecord
  scope :desc, -> { order(created_at: :desc) } #作成の降順
  scope :asc, -> { order(id: :asc) } #idの昇順 デフォルトでasc(昇順)で表記されるので明示的に書くことは少ない
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人アプリRails6.0をHerokuにデプロイした時の失敗話

背景

 個人でRailsアプリをHerokuへデプロイしようした際に、つまづいたポイントが2点あったので記事にします。

  1. DBをMySQLで作成してしまった。
  2. master.keyの設定をミスっていた。

1. DBをMySQLで作成してしまった。

 結論:HerokuへデプロイするならPostgreSQLを使うべきです。

なぜMySQLだといけないのか

 Herokuの無料枠でMySQLを使う場合、ストレージが5 MBの制限があります。一方PostgreSQLの場合、ストレージ制限ではなく、10,000行使用可能になります。個人アプリで5 MB、使用すことはあっても10,000レコード使うことは少ないと思うのでPostgreSQLを使用することをお勧めします。

MySQL ⇒ PostgreSQL へ変更

Rails6.0からデータベースの変更は簡単に行えるようになりました。ターミナルで以下のコマンドを実行すると

ターミナル
rails db:system:change --to=postgresql

PostgreSQLのデータをGUIで操作する。

 MySQLではSequel Proを使ってデータをGUI操作していました。
 PostgreSQLではPosticoを使ってデータをGUI操作します。
 参考:PostgreSQLのデータをGUIでいじる
 本番環境のPostgreSQLへのアクセスは次の記事がとても参考になりました。
How to Set Up Postico 3 to Connect to a Remote PostgreSQL Server (Heroku)

2. master.keyの設定をミスっていた。

Herokuにデプロイする

 公式サイトをGetting Started on Heroku with Rails 6.x を参考にしながら行いました。
 Herokuアカウントを所持していたのと、rails db:system:changeコマンドを実行済みであったので、Deploy your application to Herokuから手順を進めました。

エラーの発生

 git push heroku masterでprecompile assets faildのエラー発生。ログを読んで見ると、ActiveSupport::MessageEncryptor::InvalidMessageの文字。
早速のググると、直ぐに原因は分かりました。
 Herokuの環境変数にmaster.keyをセットしていなかった。次のコマンドを打って再トライ。

ターミナル
heroku config:set RAILS_MASTER_KEY= <ご自身のmaster.key内の値>

 私の場合は、ここでエラーが解決せず数日間悩みました。
 原因は、ローカルの~/.bash_profileに別のRailsアプリケーションのRAILS_MASTER_KEYを設定していた為でした。
 本アプリケーションのmaster.keyよりも優先してENV[RAILS_MASTER_KEY]が、credentials.yml.enc のペアになっていました。

対処

ターミナル
vim ~/.bash_profile
.bash_profile
# 以下の記述を削除
export RAILS_MASTER_KEY="################################"

config/credentials.yml.encとconfig/master.keyを削除

ターミナル
EDITOR=vim rails credentials:edit

を実行して、config/credentials.yml.encとconfig/master.keyを再生成。
新しいmaster.keyの値をHerokuの環境変数に設定する。

ターミナル
heroku config:set RAILS_MASTER_KEY= <ご自身のmaster.key内の値>

 再びコマンドを打ち込むと、無事デプロイに成功しました。

ターミナル
git push heroku master

余談

.bash_profileから消した環境変数が反映されないと思ったらbashではなくzshを使っていた件

ここではターミナルをbashを使って説明しましたが、実際にはzshに変更していた為にさらに時間がかかりました。

症状

ターミナル
% printenv
 :
RAILS_MASTER_KEY="################################"
 : 
% vim ~/.bash_profile
.bash_profile
# 以下の記述を削除
export RAILS_MASTER_KEY="################################"
ターミナル
% source ~/.bash_profile
% printenv
 :
#RAILS_MASTER_KEYが消えています。
 : 

一旦は環境変数が消えますが、ターミナルを再起動すると、なぜか消したはずの環境変数が復活していました。what's!?

ターミナル
% printenv
 :
RAILS_MASTER_KEY="################################"
 : 

原因

ターミナルをbashではなくzshを使っていた。

環境変数はターミナルごとで管理しているので、bashとzshで記述するファイルが異なります。

  • bashでは、~/.bash_profile
  • zshでは、~/.zprofile

対処

 上記の操作を~/.zprofileに変更して行うと、うまくいきました。

ターミナル
% printenv
 :
RAILS_MASTER_KEY="################################"
 : 
% vim ~/.zprofile
.zprofile
# 以下の記述を削除
export RAILS_MASTER_KEY="################################"
ターミナル
% source ~/.zprofile

 初歩的なミスで数日時間を取られてしました。
 エラー解決のきっかけは、たまたま同じ事をしている記事が見つけたからです。
 小さなエラー解決の記事でも、誰かの助けになる事が身に沁みました。だから私もこの記事を書くことに決めました。

参考

.bash_profileに書いた環境変数が反映されないと思ったらbashではなくzshを使っていた件

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【神業】面倒なHTMLタグ入力を光の速さで終わらせる【定型文ツール】

はじめに

フリーランスRailsエンジニアおよびWEBライターをやっているソエノと申します。この記事では「面倒な定型文入力を神速で終わらせる方法」についてご紹介します。

注意事項 : コメント欄への書き込みについて

コメント欄への「誹謗中傷・暴言」の書き込みはインターネット上であれ「威力業務妨害」が成立する危険性があります。全てのコメントは削除後も筆者のメールアドレスに保存されていますので、何卒ご注意の程よろしくお願いいたします。

結論

「Dash」というツールを使います。以下のようなことができます。以下は、HTMLのaタグを書くときの例です。

a241ccbb5154265091554405f3d18873.gif

う〜ん、神。普通、aタグを書こうとしたら<a href="hoge">fuga</a>のhogeとfuga以外の部分って手入力しますよね。上記の動画では;atagという呪文によって謎のウィンドウを召喚し、hogeとfugaだけを入力してEnterすることによって何とaタグを完成させてしまいました。変換キーとかも使ってないので、もはや辞書登録使うより速いです。

このツールを使うなら、ブロガーやフロントエンジニアの方はHTMLタグを書くために<a href="hoge">fuga</a>みたいなテキストを手入力するのは金輪際やめましょう。Railsエンジニアなら「routes.rbのresourcesってどういう風に書くんだっけ...」とか「バリデーションってどう書くんだっけな?ググるか...」みたいなことが今後一切なくなります。

ダウンロード方法

こちらからダウンロードできます。リンク先にアクセスしたら、画面右にあるDownloadをクリックです。zipファイルがダウンロードされるので、解凍してアプリケーションフォルダにアプリを移動させましょう。

使い方

使う前に、アプリの動作許可設定しないと動かないのでMacのシステム環境設定を開きましょう。パソコン左上に表示されているりんごマークをクリックしたら「システム環境設定」というのがありますから選びます。スクリーンショット 2020-05-28 14.57.32.png

スクリーンショット 2020-05-28 14.58.04.png

家のマークで「セキュリティとプライバシー」というのがありますので、これを選びます。下記画面で左側のウィンドウを少し下にスクロールすると、「アクセシビリティ」という項目があるので、選びます。そして左下の錠マークをクリックしてパソコンのロックを解除します。あとは「Dash」というところのチェックをONにしましょう。これで準備OKです。

スクリーンショット 2020-05-28 14.59.20.png

それではDashを起動しましょう。

スクリーンショット 2020-05-28 15.03.11.png

起動後の画面はこんな感じです。まずは右上の「Manage Docsets」をクリックしましょう。

スクリーンショット 2020-05-28 15.03.52.png

上記のような画面になります。続けて、上のメモマーク「Snippets」を選択しましょう。

スクリーンショット 2020-05-28 15.07.02.png

Snippetsを選択したら上記のような画面になるので、Enable snippetsというチェックをONにしましょう。ONにしたらこの設定画面は不要なので左上の×マークで閉じます。

スクリーンショット 2020-05-28 15.14.44.png

設定後、ホーム画面のSearchという欄でSnippetsというのが選択できるようになりますので、クリックします。

スクリーンショット 2020-05-28 15.15.49.png

すると上記のようになるので、New Snippetsをクリック。

スクリーンショット 2020-05-28 15.16.22.png

なんか入力欄が二つ出てきます。上の細い入力欄が、先ほど紹介した「呼び出しの呪文」です。下の大きなの欄が「実際に呼び出されるテキスト」になります。試しに、次のように設定してみましょう。上の入力欄には;atag、下の入力欄には<a href="__link__">__text__</a>と書きました。

スクリーンショット 2020-05-28 15.18.04.png

設定はこれで完了です。特に保存とかは無いみたいなので、このままDashアプリからは離れてしまって構いません。あとは、適当なテキスト入力欄に;atagと打ってみてください。なんか召喚されると思います。

新しいスニペットを登録する

この「スニペット」というのが先ほどのように「召喚の呪文と、そこから召喚されるテキストの組み合わせ」という意味のようです。新しいスニペットを登録するには、下記動画のように画面上のプラスマークをクリックすればいいようです。

20c74c6c0dffda9faa872fab231e6f92.gif

さらに高速化していく方法【カスタマイズ機能】

上記では__link____text__といった記述によってテキストを入力できるようにカスタマイズしました。これは「アンダースコア2つで囲むと任意テキスト入力できるようにする」というDashのカスタマイズ機能です。このほかにも便利な機能がいくつもありますので、使えそうなものをピックアップしてご紹介します。

クリップボードの情報を自動で挿入する

ここでいうクリップボードというのは「コピーしたテキスト」のことです。例えば、さっきのaタグ生成のやつを以下のように書くと、直前にURLをコピーしていれば、コピーしたテキストを勝手にペーストしてくれます。

スクリーンショット 2020-05-28 15.32.53.png

93c5b8717f74c67fc7893ef62a7a3925.gif

@clipboardという記述がそのままクリップボードの情報に置き換わるようです。

挿入後のカーソルの位置を指定する

さっきはDashで呼び出されたウィンドウ上で文字入力していましたが、先にaタグを生成してからエディタ上でテキストを入れたい時はこれが便利です。@cursorと書くと、スニペット挿入後に勝手にカーソルが@cursorと書いた位置に飛んでくれます。

スクリーンショット 2020-05-28 15.43.18.png

de3a1be8afb5d53bfd35edfe550621d5.gif

便利なプリセット

この記事をここまで呼んでくださった方のために、プリセットをご用意してみました。よろしければ、ご活用ください。

HTML

aタグ
;atag
<a href="@clipboard">@cursor</a>
divタグ
;divtag
<div class="__class__">@cursor</div>
hタグ(h1,h2,h3,...)
;htag
<h__number__>@cursor</h__number__>
pタグ
;ptag
<p>@cursor</p>
scriptタグ(JavaScript埋め込み)
<script type="text/javascript">
  @cursor
</script>
任意のHTMLタグ
;html
<__html__>@cursor</__html__>

Ruby on Rails

routes.rbのresources
;resources
resources :__controllerName__, only: [:__action1__, :__action2__]
form_with(haml記法)
;form_with
= form_with model: __modelName__, url: __actionUrl__, local: __trueOrFalse__ do |f|
  @cursor
バリデーション(存在)
;validate
validates :__columnName__, presence: true

JavaScript

console.log
;console
console.log("@cursor");
jQueryを使ったDOM取得
;jq
$("@cursor")

記事を読んでいただきありがとうございました

ここまで読んでくださって本当にありがとうござます。私から皆さんにGiveさせて頂けるのはこうしたエンジニア向け情報くらいしかありませんが、皆さんに記事を読んでいただけて、そして皆さんのお役に立つことができれば、それは私の何よりの喜びになります。今後とも皆さんのお役に立てるよう記事執筆活動などを進めていきたいと思っていますので、これからも何卒よろしくお願いいたします。

まとめ

・Dashというアプリを使うことで、HTMLタグの入力を楽にしたり、Railsなどのプログラミング言語における「定型句」を覚える必要がなくなりました。代わりに自分で登録した「召喚の呪文」を覚える必要がありますが、これまでの作業よりも幾分マシになっているはずです。

以上となります。それでは、生産的なコーディングライフを!!

筆者Twitter: soeno_onseo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker上にRails環境構築(DBはmariadb)

Docker上にRails環境(DBはmariadb)を構築したい!

ローカル環境上にRailsの環境構築を行うと、どんどんローカルが汚れていくので嫌だなと。
なのでローカル上でDocker for Windowsを使用してRails環境を構築したく記事を書きました。
※色々弄りすぎた結果、この投稿に記載しているDockerファイルやdocker-composeファイルも汚れた状態での投稿になっています。。。綺麗にしたい。。。

構築にあたっての前提環境

構築手順

  1. プロジェクトを作成したい場所にフォルダを作成
  2. 上記で作成したフォルダ内にDockerファイルを作成

    FROM ruby:2.7
    
    ENV LANG C.UTF-8
    ENV WORKSPACE=/usr/local/src
    
    # install bundler.
    RUN apt-get update && \
        apt-get install -y vim less && \
        apt-get install -y build-essential libpq-dev nodejs && \
        apt install -y lsb-release && \
        apt remove -y libmariadb-dev-compat libmariadb-dev && \
        apt-get install -y nginx
    
    RUN wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-common_8.0.18-1debian10_amd64.deb \
        https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient21_8.0.18-1debian10_amd64.deb \
        https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client-core_8.0.18-1debian10_amd64.deb \
        https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client_8.0.18-1debian10_amd64.deb \
        https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient-dev_8.0.18-1debian10_amd64.deb
    
    RUN dpkg -i mysql-common_8.0.18-1debian10_amd64.deb \
        libmysqlclient21_8.0.18-1debian10_amd64.deb \
        mysql-community-client-core_8.0.18-1debian10_amd64.deb \
        mysql-community-client_8.0.18-1debian10_amd64.deb \
        libmysqlclient-dev_8.0.18-1debian10_amd64.deb
    
    RUN gem install bundler && \
        apt-get clean && \
        rm -r /var/lib/apt/lists/*
    
    # create user and group.
    RUN groupadd -r --gid 1000 rails && \
        useradd -m -r --uid 1000 --gid 1000 rails
    
    # create directory.
    RUN mkdir -p $WORKSPACE $BUNDLE_APP_CONFIG && \
        chown -R rails:rails $WORKSPACE && \
        chown -R rails:rails $BUNDLE_APP_CONFIG
    
    USER rails
    WORKDIR $WORKSPACE
    
    # install ruby on rails.
    ADD --chown=rails:rails . $WORKSPACE
    RUN bundle install
    
    ADD . /app
    RUN mkdir -p tmp/sockets
    
    # Expose volumes to frontend
    VOLUME /app/public
    VOLUME /app/tmp
    
    # Start Server
    # TODO: environment
    CMD bundle exec puma
    
  3. 同一フォルダにdocker-compose.ymlを作成

    docker-compose.yml
    version: '3.2'
    services:
      api:
        build: .
        command: bundle exec rails s -p 8080 -b '0.0.0.0'
        container_name: '好きなコンテナー名'
        ports:
          - 8080:8080
        volumes:
          - type: bind
            source: ./
            target: /usr/local/src
        restart: always
        environment:
            TZ: Asia/Tokyo
        links:
            - maria-db
    
      maria-db:
        image: mariadb:10.5.2
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin
        environment:
            MYSQL_ROOT_PASSWORD: hoge
            MYSQL_DATABASE: hoge
            MYSQL_USER: hoge
            MYSQL_PASSWORD: hoge
        ports:
            - '3306:3306'
        command: --innodb-use-native-aio=0
        volumes:
            - ./docker_config/db/volumes:/var/lib/mariadb
            - ./docker_config/db/my.cnf:/etc/mysql/conf.d/my.cnf
    
  4. プロジェクトの作成
    コマンドプロンプトを立ち上げプロジェクトを作成するフォルダまで移動後、下記コマンドを実行する

    $ docker-compose run --rm api rails new . --force --database=mysql --skip-bundle
    $ docker-compose run --rm api bundle exec spring binstub --all
    
  5. databaseの接続情報を設定
    プロジェクトフォルダ/config/database.ymlにDBの接続情報を記載

    default: &default
    adapter: mysql2
    encoding: utf8
    pool: 5
    username: hoge
    password: hoge
    host: maria-db
    
  6. 起動する

$ docker-compose up
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マイグレーションファイルがダブっちまった

近況報告

 絶賛就活中でございます。最近やっているのは習慣化ですね。最近,英語と筋トレを習慣化させることができました。いきなり英語は飽きると思ったので,Gacktの母音発音のyoutubeをみて簡単な文を音読しています。考えが改まったことがひとつ,Google翻訳ってあるじゃないですか。あれ,全然翻訳してくれないからゴミだと昔は思っていたんですけど,実際は自身の発音がゴミだったってことがわかりましたw 筋トレは腹筋スクワットメインの運動を自重かつ強度をあげて20分だけ行なっています。腹筋が生まれて初めて横以外に割れそうな勢いです。そしてここ数日で始めたのは朝型転換です。ここ二ヶ月日付が変わったらアマゾンなプライムでビデオ見まくっていたので,生活習慣の改善として行なっています。

今回のお題

 マイグレーションファイルがダブってる
 始めたての初心者がいつの間にかハマるやつですね!・・・はい,やらかしました。
 ⇨解決策は片方を消す。以上。

マイグレーションファイル

●マイグレーションファイルは噛み砕くとテーブルの設計図です。
 rails db:migrateの際にテーブルを作成更新するときに利用します。一度利用されたマイグレーションファイルはテーブルを再度更新,生成しない限り用いることはありません。実際削除しても問題ありません。

●本当に削除していいの?
 そこに書かれている情報を二度と使わないのであれば消してもいいよ。
 開発においてテーブルの変更はなんども行われます。そのときにマイグレーションファイルがないと以前はあったとしてもパソコンがテーブルを読み取れなくてエラーを起こします。また,開発環境で用いたテーブルは本番環境でも使えるわけではなく,本番環境のDBを設置して再度テーブルを組み立てることが多いので,本番の段階でマイグレーションファイルがないのは一大事なわけです。長くなりましたが結論はもう使わなくてもほっとけです。

●ダブった場合
 マイグレーションファイルがダブっている場合も消したらテーブルが作成できなくなると心配になります(自分はなった)。ただ、この場合は同じ設計書が二枚あるだけなので,一枚になっても変わりませんし,マイグレーションファイルの数だけテーブルができるわけではないので安心して片方消してください。

最後に

内定が欲しい! エンジニアライフを早く堪能したい!

 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails チュートリアルをやってみる ch1

やったこと

  • ローカル開発環境の準備
  • CI/CDの準備
  • 第一章のHello World
  • 公式Docの確認

なぜやったのか

  • 実際の開発フローに近い段取りをするため

次にやること

  • Ch2
  • GitLab CI/CD を設定した方法についてQiitaの記事にまとめる
  • RuboCop, RSpec, Cypress について調べて導入する
  • 『初めての自動テスト』を読み始める

ようやく開発のスタートラインについた感じ。
余力がないので、後になるけど調べたこととか、実際にやってみた内容をまとめて記事にしてアウトプットにすると勉強になるし、誰かの役に立つかもしれないから頑張ってやる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Rails 起動しなくなった

前提

DockerでRails環境を構築し、突然コンテナが起動しないくなった原因と解決方法について記述します。

本題

dockerのアップデート終了後にdocker-compose up -dで立ち上げても一瞬で終了してしまうことがありました。

原因

すでにサーバーが起動していると認識していたからでした。

まずは、ログを確認します。

$ docker-compose up

確認すると、、、

#省略
 ・
 ・
 ・
db_1               | 2020-05-28T04:13:02.167787Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
db_1               | 2020-05-28T04:13:02.207302Z 0 [Note] InnoDB: Buffer pool(s) load completed at 200528  4:13:02
db_1               | 2020-05-28T04:13:02.234813Z 0 [Note] Event Scheduler: Loaded 0 events
db_1               | 2020-05-28T04:13:02.235265Z 0 [Note] mysqld: ready for connections.
db_1               | Version: '5.7.30'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
web_1              | => Booting Puma
web_1              | => Rails 5.2.3 application starting in development 
web_1              | => Run `rails server -h` for more startup options
web_1              | A server is already running. Check /app/tmp/pids/server.pid.
web_1              | Exiting

上記のように、A server is already runningとなっており、サーバーがすでに立ち上がっているから終了してしまっていることがわかります。
tmp/pids/server.pidというファイルが原因となってます。
確認してみると、、、

$ cat tmp/pids/server.pid
1

この1という数字はRails ServerのPID(Process ID)となります。
Linuxでは各プロセスにIDをつけて管理しています。
またDockerでは, プロセスIDが1であるプロセスが終了した場合、コンテナも終了するようになっているとのことです。

解決方法

$ rm tmp/pids/server.pid

上記でtmp/pids/server.pidを削除後、再度立ち上げると無事に立ち上がりました!

$ docker-compose up -d

毎回 rm tmp/pids/server.pid するのも手間であるため、docker-compose.ymlを修正しました。

docker-compose.yml
#変更前
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
    environment:
    - "SELENIUM_DRIVER_URL=http://selenium_chrome:4444/wd/hub"
docker-compose.yml
#変更後
version: '3'
services:
  web:
    build: .
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
    environment:
    - "SELENIUM_DRIVER_URL=http://selenium_chrome:4444/wd/hub"

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6.0で"Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1."という警告が出たときの対処法

はじめに

MySQLを使っている既存のRailsアプリケーションをRails 6.0にアップデートすると、次のような警告が出ることがあります。

DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :name attribute in User model, pass `case_sensitive: true` option explicitly to the uniqueness validator.

(翻訳)
非推奨の警告: UniquenessバリデータはRails 6.1で「強制的に大文字小文字を区別する比較」をしなくなります。Userモデルの:name属性について引き続き「大文字小文字を区別する比較」を使い続けたい場合は、uniquenessバリデータに対して明示的にcase_sensitive: trueオプションを指定してください。

警告が出るのは次のようにuniquenessバリデータを使っている部分です。

class User < ApplicationRecord
  validates :name, uniqueness: true
end

とりあえず、こんなふうにcase_sensitiveオプションを付けると警告は出なくなります。

class User < ApplicationRecord
  # こうすれば警告は出なくなる、が!!!
  validates :name, uniqueness: { case_sensitive: true }
end

しかし、深く考えずにオプションを付けるのはあまりよくありません。
というわけで、この記事ではこの警告に対する対処方法を詳しく説明していきます。

Rails 5.2以前の仕様(と問題)

前提としてこの問題はMySQLを使っている場合に発生します。PostgreSQLを使っている場合は通常問題になりません。

詳しい話は省略しますが、MySQLにはcollationという概念があります。
デフォルトではutf8mb4_unicode_ciというようなcollationになっており、この場合はデータベースに保存された文字列の大文字小文字を区別しません。

つまり、"jnchito"という名前を検索するのに、WHERE name = 'jnchito'というSQLを発行しても、WHERE name = 'JNCHITO'というSQLを発行してもどちらもヒットします。

しかし、Rails 5.2以前のuniquenessバリデータはデフォルトで親切にも大文字小文字を区別する比較をしてくれます。

なので、DBに"jnchito"がすでに保存されている場合は、次のように振る舞います。

# 小文字のjnchitoはすでに登録済みなのでNG
user.name = 'jnchito'
user.valid? #=> false

# 大文字のjnchitoはすでに未登録なのでOK
user.name = 'JNCHITO'
user.valid? #=> true

# 背後では以下のようなSQLが発行されている(BINARYが付く)
# SELECT 1 AS one FROM `users` WHERE `users`.`name` = BINARY 'JNCHITO' LIMIT 1

一見これはありがたい仕様のように見えますが、次のような思わぬデメリットがあります。

  • DB上のユニーク制約に一致しない
  • DB上のINDEXが効率良く使えないため、DBの負荷が大きくなる

実際、先ほど挙げたコードは以下のような矛盾した振る舞いをします。
(DB側にユニーク制約が付けられていた場合)

# 大文字の"JNCHITO"なら検証エラーなしだから保存できそうだ
user.name = 'JNCHITO'
user.valid? #=> true

# 保存実行・・・あれっ、DBのユニーク制約違反に引っかかって例外が発生しちゃった!!
user.save
#=> ActiveRecord::RecordNotUnique:
#     Mysql2::Error: Duplicate entry 'JNCHITO' for key 'users.index_users_on_name'

RailsでMySQLを使っているとこのような問題がたびたび発生していたようです。
(僕は普段PostgreSQLを使っているので気づいていませんでしたが)

(Rails 6.0ではなく)Rails 6.1で導入される仕様

この問題を回避するため、Rails 6.1のuniquenessバリデータはデフォルトで大文字小文字を区別しなくなります。
というか、厳密には「Rails側では素直にSQLを発行して、大文字小文字の区別はDB側の設定に任せる」という仕様になります。

これにより、DB側の機能をフル活用できるようになるため、上で挙げていた、

  • DB上のユニーク制約に一致しない
  • DB上のINDEXが効率良く使えないため、DBの負荷が大きくなる

といった問題が発生しなくなります。

たとえば、DBに"jnchito"がすでに保存されている場合、Rails 6.1ではおそらく次のような振る舞いになるはずです。

# jnchitoはすでに登録済みなのでNG(大文字小文字を区別しない)
user.name = 'jnchito'
user.valid? #=> false

# JNCHITOはすでに登録済みなのでNG(大文字小文字を区別しない)
user.name = 'JNCHITO'
user.valid? #=> false

# 背後では以下のようなSQLが発行されるはず(BINARYが付かない)
# SELECT 1 AS one FROM `users` WHERE `users`.`name` = 'JNCHITO' LIMIT 1

Rails 6.0は6.1の仕様変更に向けて、開発者にコードやDB設定の見直しを促す

しかし、Rails 6.1の仕様変更は「思わぬデメリット」を避けられるのと引き換えに、「大文字小文字の区別をしなくなる」という振る舞いの変化を招いてしまいます。

そこで、Rails 6.0ではRails 5.2以前の振る舞いを保ちつつ、「Rails 6.1は振る舞いが変わるよ!今のうちにどうしたいか決めて!」と、開発者に変更を促します。それが冒頭に紹介した警告です。

大文字小文字を区別する場合

Rails 5.2時代と同様に大文字小文字を区別したい場合は、明示的にcase_sensitive: trueのオプションを付ければ警告は消えます。

ただし、DB側のcollationに変更がなければ、

  • DB上のユニーク制約に一致しない
  • DB上のINDEXが効率良く使えないため、DBの負荷が大きくなる

という問題を抱えたままになってしまいます。

class User < ApplicationRecord
  # 警告は出なくなるが、DB側のcollationを変えなければ「思わぬデメリット」は残ったまま
  validates :name, uniqueness: { case_sensitive: true }
end

こうした問題を解消したい場合は、Rails側のコードを修正するのではなく、DB側のcollationをutf8mb4_binのような「大文字小文字を区別するcollation」に変更する必要があります。(collationを変更する手順はここでは割愛します)

DB側のcollationが大文字小文字を区別するようになっていれば、Railsのuniquenessバリデータの振る舞いとミスマッチがなくなるので警告は出なくなります。(case_sensitiveオプションを指定する必要はありません)

class User < ApplicationRecord
  # DB側のcollationを変えればcase_sensitiveオプションは不要。警告も「思わぬデメリット」も発生しない
  validates :name, uniqueness: true
end

大文字小文字を区別しない場合

大文字小文字を区別しなくていい場合は明示的にcase_sensitive: falseを指定します。
こうすればDB側のcollationもRailsのuniquenessバリデータも大文字小文字を区別しなくなるので、ミスマッチが解消され、警告も表示されなくなります。

ただし、この場合はアプリケーションの挙動が変わってしまうので、ユーザーに混乱を招いたりしないか、よく検討する必要があります。

class User < ApplicationRecord
  # 警告は出なくなる。「思わぬデメリット」もなくなる。が、Rails 5.2と挙動が変わる
  validates :name, uniqueness: { case_sensitive: false }
end

また、Rails 6.1にアプリケーションをアップグレードしたあとはcase_sensitive: falseのオプションを外しても問題ありません。(デフォルトで大文字小文字を区別しなくなるため)

class User < ApplicationRecord
  # Rails 6.1ではcase_sensitiveをなくしてしまってもOK
  validates :name, uniqueness: true
end

参考:Rails 5.2〜6.1の振る舞いまとめ

この話は「MySQL側のcollation」と「uniquenessバリデータのcase_sensitiveオプション」と「Railsのバージョン」の組み合わせによって話がいろいろと変わってきます。

それぞれの組み合わせで何が起きるか、以下の表にまとめておきます。

Screen Shot 2020-05-28 at 11.14.41.png

最終的には上の表の「DB側とRailsのミスマッチ?」欄が"NO"になる組み合わせが実現できれば理想的な状態、となります。

参考文献

謝辞

この件についてはRailsコミッタのkamipoさんにTwitter上で質問して丁寧に回答していただきました(参考)。kamipoさん、どうもありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Composeを使った初期装備なRails6/PostgreSQL環境の作り方

Docker Composeを使ったRails環境を作る場合、Quickstart: Compose and Rails | Docker Documentationがとても参考になります。ただし、このドキュメントはRails5を前提として書かれており、現時点(2020-05-28)で最新のRails6を使う場合には一部修正が必要です。

そこでこの記事では、Rails6を使う場合について記載します。また、この記事の対象読者は下記を想定しており、環境構築は下記の方針と前提に基づいて行ないます。

:family_mmgb: 想定読者

  • Rails環境構築でいつも躓くの方(丁寧めに説明します)
  • 未来の自分(思い出せるように書きます)

:telescope: 方針

:pushpin: 前提

  • dockerをインストール済みである
  • gitをインストール済みである

なお、今回の作業内容はこちらのプルリクエスト https://github.com/tanaken0515/docker-compose-rails-sample/pull/1 にまとまっています。

では、やっていきましょう。

リポジトリを作る

まずはリポジトリを作りましょう。

$ mkdir docker-compose-rails-sample
$ cd docker-compose-rails-sample
$ git init
$ touch README.md
$ git add .
$ git commit -m "initial commit"

ここではdocker-compose-rails-sampleという名前でリポジトリを作りました。
空のREADME.mdを作ってコミットしています。

プロジェクトを定義する

ここから環境構築です。
まずは https://docs.docker.com/compose/rails/#define-the-project をまるっと書き写しながらファイルを作っていきます。
このドキュメントは解説も交えて書かれているので、詳しくはドキュメントを参照してください。

(英語のドキュメントが苦手な方は @daichi41 さんが書かれた DockerでのRuby on Rails環境構築を一つずつ詳解する - Qiita と照らし合わせながら読むと理解が進むかもしれません)

一つ目はDockerfileです。FROM ruby:2.5となってますがこれは後で書き換えるのでいったんそのままで。

Dockerfile
FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

次にGemfileです。gem 'rails', '~>5'となってますがこれもあとで書き換えます。このままで。

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5'

Gemfile.lockは空のファイルを作っておきます。これはあとでrails newするときに上書きされます。

$ touch Gemfile.lock

続いてentrypoint.shです。これもそのまま。

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

説明の都合上、ここまでいったんコミットしました。

さて、Ruby2.7とRails6を使うために少し手を加えます。

DockerfileのベースイメージをRuby2.7にします。また、Rails6のデフォルトではWebpackerを使うのでyarnpkgもインストールするようにしておきます。

Dockerfile
-FROM ruby:2.5
-RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
+FROM ruby:2.7
+RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarnpkg
+RUN ln -s /usr/bin/yarnpkg /usr/bin/yarn
 RUN mkdir /myapp
 WORKDIR /myapp
 COPY Gemfile /myapp/Gemfile

Gemfileのrailsを~>6に書き換えます。

Gemfile
 source 'https://rubygems.org'
-gem 'rails', '~>5'
+gem 'rails', '~>6'

プロジェクトの定義はこれで完了です。

プロジェクトをビルドする

https://docs.docker.com/compose/rails/#build-the-project の通りに進めます。

まずはrails newします。少々時間がかかりますので休憩しつつ待ちましょう。(僕の環境では15分くらい待ちました)

docker-compose run web rails new . --force --no-deps --database=postgresql

rails newしたことで、たくさんのファイルが生成されているはずです。
(この段階でコミットしておくと良いでしょう)

続いてビルドしましょう。これも少々時間がかかります。(僕の環境では10分くらいでした)

docker-compose build

プロジェクトのビルドはこれで完了です。

データベースに接続する

https://docs.docker.com/compose/rails/#connect-the-database に沿って進めます。

rails newしたときにconfig/database.ymlが生成されています。これを書き換えます。

config/database.yml
 default: &default
   adapter: postgresql
   encoding: unicode
-  # For details on connection pooling, see Rails configuration guide
-  # https://guides.rubyonrails.org/configuring.html#database-pooling
-  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+  host: db
+  username: postgres
+  password:
+  pool: 5

 development:
   <<: *default

アプリケーションを起動します。

$ docker-compose up

おっと、ここでエラーに遭遇しました。

db_1   | Error: Database is uninitialized and superuser password is not specified.
db_1   |        You must specify POSTGRES_PASSWORD to a non-empty value for the
db_1   |        superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run".
db_1   | 
db_1   |        You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all
db_1   |        connections without a password. This is *not* recommended.
db_1   | 
db_1   |        See PostgreSQL documentation about "trust":
db_1   |        https://www.postgresql.org/docs/current/auth-trust.html
docker-compose-rails-sample_db_1 exited with code 1

一旦別のターミナルで以下を実行して、アプリケーションを停止しましょう。

$ docker-compose down

さて、先ほどのエラーは割と最近の変更によってPostgreSQLにパスワードなしでアクセスできなくなったからのようです。
ref: https://github.com/docker-library/postgres/issues/681

ここまでの手順でインストールされたPostgreSQLのバージョンはdocker-compose.ymlで指定されたimage: postgresに依存していますが、そもそもここでバージョンを指定していないので、どのバージョンがインストールされるのかが自明ではないですね(最新の安定版がインストールされそう)

docker-compose.ymlでPostgreSQLのバージョンを指定し、環境変数にPOSTGRES_HOST_AUTH_METHOD=trustを設定しましょう。
バージョンは https://hub.docker.com/_/postgres をみて現時点(2020-05-28)で最新安定版の12.3を指定します。

docker-compose.yml
 version: '3'
 services:
   db:
-    image: postgres
+    image: postgres:12.3
     volumes:
       - ./tmp/db:/var/lib/postgresql/data
+    environment:
+      POSTGRES_HOST_AUTH_METHOD: trust
   web:
     build: .
     command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"

改めてアプリケーションを起動します。

$ docker-compose up

warningが出ますが、無事にデータベースと接続することができました。

db_1   | ********************************************************************************
db_1   | WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow
db_1   |          anyone with access to the Postgres port to access your database without
db_1   |          a password, even if POSTGRES_PASSWORD is set. See PostgreSQL
db_1   |          documentation about "trust":
db_1   |          https://www.postgresql.org/docs/current/auth-trust.html
db_1   |          In Docker's default configuration, this is effectively any other
db_1   |          container on the same system.
db_1   | 
db_1   |          It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace
db_1   |          it with "-e POSTGRES_PASSWORD=password" instead to set a password in
db_1   |          "docker run".
db_1   | ********************************************************************************
(中略)
db_1   | PostgreSQL init process complete; ready for start up.
db_1   | 
db_1   | 2020-05-28 01:21:45.583 UTC [1] LOG:  starting PostgreSQL 12.3 (Debian 12.3-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
db_1   | 2020-05-28 01:21:45.583 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db_1   | 2020-05-28 01:21:45.584 UTC [1] LOG:  listening on IPv6 address "::", port 5432
db_1   | 2020-05-28 01:21:45.588 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db_1   | 2020-05-28 01:21:45.694 UTC [58] LOG:  database system was shut down at 2020-05-28 01:21:45 UTC
db_1   | 2020-05-28 01:21:45.735 UTC [1] LOG:  database system is ready to accept connections

データベースへの接続はこれで完了です。

アプリケーションの動作確認

ここまでの状態で localhost:3000 にアクセスするとActiveRecord::NoDatabaseErrorになります。データベースの環境に接続できる状態ではあるものの、このアプリケーション用のデータベースを作っていないからですね。

ドキュメントに従って以下を実行しましょう(アプリケーションを起動したターミナルとは別のターミナルで実行して下さい)。
データベースが作成されます。

$ docker-compose run web rake db:create
Starting docker-compose-rails-sample_db_1 ... done
Created database 'myapp_development'
Created database 'myapp_test'

改めて localhost:3000 にアクセスするとお馴染みのこの画面が出るはずです。

スクリーンショット 2020-05-28 10.30.38.png

環境構築は以上です! :tada::tada::tada:

まとめ

今回はQuickstart: Compose and Rails | Docker Documentationに沿ってRuby2.7とRails6の環境を作る方法についてまとめました。参考になれば幸いです。

なお、この記事の続編として以下のような記事を書いていこうかなと思っています。

  • Docker Composeのvolumesを使ってもっと効率的に
  • 初期装備なRails6にVue.jsを導入する
  • Docker Composeでwebpack-dev-serverを動かしてもっと快適に

(予定は未定です)

ではまた〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デイリードリル(CSRF)

Question

Railsには、悪意のある攻撃に対してセキュリティを高める仕組みが様々用意されています。
CSRFとは何か。
またCSRFと呼ばれるサイトの成りすましによるクラッキングに対して、どのような対策が取られているか述べてください。

Answer

CSRFとは

Webアプリケーション利用者自身が意図しない処理が実行されてしまう脆弱性または攻撃手法のことをクロスサイトリクエストフォージェリ(CSRF)といいます。

被害例

  • 利用者のアカウントによる物品の購入
  • 利用者の退会処理
  • 利用者のアカウントによる掲示板への書き込み

- 利用者のパスワードやメールアドレスが変更

※CSRF脆弱性の影響は「重要な処理」の悪用に限られるため、CSRFの脆弱性を個人情報の取得等に用いることはできません。

対策

rails では、ApplicationControllerにデフォルトで

protect_from_forgery with: :exception

という記述がされています。これにより、アプリで作られたフォームに対してトークンが発行され、正しいフォームからの通信なのかを判別することができます。
具体的には、まずサイトのHTMLに一意のトークンを埋め込みます。これと同じトークンを、セッションcookie(クッキー)にも保存しています。ユーザーがPOSTリクエストを送信すると、HTMLに埋められているCSRFトークンも一緒に送信されます。あとは、サーバ側でページのトークンとセッション内のトークンを比較し、両者が一致することを確認したらリクエストを受け付けます。

rails5.1まではprotect_from_forgery with: :exceptionがapplication_controller.rbで記述されていますが、Rails5.2 以降ではActionController::Base内でプロテクトしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails テーブルにcommentを追加するマイグレーションファイル

マイグレーションファイル

class AddCommmentsToStaffEmail < ActiveRecord::Migration[6.0]
  def change
    change_column_comment(:staff_emails, :employee_id, '業者ID') 
  end
end

change_column_comment

change_column_commentにテーブル名、カラム名、コメント内容 を記載する

change_column_comment(:table_name, :column_name, 'contents') 

環境

  • activerecord (6.0)

実行手順

マイグレーションファイル作成

コマンド

cmd.
$bundel exec rails g migration AddCommentsToTable

マイグレーションの実行

コマンド

cmd.
$bundel exec rake db:migrate

備考

  • 参考リンク

http://hotoolong.hatenablog.com/entry/2018/06/20/083106

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのユーザー登録機能で登場する「password」と「password_confirmation」の正体【初学者】

オンラインプログラムスクール「TeacAcademy(テックアカデミー)」さんのWebアプリケーションコースを受講していますがTwitterクローンのユーザー登録機能を作っている時にフォームの部分にテーブルのカラムに追加していないはずの「password」と「password_confirmation」という2つの謎のカラム指定されており「なんじゃこれ」って調べてみたら答えがあっさり見つかった。

この「password」と「password_confirmation」という2つはユーザーカラムで記述していた「has_secure_password」に謎が隠されていました。

「password」と「password_confirmation」の2つが登場するまで「has_secure_password」はセキュリティ強化のためのおまじない程度にしか思ってなかったのですが当然のことながらかなり重要な役割を果たしていましたね。

主な役割は3つ

①テーブルにパスワードを保存する時暗号化して保存。
②フォーム用に「password」と「password_confirmation」という変数をモデルに追加し、ログイン認証時のパスワード確認。
③ログイン認証のためのメソッド「authenticate」を提供。

今回該当するのは2つ目に示した役割。

このおかげで登録時のパスワードの2重確認が出来るようになったということですね。

ちなみに「password」と「password_confirmation」はテーブルのカラムではなく、一時的な変数として扱われるため、保存はされません。

分からないことがわかったときにはとても気持ち良いですが分かったつもりになっていたことも気付かされます。

初学者は大変です。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む