- 投稿日: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:56:34+09:00
【Ruby】複数の条件下で、配列から特定の値を取り出す方法【select・each】
できるようになること
複数の条件を用いて、配列から特定の値を見つけることができます。
例)1~10までの数字のうち、5以下かつ2で割れるものを導き出しなさい。
みたいな問題が解けるようになります。方法
Array#eachとArray#selectを使います。
numbers = (1..10).to_a rule = [["<", 5],["%", 2]] rule.each do |b| numbers.select! do |a| if b[0] == "<" a <= b[1] else a % b[1] == 0 end end end p numbers # [2,4]つまりどう言うことか
条件(今回はrule)をeachで回して、その中に判別したいもの(今回はnumber)を入れる必要があります。かつて、逆にして困っており、最近この方法に気づいたのでまとめさせていただきました。
何か同じ悩みで引っかかっていた人の解決になれば嬉しいです。
- 投稿日: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-28T19:49:54+09:00
Ruby と Python で解く AtCoder ARC080 D シミュレーション
はじめに
AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。今回のお題
AtCoder Regular Contest D - Grid Coloring
Difficulty: 855今回のテーマ、シミュレーション
内容は難しいところはないと思いますが、実装に手間がかかりそうな問題です。
1 2 2 3 3 4 4 4 4 3 5 5 5 5 5与えられた数値で上から順に左右に塗りつぶせばOKです。
Ruby Array
ruby.rbh, w = gets.split.map(&:to_i) _ = gets.to_i a = gets.split.map(&:to_i) m = Array.new(h){Array.new(w, 0)} y, x, lr = 0, 0, 1 a.each_with_index do |n, v| n.times do m[y][x] = v + 1 x += lr if x == w y += 1 x = w - 1 lr = -1 elsif x < 0 y += 1 x = 0 lr = 1 end end end h.times do |i| puts m[i].join(' ') endjoin.rbputs m[i].join(' ') puts m[i] * ' '最近知ったのですが、
join
は上記の様に書くこともできます。Ruby Class
ruby.rbclass MASS def initialize(h, w) @h = h @w = w @m = Array.new(@h){Array.new(@w, 0)} @x = 0 @y = 0 @LR = 1 end def draw(v, n) n.times do @m[@y][@x] = v + 1 @x += @LR if @x == @w @y += 1 @x = @w - 1 @LR = -1 elsif @x < 0 @y += 1 @x = 0 @LR = 1 end end end def out @h.times do |i| puts @m[i].join(' ') end end end h, w = gets.split.map(&:to_i) _ = gets.to_i a = gets.split.map(&:to_i) m = MASS.new(h, w) a.each_with_index do |v, i| m.draw(i, v) end m.out今回の問題では、あまり有効ではないのですが、学習を兼ねて
class
を使用してみました。
コード長が倍近くになりましたが、実行時間は変わらないようです。Python
python.pyfrom sys import stdin def main(): input = stdin.readline h, w = map(int, input().split()) _ = int(input()) a = list(map(int, input().split())) m = [[0] * w for _ in range(h)] x, y, LR, v = 0, 0, 1, 0 for n in a: v += 1 for _ in range(n): m[y][x] = str(v) x += LR if x == w: y += 1 x = w - 1 LR = -1 elif x < 0: y += 1 x = 0 LR = 1 for i in range(h): print(" ".join(m[i])) main()Python のコード長が長いのは、半角スペースもカウントしているからでしょうか。
Ruby Array Ruby Class Python コード長 (Byte) 392 631 603 実行時間 (ms) 17 18 25 メモリ (KB) 2428 3836 3828 まとめ
- ARC 080 D を解いた
- Ruby に詳しくなった
- Python に詳しくなった
- 投稿日:2020-05-28T19:21:37+09:00
Rubyでリスキーダイスを作ってみた
rubyの練習でハンターハンターに登場するリスキーダイスを作ってみた。
1から20の数字があり20が大凶です。def risky_daice array = [] puts '何回振ろうかな' stop = gets.to_i puts 'ダイスを振るよ' while true deme = rand(1..20) num = array.push deme if deme == 20 puts deme puts 'あーあ出ちゃった' p "#{num.count}回目で大凶がでた" break elsif num.count == stop puts deme puts 'そこでやめ' p "#{num.count}回振れたよ。また挑戦してね" break else puts deme end end end risky_daice-> % ruby risky_daice.rb 何回振ろうかな 20 ダイスを振るよ 16 12 4 1 12 8 18 19 15 2 16 5 3 5 5 3 20 あーあ出ちゃった "17回目で大凶がでた"本番の前に振るといいことあるかも?
- 投稿日:2020-05-28T18:19:43+09:00
インスタンスメソッドとクラスメソッドの違い
Rubyに関して学習中に表題の部分で混乱してしまったので、
整理するためにもまとめて見ました。※誤っている点がありましたら、ご指摘いただけると幸いです。。。
インスタンスメソッドとクラスメソッドの違い
class Example def index end end ①example = Example.new example.index //インスタンスから呼び出す →○ ②Example.index //クラスから直接呼び出せない →こちらを使用する際はメソッドにself.をつける(self.index) index //もちろんこれではエラー表示要するに、基本メソッドを呼ぶ際は、インスタンスを作成してそこからよび起こす(①)。
→インスタンスメソッド②のようにインスタンスの作成をせずともメソッドを呼び起こす方法。
→クラスメソッドといったようなイメージ。
クラスメソッドの使用のメリットとしては、他インスタンスの情報を含まないメソッドとして使用できる点があるようです(具体例はわからなかったので、わかり次第まとめます。)
まずはクラスと関係性はあるが、他インスタンスの情報を含まない独立したメソッドという認識でOKそう。
- 投稿日:2020-05-28T18:19:43+09:00
インスタンスメゾットとクラスメゾットの違い
Rubyに関して学習中に表題の部分で混乱してしまったので、
整理するためにもまとめて見ました。※誤っている点がありましたら、ご指摘いただけると幸いです。。。
インスタンスメゾットとクラスメゾットの違い
class Example def index end end ①example = Example.new example.index //インスタンスから呼び出す →○ ②Example.index //クラスから直接呼び出せない →こちらを使用する際はメゾットにself.をつける(self.index) index //もちろんこれではエラー表示要するに、基本メゾットを呼ぶ際は、インスタンスを作成してそこからよび起こす(①)。
→インスタンスメゾット②のようにインスタンスの作成をせずともメゾットを呼び起こす方法。
→クラスメゾットといったようなイメージ。
クラスメゾットの使用のメリットとしては、他インスタンスの情報を含まないメゾットとして使用できる点があるようです(具体例はわからなかったので、わかり次第まとめます。)
まずはクラスと関係性はあるが、他インスタンスの情報を含まない独立したメゾットという認識でOKそう。
- 投稿日: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:47:24+09:00
なぜファイル末尾に改行を入れるのか
Rubyのプログラムを書いていてふと思ったのでメモ。
POSIX上、テキストファイルは「行」の集合体で、そして「行」は「0文字以上の文字+改行」からなっている、とのことです(Qiita)。
Rubyのコードでも、(よほど変なことをしない限り)それに従わない必要性のある理由もないことでしょう。C言語においても、改行で終わらないコードは正しく動くことが保証されません。
- そう決まっている
- 改行がなかったらエラーが起きるかもしれない
ということみたいですね。
VSCodeでファイル保存時に自動で改行うを入れる設定
- ツールバー > Code > Preferences > Settings で設定を開く
- 「insert newline」で検索
- 「Files: Insert Final Newline」の項目にチェックを入れる
- 投稿日:2020-05-28T13:58:00+09:00
マイグレーションファイルがダブっちまった
近況報告
絶賛就活中でございます。最近やっているのは習慣化ですね。最近,英語と筋トレを習慣化させることができました。いきなり英語は飽きると思ったので,Gacktの母音発音のyoutubeをみて簡単な文を音読しています。考えが改まったことがひとつ,Google翻訳ってあるじゃないですか。あれ,全然翻訳してくれないからゴミだと昔は思っていたんですけど,実際は自身の発音がゴミだったってことがわかりましたw 筋トレは腹筋スクワットメインの運動を自重かつ強度をあげて20分だけ行なっています。腹筋が生まれて初めて横以外に割れそうな勢いです。そしてここ数日で始めたのは朝型転換です。ここ二ヶ月日付が変わったらアマゾンなプライムでビデオ見まくっていたので,生活習慣の改善として行なっています。
今回のお題
マイグレーションファイルがダブってる
始めたての初心者がいつの間にかハマるやつですね!・・・はい,やらかしました。
⇨解決策は片方を消す。以上。マイグレーションファイル
●マイグレーションファイルは噛み砕くとテーブルの設計図です。
rails db:migrateの際にテーブルを作成更新するときに利用します。一度利用されたマイグレーションファイルはテーブルを再度更新,生成しない限り用いることはありません。実際削除しても問題ありません。●本当に削除していいの?
そこに書かれている情報を二度と使わないのであれば消してもいいよ。
開発においてテーブルの変更はなんども行われます。そのときにマイグレーションファイルがないと以前はあったとしてもパソコンがテーブルを読み取れなくてエラーを起こします。また,開発環境で用いたテーブルは本番環境でも使えるわけではなく,本番環境のDBを設置して再度テーブルを組み立てることが多いので,本番の段階でマイグレーションファイルがないのは一大事なわけです。長くなりましたが結論はもう使わなくてもほっとけです。●ダブった場合
マイグレーションファイルがダブっている場合も消したらテーブルが作成できなくなると心配になります(自分はなった)。ただ、この場合は同じ設計書が二枚あるだけなので,一枚になっても変わりませんし,マイグレーションファイルの数だけテーブルができるわけではないので安心して片方消してください。最後に
内定が欲しい! エンジニアライフを早く堪能したい!
- 投稿日:2020-05-28T12:18:29+09:00
hamlでクラス名やid名に変数を使う
アプリ開発中にクラス名にインデックスをつけたいときがあり、書き方が分からずつまづいたので、同じような方がおられましたら参考にしてください。
基本
まず、基本的なクラス名id名の書き方は「.」「#」始まりで書き始めます。
#クラス名 .item_name %p.item_name #ID名 #item_name %p#item_name変数を使った書き方
そして今回わたしがつまづいたのはeach文の中で変数名を使った書き方です。
下記の書き方で適用されました!-@items.each_with_index do |item, i| %div{class: "#{item.name}"} %p{class: "item_name_#{i}"}クラス名を{}で囲うと、適用されます!
each文ではない場合でも同じ書き方で適用されます%div{class: @item.name}
- 投稿日: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上のユニーク制約に一致しないため、バリデーションの結果が100%信用できない
- 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上のユニーク制約に一致しないため、バリデーションの結果が100%信用できない
- 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上のユニーク制約に一致しないため、バリデーションの結果が100%信用できない
- 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-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-28T01:27:11+09:00
Ruby カラオケマシン問題 解いてみた(解答例あり)
はじめに
『プロを目指す人のためのRuby入門』通称チェリー本を学習後のプログラミング初心者です。
インプットしたものを手を動かして実践してみたいなと思ったら、作者の記事を見つけました。
「アウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題を集めてみた(全10問)」この二つ目の問題を解いてみました。
一問目:カレンダー作成問題(たのしいRuby 練習問題)
問題
詳しくは実際の問題文をみてください。
問題知ってる人は、解答例へジャンプ。<前提>
カラオケにはキーを変える機能があります。
+1するとキーが1つ上がります。
-1するとキーが1つ下がります。
たとえば「ドレミファソ」というメロディのキーを2つ上げると「レミファ#ソラ」になります。
「ドレミファソ」のようにカタカナだとプログラムで扱いづらいので、英語の読み方、つまりアルファベットに置き換えましょう。
ド レ ミ ファ ソ → C D E F G
レ ミ ファ# ソ ラ → D E F# G A<本題>
かえるのうたのメロディは以下のような文字列で表現されます。
これを(+6)や(-11)など指定大きさ分キーを変えます。"C D E F |E D C |E F G A |G F E |C C |C C |CCDDEEFF|E D C "
実行例はこんな感じになります。
melody = "C D E F |E D C |E F G A |G F E |C C |C C |CCDDEEFF|E D C " karaoke = KaraokeMachine.new(melody) karaoke.transpose(2) # => "D E F# G |F# E D |F# G A B |A G F# |D D |D D |DDEEF#F#GG|F# E D " karaoke.transpose(-1) # => "B C# D# E |D# C# B |D# E F# G# |F# E D# |B B |B B |BBC#C#D#D#EE|D# C# B " # 1オクターブ(12音)以上変えることもできる karaoke.transpose(14) # => "D E F# G |F# E D |F# G A B |A G F# |D D |D D |DDEEF#F#GG|F# E D "解答例
こんな感じになりました。
class KaraokeMachine def initialize(melody) @melody = melody end def transpose(amount) scales = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] @melody.split(/(\w#?)/).map{ |m| /(\w#?)/.match?(m) ? scales[(scales.find_index(m) + amount) % scales.length] : m }.join end end当初下記のコードで完成したと思ってたのですが、
class KaraokeMachine def initialize(melody) @melody = melody end def transpose(amount) scales = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] @melody.split("").map{ |m| m == " " || m == "|" ? m : scales[(scales.find_index(m) + amount) % scales.length] }.join end endこれだと下記のようなメロディが来たときに
アルファベット
と#
が分離してしまうことに気付き正規表現を用いて修正しました。melody = "F# G# A# B |A# G# F# |A# B C# D# |C# B A# |F# F# |F# F# |F#F#G#G#A#A#BB|A# G# F# "解説
自分の備忘録としても書いておきます。
この問題での音階はシャープも入れると「ドド#レレ#ミファファ#ソソ#ララ#シ」の繰り返しなので,
scales = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]`このように一つの配列にしました。
melody =
で受け取った文字列を、音階
とそれ以外
に区切り、配列にします。melody = "C D E F |E D C |E F G A |G F E |C C |C C |CCDDEEFF|E D C " melody.split(/(\w#?)/) => ["", "C", " ", "D", " ", "E", " ", "F", " |", "E", " ", "D", " ", "C", " |", "E", " ", "F", " ", "G", " ", "A", " |", "G", " ", "F", " ", "E", " |", "C", " ", "C", " |", "C", " ", "C", " |", "C", "", "C", "", "D", "", "D", "", "E", "", "E", "", "F", "", "F", "|", "E", " ", "D", " ", "C", " "]配列の各要素を
mapメソッド
のブロック内で条件の真偽値を求めぞれぞれの値を、新しい配列に追加していきます。
1.音階
なら指定された分キーを変更する。
2.それ以外
ならそのままの要素を追加。
を次のコードで実行していきます。@melody.split(/(\w#?)/).map{ |m| /(\w#?)/.match?(m) ? scales[(scales.find_index(m) + amount) % scales.length] : m }.join
音階
のindex
と指定されたキーの増減数を足した数をscales
のlength
で割ると、キーを変更した先の対応する音階
のindex
が求められます。
ですので、scales[ ]
の値に代入しindex
を指定すればキー変更をした音階
がわかります。最後にこの
音階
とそれ以外
をもう一度join
して文字列に直せば完成です。さいごに
若干強引気味になってしまったところもあるかなと思います。
何か間違いやご意見あれば教えてください。