- 投稿日:2020-05-28T23:46:41+09:00
[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 5imageのクエリーは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つの手であるということを忘れず、他の観点も頭に入れておくようにしましょう。
- 投稿日:2020-05-28T22:34:59+09:00
[Rails]strftimeあれこれ
前置き
現在Railsでマッチングアプリ作成中ですが、strftimeをよく使うので、すぐ見れるよう書き方をまとめておきます。
strftimeって?
日時データを好きなようにビューに表示出来るメソッドです。
やってみよう
今回はcreated_atでやっていきます。
デフォルトだとこうですね。後ろの+0900は使わなさそうですね。
2020-05-22 16:11:01 +0900created_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はうるう秒 ありがとうございました!
- 投稿日:2020-05-28T22:03:02+09:00
Railsアプリで初めて自作ヘルパーを使ってリファクタリングした話
はじめに
今までRuby on Railsでアプリケーションを作成していて
ふと気になったことがありました。コントローラー作成時に毎回ついてくるコイツ
〇〇_helper.rb
このファイル何に使うんだろ...。
ヘルパーって書いてあるからヘルパーメソッドに関係しているのかな...??この時、私は
"わざわざ同時に作成される物だからきっと便利なファイルに違いない"
と、興味の向くままにQiitaで解説記事などを調べてみました。結論
自作のヘルパーメソッドを記述するファイルのようです。
(自作ヘルパー == カスタムヘルパー と言うらしい)
ヘルパーメソッドは今日の今日までGemに用意されているモノしか使用したことがなかったので
これは便利そうだ、と直感で思いました。さっそく使ってみた
他の方のQiita記事を参考にさせていただき
今回は個人開発したアプリのコードリファクタリングに使用してみました。
コントローラー内のリファクタリング内容を本記事の内容にします。環境
Rails 5.2.3
Ruby 2.5.1使い方
まずはリファクタリングをするコントローラーを見ていきましょう。
users_controller.rbclass 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 endcheck_userというメソッド内のunless以下がイケてないので
ヘルパーメソッドを作成してリファクタリングします。
helpers/users_helper.rbmodule UsersHelper def current_user?(user) current_user.id == user.id end endcurrent_user?というヘルパーメソッドを作成しました。
このメソッド名および内容はけっこう定番のようです。
helpersというディレクトリ直下に
application_helper.rbというファイルもありますが
これは複数のコントローラーやビューに適用させるヘルパーメソッドを管理するファイルのようです。
※今回は未使用
さっそく作成したヘルパーメソッドをコントローラー内に適用させましょう。users_controller.rbclass 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.rbclass 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.rbmodule PostsHelper def current_user_post?(a_post) current_user.id == a_post.user_id end endこれをコントローラーに適用していきます。
posts_controller.rbclass 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こちらもヘルパーを読み込んでリファクタリングしたら完了です。
最後に
今回はコントローラーに絞って記事を書きましたが自作ヘルパーはもちろんビューにも使用できます。
また、ビューに使用する場合はコントローラーと違って、読み込みの記述が必要ないみたいですね。
私もこのあとビューファイルのコードをリファクタリングしようと思います。どこか不備がありましたらコメントにてご指摘いただけると幸いです。
参考記事
- 投稿日:2020-05-28T21:54:29+09:00
個人的に頻繁に使った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を使った開発にアサインされて、戸惑いながらも開発していた時に得たものなので、優しい目で見ていただけますと幸いでございます。
使えそうなものが出てきたら追記していこうと思います。ありがとうございました。
- 投稿日:2020-05-28T20:49:28+09:00
Ruby 自作クラスの配列をソートする
Ruby 自作クラスの配列をソートする
100日後に1人前になる新人エンジニア(8日目)です
あれ自作したクラスの入った配列ってどうやってソートするんだっけ...
って今日ちょっと詰まってしまったのでメモとして残しておきます。自作のクラス
user.rbclass 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.rbusers.sort #エラーが発生 #`sort': comparison of User with User failed (ArgumentError)上のように怒られます。
Userを比較することに失敗しました。
オブジェクトそのままの比較はできないですね。ということでオブジェクトの属性で比較します。
そのためにはsortメソッドではなくsort_byを使います
年齢で比較するruby.rbusers = 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.rbusers = users.sort_by(&:height)この一行でusersの配列に入ったuserを身長昇順に並び替えられます。
Rubyてすごいよね。本日はここまでです。
みてくださった方ありがとうございました。1人前のエンジニアになるまであと93日
- 投稿日:2020-05-28T20:39:17+09:00
【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/
- 投稿日:2020-05-28T20:39:17+09:00
【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/
- 投稿日:2020-05-28T20:22:38+09:00
【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.rbresources :users,only: [:show]今回使うアクションはshowアクションだけなので、ひとまず上記の記述でいきます。
手順❸:コントローラーに記述を追加
users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end endMySQLから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>ユーザー情報が少なく、サイドバーも編集ボタン等もないのでかなり質素ですが、基本形はできあがりました。
- 投稿日:2020-05-28T18:19:09+09:00
fullcalendarのイベントを特定の条件のイベントだけ表示させる方法
Qiita初投稿です。よろしくお願いします。
fullcelndarのイベントをユーザー詳細画面でそのユーザーのみのイベントで表示させる時に、苦戦したので備忘録として投稿させていただきます。準備
本記事は特定のイベントの表示を目的にしているため、準備は自分が参考にした記事を貼らせていただきますので、参考にしていただけたらと思います。この記事の通りに実装してもらったらカレンダーは表示できると思います。
参考記事目的
では、本来の目的であるユーザーのみのイベントを表示させていきます。
実装
ログイン機能の実装
Gemfilegem '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.rbclass 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の知識が少し必要でした。
ご不明な点があれば、コメントしていただけたらと思います。
- 投稿日:2020-05-28T17:28:19+09:00
データの並び替え降順、昇順/Railsでまとめてみた
投稿したデータや記事の並び替えを変える際に、descが降順?降順?で毎回ググって調べていたのでまとめました。
board.rbclass Board < ApplicationRecord scope :desc, -> { order(created_at: :desc) } #作成の降順 scope :asc, -> { order(id: :asc) } #idの昇順 デフォルトでasc(昇順)で表記されるので明示的に書くことは少ない end
- 投稿日:2020-05-28T16:58:57+09:00
個人アプリRails6.0をHerokuにデプロイした時の失敗話
背景
個人でRailsアプリをHerokuへデプロイしようした際に、つまづいたポイントが2点あったので記事にします。
- DBをMySQLで作成してしまった。
- 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=postgresqlPostgreSQLのデータを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
初歩的なミスで数日時間を取られてしました。
エラー解決のきっかけは、たまたま同じ事をしている記事が見つけたからです。
小さなエラー解決の記事でも、誰かの助けになる事が身に沁みました。だから私もこの記事を書くことに決めました。参考
- 投稿日:2020-05-28T16:45:51+09:00
【神業】面倒なHTMLタグ入力を光の速さで終わらせる【定型文ツール】
はじめに
フリーランスRailsエンジニアおよびWEBライターをやっているソエノと申します。この記事では「面倒な定型文入力を神速で終わらせる方法」についてご紹介します。
注意事項 : コメント欄への書き込みについて
コメント欄への「誹謗中傷・暴言」の書き込みはインターネット上であれ「威力業務妨害」が成立する危険性があります。全てのコメントは削除後も筆者のメールアドレスに保存されていますので、何卒ご注意の程よろしくお願いいたします。
結論
「Dash」というツールを使います。以下のようなことができます。以下は、HTMLのaタグを書くときの例です。
う〜ん、神。普通、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のシステム環境設定を開きましょう。パソコン左上に表示されているりんごマークをクリックしたら「システム環境設定」というのがありますから選びます。
家のマークで「セキュリティとプライバシー」というのがありますので、これを選びます。下記画面で左側のウィンドウを少し下にスクロールすると、「アクセシビリティ」という項目があるので、選びます。そして左下の錠マークをクリックしてパソコンのロックを解除します。あとは「Dash」というところのチェックをONにしましょう。これで準備OKです。
それではDashを起動しましょう。
起動後の画面はこんな感じです。まずは右上の「Manage Docsets」をクリックしましょう。
上記のような画面になります。続けて、上のメモマーク「Snippets」を選択しましょう。
Snippetsを選択したら上記のような画面になるので、Enable snippetsというチェックをONにしましょう。ONにしたらこの設定画面は不要なので左上の×マークで閉じます。
設定後、ホーム画面のSearchという欄でSnippetsというのが選択できるようになりますので、クリックします。
すると上記のようになるので、New Snippetsをクリック。
なんか入力欄が二つ出てきます。上の細い入力欄が、先ほど紹介した「呼び出しの呪文」です。下の大きなの欄が「実際に呼び出されるテキスト」になります。試しに、次のように設定してみましょう。上の入力欄には
;atag
、下の入力欄には<a href="__link__">__text__</a>
と書きました。設定はこれで完了です。特に保存とかは無いみたいなので、このままDashアプリからは離れてしまって構いません。あとは、適当なテキスト入力欄に
;atag
と打ってみてください。なんか召喚されると思います。新しいスニペットを登録する
この「スニペット」というのが先ほどのように「召喚の呪文と、そこから召喚されるテキストの組み合わせ」という意味のようです。新しいスニペットを登録するには、下記動画のように画面上のプラスマークをクリックすればいいようです。
さらに高速化していく方法【カスタマイズ機能】
上記では
__link__
や__text__
といった記述によってテキストを入力できるようにカスタマイズしました。これは「アンダースコア2つで囲むと任意テキスト入力できるようにする」というDashのカスタマイズ機能です。このほかにも便利な機能がいくつもありますので、使えそうなものをピックアップしてご紹介します。クリップボードの情報を自動で挿入する
ここでいうクリップボードというのは「コピーしたテキスト」のことです。例えば、さっきのaタグ生成のやつを以下のように書くと、直前にURLをコピーしていれば、コピーしたテキストを勝手にペーストしてくれます。
@clipboard
という記述がそのままクリップボードの情報に置き換わるようです。挿入後のカーソルの位置を指定する
さっきはDashで呼び出されたウィンドウ上で文字入力していましたが、先にaタグを生成してからエディタ上でテキストを入れたい時はこれが便利です。
@cursor
と書くと、スニペット挿入後に勝手にカーソルが@cursor
と書いた位置に飛んでくれます。便利なプリセット
この記事をここまで呼んでくださった方のために、プリセットをご用意してみました。よろしければ、ご活用ください。
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: trueJavaScript
console.log;console console.log("@cursor");jQueryを使ったDOM取得;jq $("@cursor")記事を読んでいただきありがとうございました
ここまで読んでくださって本当にありがとうござます。私から皆さんにGiveさせて頂けるのはこうしたエンジニア向け情報くらいしかありませんが、皆さんに記事を読んでいただけて、そして皆さんのお役に立つことができれば、それは私の何よりの喜びになります。今後とも皆さんのお役に立てるよう記事執筆活動などを進めていきたいと思っていますので、これからも何卒よろしくお願いいたします。
まとめ
・Dashというアプリを使うことで、HTMLタグの入力を楽にしたり、Railsなどのプログラミング言語における「定型句」を覚える必要がなくなりました。代わりに自分で登録した「召喚の呪文」を覚える必要がありますが、これまでの作業よりも幾分マシになっているはずです。
以上となります。それでは、生産的なコーディングライフを!!
筆者Twitter: soeno_onseo
- 投稿日:2020-05-28T15:14:46+09:00
Docker上にRails環境構築(DBはmariadb)
Docker上にRails環境(DBはmariadb)を構築したい!
ローカル環境上にRailsの環境構築を行うと、どんどんローカルが汚れていくので嫌だなと。
なのでローカル上でDocker for Windowsを使用してRails環境を構築したく記事を書きました。
※色々弄りすぎた結果、この投稿に記載しているDockerファイルやdocker-composeファイルも汚れた状態での投稿になっています。。。綺麗にしたい。。。構築にあたっての前提環境
- Windows10 Pro(HomeだとDocker for Windowsがインストールできないため)
- Docker for Windowsのインストール 公式を見てインストール
構築手順
- プロジェクトを作成したい場所にフォルダを作成
上記で作成したフォルダ内に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同一フォルダにdocker-compose.ymlを作成
docker-compose.ymlversion: '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プロジェクトの作成
コマンドプロンプトを立ち上げプロジェクトを作成するフォルダまで移動後、下記コマンドを実行する$ docker-compose run --rm api rails new . --force --database=mysql --skip-bundle $ docker-compose run --rm api bundle exec spring binstub --alldatabaseの接続情報を設定
プロジェクトフォルダ/config/database.ymlにDBの接続情報を記載default: &default adapter: mysql2 encoding: utf8 pool: 5 username: hoge password: hoge host: maria-db起動する
$ docker-compose up
- 投稿日:2020-05-28T13:58:00+09:00
マイグレーションファイルがダブっちまった
近況報告
絶賛就活中でございます。最近やっているのは習慣化ですね。最近,英語と筋トレを習慣化させることができました。いきなり英語は飽きると思ったので,Gacktの母音発音のyoutubeをみて簡単な文を音読しています。考えが改まったことがひとつ,Google翻訳ってあるじゃないですか。あれ,全然翻訳してくれないからゴミだと昔は思っていたんですけど,実際は自身の発音がゴミだったってことがわかりましたw 筋トレは腹筋スクワットメインの運動を自重かつ強度をあげて20分だけ行なっています。腹筋が生まれて初めて横以外に割れそうな勢いです。そしてここ数日で始めたのは朝型転換です。ここ二ヶ月日付が変わったらアマゾンなプライムでビデオ見まくっていたので,生活習慣の改善として行なっています。
今回のお題
マイグレーションファイルがダブってる
始めたての初心者がいつの間にかハマるやつですね!・・・はい,やらかしました。
⇨解決策は片方を消す。以上。マイグレーションファイル
●マイグレーションファイルは噛み砕くとテーブルの設計図です。
rails db:migrateの際にテーブルを作成更新するときに利用します。一度利用されたマイグレーションファイルはテーブルを再度更新,生成しない限り用いることはありません。実際削除しても問題ありません。●本当に削除していいの?
そこに書かれている情報を二度と使わないのであれば消してもいいよ。
開発においてテーブルの変更はなんども行われます。そのときにマイグレーションファイルがないと以前はあったとしてもパソコンがテーブルを読み取れなくてエラーを起こします。また,開発環境で用いたテーブルは本番環境でも使えるわけではなく,本番環境のDBを設置して再度テーブルを組み立てることが多いので,本番の段階でマイグレーションファイルがないのは一大事なわけです。長くなりましたが結論はもう使わなくてもほっとけです。●ダブった場合
マイグレーションファイルがダブっている場合も消したらテーブルが作成できなくなると心配になります(自分はなった)。ただ、この場合は同じ設計書が二枚あるだけなので,一枚になっても変わりませんし,マイグレーションファイルの数だけテーブルができるわけではないので安心して片方消してください。最後に
内定が欲しい! エンジニアライフを早く堪能したい!
- 投稿日:2020-05-28T13:56:29+09:00
Rails チュートリアルをやってみる ch1
- 投稿日:2020-05-28T13:42:12+09:00
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"以上です。
- 投稿日:2020-05-28T11:38:26+09:00
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 1Rails 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のバージョン」の組み合わせによって話がいろいろと変わってきます。それぞれの組み合わせで何が起きるか、以下の表にまとめておきます。
最終的には上の表の「DB側とRailsのミスマッチ?」欄が"NO"になる組み合わせが実現できれば理想的な状態、となります。
参考文献
- Rails 6.0でDeprecatedになるActive Recordの振る舞い3つ - かみぽわーる
- Deprecate mismatched collation comparison for uniquness validator by kamipo · Pull Request #35350 · rails/rails
- 本当にあったRailsの怖い話
謝辞
この件についてはRailsコミッタのkamipoさんにTwitter上で質問して丁寧に回答していただきました(参考)。kamipoさん、どうもありがとうございました!
- 投稿日:2020-05-28T11:07:30+09:00
Docker Composeを使った初期装備なRails6/PostgreSQL環境の作り方
Docker Composeを使ったRails環境を作る場合、Quickstart: Compose and Rails | Docker Documentationがとても参考になります。ただし、このドキュメントはRails5を前提として書かれており、現時点(2020-05-28)で最新のRails6を使う場合には一部修正が必要です。
そこでこの記事では、Rails6を使う場合について記載します。また、この記事の対象読者は下記を想定しており、環境構築は下記の方針と前提に基づいて行ないます。
想定読者
- Rails環境構築でいつも躓くの方(丁寧めに説明します)
- 未来の自分(思い出せるように書きます)
方針
- Quickstart: Compose and Rails | Docker Documentationに準拠する(DBはPostgreSQLを使う)
- Ruby2.7とRails6の環境を作る
- できる限りRails6の初期装備を使う(Webpackerを使う、turbolinksを使う、など)
前提
- 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
となってますがこれは後で書き換えるのでいったんそのままで。DockerfileFROM 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'
となってますがこれもあとで書き換えます。このままで。Gemfilesource '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.ymlversion: '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
に書き換えます。Gemfilesource '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.ymldefault: &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.ymlversion: '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 にアクセスするとお馴染みのこの画面が出るはずです。
環境構築は以上です!
まとめ
今回はQuickstart: Compose and Rails | Docker Documentationに沿ってRuby2.7とRails6の環境を作る方法についてまとめました。参考になれば幸いです。
なお、この記事の続編として以下のような記事を書いていこうかなと思っています。
- Docker Composeのvolumesを使ってもっと効率的に
- 初期装備なRails6にVue.jsを導入する
- Docker Composeでwebpack-dev-serverを動かしてもっと快適に
(予定は未定です)
ではまた〜
- 投稿日:2020-05-28T11:04:17+09:00
デイリードリル(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内でプロテクトしています。
- 投稿日:2020-05-28T10:26:46+09:00
Rails テーブルにcommentを追加するマイグレーションファイル
マイグレーションファイル
class AddCommmentsToStaffEmail < ActiveRecord::Migration[6.0] def change change_column_comment(:staff_emails, :employee_id, '業者ID') end endchange_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備考
- 参考リンク
- 投稿日:2020-05-28T08:35:40+09:00
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」はテーブルのカラムではなく、一時的な変数として扱われるため、保存はされません。
分からないことがわかったときにはとても気持ち良いですが分かったつもりになっていたことも気付かされます。
初学者は大変です。。