20200830のRailsに関する記事は18件です。

【Rails】間違えたマイグレーションファイルをdown,削除できない→rails db:migrate:resetで解決できた

前の記事である、
【Rails】テーブルにカラムを追加する際、キャメルケース以外で記載したらどうなるの?
にて、自分が感じた疑問を素直にコマンドで実行したら、(複数作成した)不要なファイルがdown・削除が出来なくなりました(汗)

今回、たくさんの気づきがありましたので、反省を込めて記事にしようと思います。

出来ないことと、エラー内容

ターミナルにおいて、不要ファイルを削除したいが、削除以前に、down状態にできない(rollbackできない)

#ターミナルに表示されるエラー
foo@fogefogenoMacBook-Air bar % bundle exec rake db:migrate:down VERSION=20200829125501_Nickname2.rb
rake aborted!
Invalid format of target version: `VERSION=20200829125501_Nickname2.rb`
/Users/fogefoge/.rbenv/versions/2.6.5/bin/bundle:23:in `load'
/Users/fogefoge/.rbenv/versions/2.6.5/bin/bundle:23:in `<main>'
Tasks: TOP => db:migrate:down
(See full trace by running task with --trace)

解決策

先に解決した方法を載せておきます。
私は現在、プログラミングスクールに通っており、メンターさんに助けていただきました。
(本当に、ありがとうございました。)

1.Railsファイル上で、自分がこうしたいというファイルの状況を作り出す。
①左側で、削除したかったファイルを全て右クリック押して削除
②今回残しておきたい「20200829063700_devise_create_users.rb」のファイル中に、右側9行目にあたる(今回テーブルにカラムとして追加したかった)nicknameを追加して記載。

20200830VScode.png

2.ターミナル上で、以下のコマンドを実行する。
 「rails db:migrate:reset」とは、削除して作ってマイグレートしてくれるコマンドだそうです。

#ターミナル上で、以下コマンドを実行
foo@fogefogenoMacBook-Air bar % rails db:migrate:reset
#実行後、表示された内容
Dropped database 'pictweet_development'
Dropped database 'pictweet_test'
Created database 'pictweet_development'
Created database 'pictweet_test'
== 20200825113455 CreateTweets: migrating =====================================
-- create_table(:tweets)
   -> 0.0130s
== 20200825113455 CreateTweets: migrated (0.0131s) ============================

== 20200829063700 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0412s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0603s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0429s
== 20200829063700 DeviseCreateUsers: migrated (0.1469s) =======================


出来ましたー!!

そもそも、やらかしたこと

ここからは、デバック中含め「やらかしてしまったなー」と感じたポイントを書いていきます。

やらかしたことその1

一つ一つ試さないで(ロールバックしないで)、(興味本位で)不要なマイグレーションファイルを一気に複数作ってしまったこと。

やらかしたことその2

実験→正常(カリキュラム通り)→実験→実験の順で行ったこと。
rollbackの方法では、手前から差し戻しするんだったと、デバック中に思い出す。
途中に、正常を挟んでしまったため、rollbackで戻すなら「いったん全戻し」かつ「正しいの作り直し」になってしまうことに気づく。

やらかしたことその3

何も考えず、実験に使った不要マイグレーションファイルを右クリックして、一気に削除したこと。

ふと、削除ってこんな感じでいいんだっけ?そういえばrollbackっていう単語あったけどそれやってないけどいいんだっけ?と疑問に思い調べたところ、
●右クリックでマイグレーションファイルを消すとファイル上は削除されるが、ターミナル上は消えない(Rails内に残ってしまう)
●消したファイルは見えないので、同じファイル名などで今後作成したときバグになりうることがある
という事がわかった、対処し始めた。

やらかしたことその4

やみくもにデバックしてしまい、(履歴も上手に直さず)、沼に陥ったこと。
特に、「あ、サーバーが起動しているからダメなのかな?」と思ったが、コントロールC(サーバーを落とすショートカットキー)が効かず、ターミナルを×で閉じてしまったこと。コマンド履歴消えた。

やみくもにデバックしてしまったこと

ここからは、上記「やらかしてしまったこと」に気づき、やみくもにデバックしてしまったことを書いていきます。

やみくもデバック その1

キータで見つけた「NO FILE」の削除方法をやってみよう。
参考にさせていただいた記事
【Rails】マイグレーションファイルの削除
Ruby on Rails 『NO FILEのmigrationを削除する方法』

?touthして、migrateして、downしようと思ったが、downがうまくいかない。
?キータで書かれていた通り、touth時に、ファイル名を消した名前ではなく「hoge」名にしたからか?

やみくもデバック その2

ファイル名を、「hoge」ではなく、削除した名前に変えてみよう
?うまくいかない

やみくもデバック その3

クラス名がそういえば一致してないや
?うまくいかない
クラス名とファイル名が違うからこのエラーでは?と気づけた記事

やみくもデバック その4

そういえば、上記1〜3について、手前のファイルではなく、一番古いファイルからコマンド実行していた。
一番古いファイルではなく、手前のファイルからやってみよう。
?うまくいかない
手前から直さないといけないことに気づけた記事

やみくもデバック その5

そういえば、サーバー立ち上げていたからかな?「コントロールC」のショートカットキーが上手くいかず×で閉じる。
?うまくいかないし、履歴も消えてしまったし、本当にミスった。

やみくもデバック その6

そもそもちゃんとエラー見てなかった。binって書いてある。
大枠のフローが載っている処理っぽい。
クラス名やファイル名が、存在していないよっていうエラーぽいけど、上記を行っても上手くいかない。

以上です。

まとめ

調べれば調べるほど、マイグレーションファイルは安易に追加したり、削除したりしてはいけないことを痛感しました。

特にチーム開発の時では、ファイル削除だけだと、他の人が何をしたのか見えないため、バグの原因になってしまうことを学べたのが大きかったです。

また何度も、マイグレーションの状況を確認したり、downさせたいコマンドを打ったりしたので、マイグレーションに対する理解も深まりました。

そして、
●やみくもデバックしてしまうと沼にハマることを痛感
●やみくもデバックしないためにもエラーが起こった時に、いろんな可能性が選択肢として浮かぶレベル
まで成長したいと思いました。

メンターさんには、解決策と温かい言葉をかけていただいて本当に感謝です。

読んでいただいてありがとうございました。

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

【エラー】アソシエーション

特技はエラー生成・・・

アソシエーションの蓋を開けてみれば大したことのないエラーです。

cba1fc592f3761b8133678864f62fa7b.png

<% current_user.rooms.each do |room|>

userというテーブルとroomテーブル関係のアソシエーションを組んでいた記述で、roomsがないよと言われたんですが、どこを探しても間違っていないと思いきや、アソシエーションはやっぱりアソシエーションが原因でした。

/models/room.rb

has_many :room_users
has_many :users, through: :room_users

/models/user.rb

has_many :room_users
has_many :users, through: :room_users

ここで何が違ったのか全く気づけなかったのですが、アソシエーション指定がどちらもusersになっていました。

なので
/models/user.rb

has_many :room_users
has_many :rooms, through: :room_users

でOKです。

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

【エラー】localhostに接続できなかったサーバーの重複

特技はエラー生成・・・

localhost:3000にアクセスしたら急に読み込みが止まったままになってしまい、ターミナルを確認したところ
こんなような記述が(他略)

Address already in use - bind(2) for "127.0.0.1" port 3000 (Errno::EADDRINUSE)

原因は、ローカルサーバー(rails s)を切らずに(control cをせずに)ターミナルのタブを消してしまっていたので接続が重複して飽和状態になっていたとのこと

lsof -i :3000
を入力してポート接続を確認→PIDの番号を確認

kill -QUIT <PIDの番号>

で、ポートを空けてあげると直りました。

こちらの記事を参考にしました
https://qiita.com/nagao_norihiro/items/aba40bd4e0eac9f9a92d#comments

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

Rails label_tag 英語が小文字表示になってしまう(humanize)

はじめに

label_tagを用いてフォームを作成していた時にニュース IDをnameに指定しても表示がニュース idとなってしまうことが起きたので、Railsのソースコードを読んで小文字変換されてしまう理由を調べてみました。

前提として

<%= label_tag 'ニュース ID' %>

> ニュース id

ソースコード

# File actionview/lib/action_view/helpers/form_tag_helper.rb, line 215
def label_tag(name = nil, content_or_options = nil, options = nil, &block)
  if block_given? && content_or_options.is_a?(Hash)
    options = content_or_options = content_or_options.stringify_keys
  else
    options ||= {}
    options = options.stringify_keys
  end
  options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
  content_tag :label, content_or_options || name.to_s.humanize, options, &block
end

引用先
https://api.rubyonrails.org/

最初にあげたコードではnameしかしてしていないので、content_or_options, options, blockは引数として渡していない。
のでコード読んでいくと、最後の'name.to_s.humanize'が怪しそうだったのでhumanizeについて調べてみます。

humanize

属性名(英語)を調整してくれるメソッド。
具体的には

  • 先頭にアンダースコアがある場合は削除します。

  • 末尾に"_id"がある場合は削除します。

  • アンダースコアがあれば、スペースに置き換えます。

  • 頭文字を除くすべての単語を小文字にします。

  • 最初の単語を大文字にします。

最初の単語の大文字化は、 :capitalize オプションを false に設定することでオフにすることができます(デフォルトは true)。

オプションのパラメータ keep_id_suffix を true (デフォルトは false) に設定することで、末尾の '_id' を保持して大文字化することができます。

humanize('news_number')                  # => "News number"
humanize('news_id')                        # => "News"
humanize('news_id', capitalize: false)     # => "news"
humanize('_id')                              # => "Id"
humanize('news_id', keep_id_suffix: true)  # => "News Id"

今回の場合だと

humanize('ニュース ID')  # => "ニュース id"

頭文字を除くすべての単語を小文字にします。こちらが適応されていてidが小文字になってしまっていたということでした。

最後に

ロジックを知るためにも、Railsのソースコードを読むということが良いとわかっていつつも、二次情報に頼ってしまっていたということもあったので、理解度を上げるためにもちゃんと読まないとダメだなーと感じました。その上で二次情報なども活用することで理解度を高めていけるのかなと勉強になりました。

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

rails AWSのデプロイが反映されない場合

AWS デプロイが反映されない場合に行うこと

環境
rails 5.2.4
unicorn
nginx
capistranoで自動デプロイした場合にすぐにAWSに反映されないことがある方向け

解決法

デプロイする前にローカルで
cap production unicorn:stop
この後に、
bundle exec cap production deploy
をターミナルでうちましょう。unicornがストップします。自動デプロイでも同じようにunicornはストップ
させているはずですが、すぐに反映されないことも多いです。私の環境ではこれですぐにデプロイが反映されています

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

[過去POST][初心者向け]ページネーションの原理がわからない。railsにて

過去POST

過去自分がTECH::CAMPのメンターをしていた時期にメモしていた内容を公開します。
rails でよしなにやってくれるので知らなくてもいいのですが、たまに聞かれることがあったので書きました。
初心者向けのざっくりとした解説です。

Q.ページネーションの原理がわからない。

class TweetsController < ApplicationController

  def index
    @tweets = Tweet.order("created_at DESC").page(params[:page]).per(5)
  end

  def new
  end

  def create
    Tweet.create(tweet_params)
  end

  private
  def tweet_params
    params.permit(:name, :image, :text)
  end

end

ここで

.page(params[:page]).per(5)

とありますが、これが納得できないという人がいました。

これもSQL文の発行に関わるものです。

# page(params[:page]).per(5)
params[:page]= 1

の場合SQLは(順番は一旦無視します。)

SELECT `tweets`.* FROM `tweets` LIMIT 5 OFFSET 0

ページが代わり

# page(params[:page]).per(5)
params[:page]= 2

の場合

SELECT `tweets`.* FROM `tweets` LIMIT 5 OFFSET 5

となります。
ここで重要なのが

LIMIT 5
OFFSET 5

です。

まず
LIMIT 5
は、データをmax5件まで取得するという意味です。

OFFSET 5
は、データを前から5件をスキップするという意味です。

つまり

SELECT `tweets`.* FROM `tweets` LIMIT 5 OFFSET 5

というのは、ツイートテーブルから、ツイートレコードを前から5件飛ばし、
その後最大5件のレコードを取得してくるというものです。

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

[過去POST][初心者向け]N+1問題が理解できない。.includesを使うとどうなるのか(Railsにて)

過去POST

過去自分がTECH::CAMPのメンターをしていた時期にメモしていた内容を公開します。

N+1問題が理解できない。includes(:user)とする意味がわからない。

class TweetsController < ApplicationController

# 中略

  def show
    @tweet = Tweet.find(params[:id])
    @comments = @tweet.comments.includes(:user)
  end

# 中略

end

で使用している。

これはなにも考えなければ
@comments = @tweet.comments.includes(:user)
の部分は
@comments = @tweet.comments
でもできる。

しかしこれではN+1問題が発生してしまう。
(要は、SQLの発行回数が多くなってしまう。)

【問題の概要】

この文の意味は

詳細を表示したツイートについているコメントのレコードを取得
コメントのレコードに紐つくユーザーレコードの取得

をしている

単純に
@tweet.comments
のように書くと、コメントレコードごとにユーザーレコードを取ってくるという動作をしてしまう。
つまりSQL文でかくと、
(tweetは一旦省略、コメント数は4件、ユーザーは2人とする。)

SELECT 'comments'.* FROM 'comments’

を実行すると

SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 1 LIMIT 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1

のようにcommentsレコードの数だけ、userレコードを取得するクエリが発行される。
これがN+1問題。

では.includes(:user)

をつかうとどうなるのか

SELECT 'comments'.* FROM 'comments'
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1, 2)

のように一文で終わる。
これがincludesの意味です。
(:user)user_idですね。

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

[過去POST]TECH::CAMPのメンターをしていた時の回答メモを一部公開します

過去POST

過去自分がTECH::CAMPのメンターをしていた時期にメモしていた内容を公開します。
小分けにしようとしましたが小粒なものはまとめておきます。
記憶が曖昧なので間違っている箇所もあるかもしれないです…

Q.ハッシュの中身が取り出せない

movie = {"title" => "ハリーポッター", "genre" => "ファンタジー", "year" => "2001年"}
から取りたい文字列を取るコードを書きましょう。
のような問題だったと思います。

該当コード

def movie_info(movie, data)
  puts movie[data]
end

movie = {"title" => "ハリーポッター", "genre" => "ファンタジー", "year" => "2001年"}

puts "以下から一つを選んで入力してください。
  ・title
  ・genre
  ・year"

info = gets.chomp

movie_info(movie, info)

解説

ここで
puts movie[:data]
としてしまうとうまくいかないです。
その原因は型が違うからです。

info = gets.chomp
ここでキーを「文字列」として受け取っているので
ハッシュの定義も文字列"title"を使っているのでここでは問題はおこりません。

puts movie[:data]
一方で、こう記述すると、シンボル型で出力してしまい、
型が違うためなにも出力されません。

ちなみに型を調べるのは.classを使うと調べられます。
ex) info.class

もしシンボル型を使いたいなら
movie = {title: => "ハリーポッター”}
のようにハッシュの定義を変えて
info = gets.chomp.to_sym
のように書き換えるとうまくいきます。

puts movie[data.to_sym]
でも大丈夫です。(シンボル型に変換)

Q.devise, no method error

devise関連でno method errorが出た時。current_sign_in_atがないなど。

対処方法

deviseによって作成した、該当のマイグレーションファイルを確認
該当箇所周辺のコメントアウト外して
データベースを作成し直す
$ rake db:migrate:reset
このコマンドで一気にできます。

このコマンドは、データベースを一度ドロップして、
今あるマイグレートファイルを元にデータベースを作り直すというコマンドです。
もちろんデータベースに入っていたデータは消えます。
色々データが入っていて残しておきたい場合、CSVでデータを出力してどっかに保存してからやるべきかと思います。

直前にマイグレートしたものならrollbackで戻っても良いかもしれません。

$ rake db:migrate:status

で現在どのファイルがmigrateされているのか確認できます。

*ちなみに db:resetやっても意味ないです。スキーマファイルから作り直すだけなので。

原因

deviceをインストールした際、デフォルトでコメントアウトされている項目をそのままにしてしまい
使用したいメソットが生成されなかったため。
もう一度マイグレーションをやり直すことで解決するはず。

Q.heroku上で、idが10刻みで付与される

開発環境では問題ないが、本番環境でのみ、変わってしまう。
あまり気になる人はいない気がしますが、これはclearDBのデフォルトの設定です。
herokuでは、dbサーバーとしてcleardbを使っているようです。

【参照】
http://w2.cleardb.net/faqs/#general_16
When I use auto_increment keys (or sequences) in my database, they increment by 10 with varying offsets. Why?
ClearDB uses circular replication to provide master-master MySQL support. As such, certain things such as auto_increment keys (or sequences) must be configured in order for one master not to use the same key as the other, in all cases. We do this by configuring MySQL to skip certain keys, and by enforcing MySQL to use a specific offset for each key used. The reason why we use a value of 10 instead of 2 is for future development.

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

【自分用メモ】ポリモーフィックな関連付けをして、フォームを作成する場合の方法論

ポリモーフィックな関連付けをした場合のフォーム

PlayerモデルとManagerモデルがあったとして、それぞれにTagモデルをポリモーフィック関連付けをしたい。
ポリモーフィック関連を扱ったアプリでフォームを作成したい場合、何がベストなのか考えている。

ActiveRecordのおかげでTagオブジェクトに紐づく親オブジェクトは、@tag.taggableで簡単に取得できる。

ただ、PlayerオブジェクトやManagerオブジェクトに紐づくTagオブジェクトを新規に作成する場合、
どのように対応するのがベストなのか色々と考えている。

ちなみに、ポリモーフィック関連でない「1対多」のアソシエーションは以下のような形になる。

def tag_params
  params.require(:tag).permit(:body).merge(player_id: params[:player_id])
end

ポリモーフィック関連の特有の問題

ここで問題なのが、merge以下の箇所。

  • params[:player_id]の場合もあれば、params[:manager_id]の場合もある
  • ポリモーフィックなので、taggable_type(紐付け先のテーブル名)についての情報も取得する必要がある

そこで、いくつか方法論を調べたり、考えたりしてみた。
例えば、Playerモデルに紐づくTagオブジェクトをcreateする場合で考えてみた。

  • 第1の手段
    • params[:player_id]だけでなく、隠しパラメータとしてparams[:taggable_type]を送ってもらう
    • paramsをTag.newに上書きする
    • データベースを経由しないので、悪手である気がする
  • 第2の手段
    • 同じく隠しパラメータとしてparams[:taggable_type]も送ってもらう
    • paramsをもってデータベースにアクセスする
    • 紐付け先のテーブルのレコードに基づく形で、.tags.buildして新しいTagオブジェクトを生成する
    • 中身に関するparamsで上書きする
  • 第3の手段
    • requestURLを活用する形でデータベースにアクセスする(RESTfulなURL設計にする必要がある)
    • 紐付け先のテーブルのレコードに基づく形で、.tags.buildして新しいTagオブジェクトを生成する
    • 中身に関するparamsで上書きする
  • 第4の手段
    • ルーティングでネストし、コントローラのディレクトリを切る
    • player/tags_controller.rbにて、params[:player_id]をもってデータベースにアクセスする
    • 紐付け先のテーブルのレコードに基づく形で、.tags.buildして新しいTagオブジェクトを生成する
    • 中身に関するparamsで上書きする
  • 第5の手段
    • accepts_nested_attributes_forを活用する
    • 方法についてはまだ調べていない
    • 複数のモデルに関する属性を更新する場合に使うとよさそう(今回は若干過剰?)
    • フォームオブジェクトも検討すべき?(これこそ過剰な感じがする)

第1の手段について

自分なりに考えてみた方法なので、悪手の可能性が高い。
(ちなみに第1と第2の方法を掲載したブログを発見することはできなかった。)

Tagオブジェクトが紐づく先のモデルが異なると、
フォームから送信されるparamsのハッシュのキーが異なってしまう。

つまり、player_id: 2manager_id:3というparamsが送られてくる場合、
strong_paramsをどうするのか問題を解決する必要がある。

そこで、フォームから送られてくるparamsの形式を揃えてしまえばよい。

  • 隠れパラメータとしてtaggable_idtaggable_typeをフォームから送る
    • <%= form.hidden_field :taggable_id, value: player.id %>
    • <%= form.hidden_field :taggable_type, value: player.class %>
    • もちろん、Managerオブジェクトであれば、valueはmanager.idmanager.classになる
  • 隠れパラメータを活用して、Tag.newを更新する
    • データベースを経由していないので、存在しないテーブルに紐づくレコードが保存されてしまうかも
    • ただし、モデルの方での制約はあると思われる(未検証)
tags_controller.rb
def create
  @tag = Tag.new(tag_params)

  # 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end

private

  def tag_params
    params.require(:tag).permit(:body, :taggable_id, :taggable_type)
  end

第2の手段について

発想としては、ほぼ第1の手段と同じである。

第1の手段の場合、データベースを経由せず、Tag.newtaggable_idtaggable_typeといった
paramsを引数として直接指定しているので、データベースを経由するような形にマイナーチェンジする。

tags_controller.rb
def create
  @tag = @taggable.tags.build(tag_params)

  # 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end

private

  def set_taggable
    # constantizeメソッドを使うことで文字を定数化できる
    @taggable = tag_params[:taggable_type].constantize.find(tag_params[:taggable_id])
  end

  def tag_params
    params.require(:tag).permit(:body)
  end  

第3の手段について

こちらについては、いくつかのブログで情報が出てきた。
requestURLのパスを活用する方法である。

tags_controller.rb
def create
  @tag = @taggable.tags.build(tag_params)

  # 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end

private

  def set_taggable
    # `/players/2/tags`の場合、'players'と'2'という要素を取得できる
    resource, id = request.path.split('/')[1,2]
    # `/player/2/tag`の場合、Player.find(2)となる
    # singularizeメソッドにより、'players'が'player'になる
    # classifyメソッドにより、'player'が'Player'になる
    # constantizeメソッドを使うことで文字を定数化できる
    @taggable = resource.singularize.classify.constantize.find(id)
  end

  def tag_params
    params.require(:tag).permit(:body)
  end  

第4の手段について

こちらについては、GoRailsという英語のサイトで紹介されている。
猫Railsさんのブログから見つけることができた。

ルーティングでplayerやmanagerのresourcesの下に、tagsのresourcesをネストさせてしまい、
コントローラのディレクトリを分ける方法である。

  1. tags_controller.rbという親玉コントローラ
  2. 親玉コントローラを継承するplayer/tags_controller.rbという子分コントローラ
  3. その親玉コントローラを継承するmanagers/tags_controller.rbという子分コントローラ

以上3つのコントローラを作ってしまうのであまりDRYではない気もするが、
モデルによって異なるロジックを書きたい場合、こちらの方法を採用する方がよいだろう。

Railsの規約に則っているような気がするので、この方法がベストなのかなという気がする。
(印象論で適当に言っているだけですが)

routes.rb
resources :players do
  resources :tags, module: :players
end
tags_controller.rb
def create
  @tag = @taggable.tags.build(tag_params)

  # 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end

private

  def tag_params
    params.require(:tag).permit(:body)
  end
players/tags_controller.rb
# tags_controller.rb を継承する
class Players::TagsController < TagsController
  before_action :set_taggable

  private

    def set_taggable
      @taggable = Player.find(params[:player_id])
    end
end

第5の手段について

accepts_nested_attributes_forを使う方法もある。
この方法については、いくつかのブログがヒットした。

調べだすと大変なので、詳細についてはここで書かないこととするが、
複数のモデルに関する属性を一度に作成したり更新したい場合、積極的に検討して良いかもしれない。

また、accepts_nested_attributes_forの利用はDHH的にあまり推奨されていない
という話を聞いたことががあるので、フォームオブジェクトを作ってみるのがよいかもしれない。
(今回の場合にそこまでやると、実装がかなり大変になってしまうけど)

注意書き

初学者が書いています。
しかも読みやすさという点でも完成度が低いので、保険をかけてます。

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

【ruby】splitメソッドで、文字列から配列にしたい。逆も然り。

文字列を配列化させて、配列を文字列にして、、、というパターンの時に、どういう書き方が期待する値になってくれるでしょうか。

ということで、ダメなパターンも交えて、整理していきましょう!

文字列を配列にする

splitメソッドを使っていきます。

split 英訳:分断する
つまり、まとまりのあるもの(文字列等)を分断させて、要素とさせるメソッドになります。(配列化)
引数は、切り取る部分の境目を指定するイメージになります

様々なまとまりのパターンから、配列化をしてみました。
させたい配列は、["foo", "bar", "baz"]とします。

さて、どの書き方が合っているでしょうか。

# 配列化                       # 返り値
"foo bar baz".split           ["foo", "bar", "baz"]  # ○
"foo bar baz".split('')       ["f", "o", "o", " ", "b", "a", "r", " ", "b", "a", "z"]
"foo bar baz".split(',')      ["foo bar baz"]

"foobarbaz".split             ["foobarbaz"]
"foobarbaz".split('')         ["f", "o", "o", "b", "a", "r", "b", "a", "z"]
"foobarbaz".split(',')        ["foobarbaz"]
"fooxbarxbaz".split('x')      ["foo", "bar", "baz"]  # ○

"foo, bar, baz".split         ["foo,", "bar,", "baz"]
"foo, bar, baz".split('')     ["f", "o", "o", ",", " ", "b", "a", "r", ",", " ", "b", "a", "z"]
"foo, bar, baz".split(',')    ["foo", " bar", " baz"]

"foo,bar,baz".split           ["foo,bar,baz"]
"foo,bar,baz".split('')       ["f", "o", "o", ",", "b", "a", "r", ",", "b", "a", "z"]
"foo,bar,baz".split(',')      ["foo", "bar", "baz"]  # ○

%w[foo bar baz]               ["foo", "bar", "baz"]  # ○

この中から、期待する配列になる書き方は、4通りということになります。

配列から、文字列にしたい

joinメソッドを使っていきます。
引数に、要素と要素の間に差し込む値を入れていくイメージになります

# 文字列化                           # 返り値
["foo", "bar", "baz"].join        "foobarbaz"
["foo", "bar", "baz"].join('')    "foobarbaz"
["foo", "bar", "baz"].join(',')   "foo, bar, baz"

おまけ 範囲を配列化

# 範囲を配列化                #戻り値
(0..9).to_a                 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
('a'..'z').to_a             ["a",..,"z"]
(1..5).map{ |i| i**2 )      [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのDeviseとセットアップ方法

はじめに

この記事は、インターネット上の百科事典や技術情報サイト、勉強会を基に、RailsにおけるDeviseについて復習しやすいようにまとめた備忘録です。
技術的に誤っている点がございましたら、ご指摘いただけますと幸いです。

Deviseとは?

DeviseとはRailsで作成したアプリケーションに認証機能を実装できるgemの一つです。
Deviseを利用することでログイン機能を簡単に実装することができます。

Deviseの使い方

Gemfileの編集とインストール

Gemfile
gem 'devise'

以下の記事のGetting startedを参考にインストールします。
https://github.com/heartcombo/devise#getting-started

$ bundle install

Gemのインストール完了です。

Deviseのセットアップ

$ rails g devise:install

Deviseの設定ファイルをRailsアプリケーションにインストールするためのコマンドです。

$ rails g devise User

Deviseで認証するためのモデルを作成するコマンドです。
今回はUserモデルを作成すると想定してUserを入力しています。

$ rake db:migrate

作成したモデルを基にデータベースを作成します。
Rails5以降をインストールしている場合は、$ rails db:migrateコマンドを使えます。

$ rails g devise:views

app/views/devise配下にDeviseで使用するビューを作成するコマンドです。

Deviseに機能を追加する

Deviseには10個のモジュールが用意されています。
以下の各モジュールの概要についてはRailsにおけるDeviseとは?から引用させていただいております。

機能 概要
database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードを暗号化してDBに登録します。
registerable 登録処理を通してユーザーをサインアップします。また、ユーザーに自身のアカウントを編集したり削除することを許可します。
recoverable パスワードをリセットし、それを通知します。
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。
trackable サインイン回数や、サインイン時間、IPアドレスを記録します。
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。
confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。
timeoutable 一定時間活動していないアカウントのセッションを破棄します。
omniauthabl intridea/omniauthをサポートします。TwitterやFacebookなどの認証を追加したい場合は追加します。

Deviseが用意しているメソッド

current_user

現在ログインしているユーザーを取得することができるメソッドです。

備考

Deviseの導入方法と追加できる機能についてまとめさせていただきました。
Deviseは趣味で何回か利用していますが、学習内容をまとめておくことで理解度が高まったように感じます。
ここまで拝読していただき、ありがとうございました。

参考文献

DIVE INTO CODE MEDIA
- RailsにおけるDeviseとは?
Github
- heartcombo/devise
Railsについての知識を交換するコミュニティ
- 【オンライン開催】銀座Rails#24@リンクアンドモチベーション

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

Railsで一つの画面に、異なる2モデルのレコードを表示させたい(検索機能付き)

環境

Rails 4.2
mac OS Catalina 10.15.5

やりたいこと

  • 一つの画面に、異なる2モデル(OldHistoryとNewHistory)のレコードを日付順に表示させたい
  • 検索フォームを設け、一度に両方のモデルのレコードに対して検索をかけたい

この2つのモデルは扉を開けた、閉じた、というアクションの履歴を記録していくものになります。
なぜ2つあるかというと、仕様変更により別のモデルからも似たようなアクション履歴が来るようになりましたが、前のモデルとデータが微妙に異なる為、旧モデル(OldHistory)と新モデル(NewHistory)と分かれています。

方法

  1. 検索フォームの条件で各モデルのレコードを絞り込んだオブジェクトの配列を作成、合体させて一つの配列を作りviewに渡す
  2. 検索フォームの条件で各モデルのレコードを絞り込んだオブジェクトの配列を作成、それぞれをViewに渡す

1の方法は、まさに1つの画面に新旧モデルのレコードを混ぜて日付順に表示させることになります。
2の方法は、OldHistoryとNewHistoryでブラウザ上の表示が分かれてしまうので、同じURLでタブでの非同期切り替えを実装すれば一つの画面であると言えそうです。

ページネーション 機能はKaminariを利用しています。

※以下は実際のコードとは名称の一部変更・コードの省略等しています。

1の方法で進める

1の仕様がベストなので、この方針で実装を進めました。
(ただしこの方法は結局うまくいかず、少し改良して2の方法で実装しましたが、振り返りの為に当時の流れを追って書いていきます。)

検索機能はransackは使わずに実装しました。ページネーション機能はKaminariを利用しています。

コントローラーはnewの方を使います。
history_search_paramsで絞り込み条件をもらって、新たに作ったhistory_searchというscopeに渡しています。

コントローラー

new_histories_contoroller.rb
  def index
    @search_params = history_search_params
    old_histories = OldHistory.includes_for_histories.history_search(@search_params)
                                              .order(occured_at: :desc)
    new_histories = NewHistory.includes_for_histories.history_search(@search_params)
                                                  .order(occured_at: :desc)
    old_and_new_histories_array = (@old_histories + @new_histories)
    @old_and_new_histories = Kaminari.paginate_array(old_and_new_histories_array).page(params[:page]).per(30)
  end

  private

  def history_search_params
    params.permit(:occured_at_from, :occured_at_to, :user_name, :item_name_or_room_name, :event)
  end

希望としては

@old_and_new_histories = (old_histories + new_histories).page(params[:new_history_page]).per(30)

といきたいところですが、
(old_histories + new_histories)をするとオブジェクトのクラスがActiveRecord_AssociationRelationではなくArrayクラスになってしまい、pageメソッドが使えなくなってしまったので、paginate_arrayというKaminariのメソッドに引数を渡しています。

モデル

モデルでは新たに作ったhistory_searchというscopeをさらに細かいscopeに分け、パラメータの中身ごとにSQL直書きでLIKE検索で絞り込みを行っています(実際のコードはかなり深いアソシエーションを探索するのでSQL直書きでなんとか実装しました)。
.blank?を各scopeに設けることで絞り込み条件がなかった場合には余計な処理をさせずに次のscopeに移るようにしています。
history_searchというscopeはold_history.rbnew_history.rbの両方に書き、その中身はそれぞれのモデルのカラム名などによって変える必要がありますが、今回は片方のモデルの分だけ下に載せています。

今回scopeに引数を渡していますが、引数を渡すのであればRailsガイドではクラスメソッドを利用することを推奨しています。

スコープで引数を使用するのであれば、クラスメソッドとして定義する方が推奨されます。

ただし注意点があり、クラスメソッドではfalseの場合にnilを返しますが、scopeの場合allメソッドの結果を返します。

ただし1つ注意点があります。それは条件文を評価した結果がfalseになった場合であっても、スコープは常にActiveRecord::Relationオブジェクトを返すという点です。クラスメソッドの場合はnilを返す

  scope :history_search, -> (search_params) do
    return if search_params.blank?
    occured_at_from(search_params[:occured_at_from])
      .occured_at_to(search_params[:occured_at_to])
      .user_name_like(search_params[:name])
      .item_name_or_room_name_like(search_params[:item_name_or_room_name])
      .event_eq(search_params[:event])
  end
  scope :occured_at_from, -> (from) { where('? <= occured_at', from) if from.present? }
  scope :occured_at_to, -> (to) { where('occured_at <= ?', to) if to.present? }
  scope :user_name_like, -> user_name {
    return if user_name.blank?
    user_ids = User.where("name LIKE ?", "%#{user_name}%").pluck(:id)
    sole_box_ids = Solebox.where(user_id: user_ids).pluck(:id)
    where(sole_box_id: sole_box_ids)
  }
  scope :item_name_or_room_name_like, -> (item_name_or_room_name) {
    return if item_name_or_room_name.blank?
    event_history_ids = EventHistoryBackup.where("item_name LIKE ?", "%#{item_name_or_room_name}%").pluck(:event_history_id)
    where(id: event_history_ids)
  }
  scope :event_eq, -> (event) {
    return if event.blank?
    where(event_type: event)
  }

ビュー

Viewでは@old_and_new_historiesという配列をeachで回します。
ただしこの配列はoldとnewの2種類がごちゃ混ぜになっているので、オブジェクトの種類によって表示を変えなければいけません。
例をあげると、
oldの方にはroom_nameというカラムがありますが、newの方にはありません。
逆にnewの方にはitem_nameというカラムがありますが、oldの方にはありません。
occured_atはどちらのモデルにもあります。
全て適切に表示する必要があります。
decoratorを使って以下のように実装しました。

index.rb
<% @old_and_new_histories.each do |old_and_new_history| %>
  <tr>
    <td><%= old_and_new_history.occured_at %></td>
    <td><%= old_and_new_history.user_name %></td>
    <td><%= old_and_new_history.decorate.item_name_or_room_name %></td>
    <td><%= old_and_new_history.decorate.event %></td>
  </tr>
<% end %>
old_history_decorator.rb
  def item_name_or_room_name
    return room.name if room.present?
    '削除済'
  end
new_history_decorator.rb
  def item_name_or_room_name
    return item.name if item.present?
    '削除済'
  end

こうしてdecoratorファイルに同じ名前のメソッドを用意して中身の処理を変えることで、old_and_new_history
old_historyオブジェクトが渡ってきたときはroom.nameを返し、
new_historyオブジェクトが渡ってきたときはitem.nameを返すことが出来ます。

これで一通り実装が出来ました(確か出来てました)。

問題点

開発環境では少ないレコード数で開発していたので気がつかなかったのですが、大量のレコードで動かしてみると検索にかなりの時間がかかりました。
old_and_new_histories_array = (@old_histories + @new_histories)
この部分で全レコードに対するSQL発行をしていることが原因です。
これを解決するには「WHERE」、「SORT」、「LIMIT」などなどを一度のSQLで行わないといけないかな、と思いましたが、実現できませんでした。仮にできたとしても本当に処理が早くなるのかどうか。

2の方法で進める

「検索フォームの条件で各モデルのレコードを絞り込んだオブジェクトの配列を作成、それぞれをViewに渡す」という方法です。
こんなイメージです。
Image from Gyazo

new_histories_contoroller.rb
  def index
    @tab_type = tab_type_params[:tab_type]
    @search_params = history_search_params
    @old_histories = OldHistory.includes_for_histories.history_search(@search_params)
                                              .order(occured_at: :desc).page(params[:old_history_page]).per(30)
    @new_histories = NewHistory.includes_for_histories.history_search(@search_params)
                                                  .order(occured_at: :desc).page(params[:new_history_page]).per(30)
  end

  private

  def history_search_params
    params.permit(:occured_at_from, :occured_at_to, :user_name, :item_name_or_room_name, :event)
  end

  def tab_type_params
    params.permit(:tab_type)
  end

これならSQL実行はすぐに終わります。

モデルにscopeの記述をするのは1の方法と一緒です。

ビューは少し変わります。
以下にhtml、css、jsのコードを載せます。
Viewページは非同期のタブ切り替えで実装します。
oldとnewの表示はrenderでそれぞれ別のファイルを参照するようにします。
それぞれのファイル内に<%= paginate old_histories, param_name: param_name, remote: true %>のようにページネーション部分を書きます。
別のファイルに分けることで、1の方法で行ったdecoratorの工夫(同じ名前のメソッドを用意する)は不要になります。

index.html.erb
<div>
  <%= hidden_field_tag :tab_type, @tab_type %>
</div>

<div class="box-body">
    <div id="js_tabBtn" class="clearfix">
      <div id="old" class="select_btn">
          <strong class="tab-menu_label"><%= t('.old_type') %></strong>
      </div>
      <div id="new" class="select_btn">
          <strong class="tab-menu_label"><%= t('.new_type') %></strong>
      </div>
    </div>

    <div id="tab_old" class="js_content">
      <%= render 'old_histories', param_name: :old_history_page, old_histories: @old_histories %>
    </div>
    <div id="tab_new" class="js_content">
      <%= render 'new_histories', param_name: :new_history_page, new_histories: @new_histories %>
    </div>
  </div>
index.js.erb
$("#tab_old").html("<%= j(render 'old_histories', param_name: :oldt_history_page, old_histories: @old_histories) %>");
$("#tab_new").html("<%= j(render 'new_histories', param_name: :new_history_page, new_histories: @new_histories) %>");
history.scss
#js_tabBtn {
  margin: 5px;
  border-bottom: solid #c4c4c4 1px;
  div {
    float: left;
    list-style-type: none;
    padding: 4px 10px;
    color: #0073BB;
    text-decoration: none;
    display: block;
    text-align: center;
  }
}

#js_tabBtn .active {
  color: black;
  .tab-menu_label {
    border-bottom: solid black 3px;
    padding-bottom: 5px;
  }
}

.js_content{
  display: none;
}
.js_content.active{
  display: block;
}

#js_tabBtn div {
  cursor: pointer;
}

new_history.js
// アクティブでないタブがクリックされたら、そのタブ&対応するコンテンツにactiveクラスを追加し、その兄弟divからactiveクラスを削除する。
$(function () {
  $("#js_tabBtn .select_btn").on("click", function () {
    if ($(this).not("active")) {
      $(this).addClass("active").siblings("div").removeClass("active");
      let index = $("#js_tabBtn .select_btn").index(this);
      $(".js_content")
        .eq(index)
        .addClass("active")
        .siblings("div")
        .removeClass("active");
    }
  });

// oldのタブがクリックされたら#tab_typeの値をoldにする。逆もしかり。
  $("#js_tabBtn #old").on("click", function () {
    $('#tab_type').val('old');
  });
  $("#js_tabBtn #new").on("click", function () {
    $('#tab_type').val('new');
  });

// #tab_typeの値がoldもしくはblankだったらoldの方をactiveに、そうでなければnewの方をactiveにする。
  if ($('#tab_type').val() == "old" || $('#tab_type').val() == "") {
    $('#old').addClass("active");
    $('#tab_old').addClass("active");
  } else {
    $('#new').addClass("active");
    $('#tab_new').addClass("active");
  }
});

検索後に選択したタブを保持する方法として、js-cookieというものを始め導入しましたが、一度ページを離れてから戻ってきてもcookieによって最後に選択したタブの方がactiveになっていました。
このページにアクセスした際はデフォルトでoldのタブを表示させたかったので、デフォルトではoldをactiveにしておき、cookieは使わずに検索ボタンでsubmitした際にhidden_field:tab_typeを持たせて(タブ選択で:tab_typeの値を変える)そのパラメータによって検索後の画面のactiveを決定するようにしました。

最後に

こうやって文章にすると方針転換は発生しながらもスラスラ実装できているように見えますが、実際はめちゃくちゃ悩みながらなんとか実装できました。
以下には気をつけた方がいいかなと思いました。

  • 開発環境でもなるべく本番と同じデータを使った方が良い
  • オブジェクトのクラスを意識する
  • SQLの発行を意識する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで「いいね!」機能を作る - ①アソシエーションに別名をつける

背景

最近人にプログラミングを教える機会があるのですが、使っているテキストに、初学者さんにはまあまあ難しそうだなー。と思える記述がありました。

自信を持って教えられるか不安なところもあったので、勉強がてら、噛み砕いてまとめてみようと思います。

データ構造

基本のデータ構造は以下の通り。
Twitterみたいなアプリで、ユーザー(users)はたくさんの投稿(posts)を持っている。そして、自分の投稿も含む全ての投稿に「いいね!」(likes)を付けられる。という感じです。

Image from Gyazo

シンプルな構造なのですが、後半、ちょっとひねりが必要でした。

アソシエーションを書き起こしてみる

まずは、Railsの規則通りにシンプルに書けるところからアソシエーションを書き起こしてみます。

models/user.rb
class User < ApplicationRecord
  has_many :posts
  has_many :likes
end
models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes
end
models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
end

とてもシンプルですね:slight_smile:
この時、例えばcontrollerで以下のように書くと、

controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @posts = User.find(params[:user_id]).posts
  end
end

「そのユーザーの投稿一覧」を取得できます。(今回は例としてユーザー詳細ページで一覧表示する形にしています)

問題点

この時、例えば「そのユーザーが『いいね!』した投稿一覧」を表示したいと思ったときには、少し工夫が必要です。

おそらく、下記@favorite_postsのように記載すれば「そのユーザーが『いいね!』した投稿一覧」は取得できるように予想しますが、

controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @posts = User.find(params[:user_id]).posts
    @favorite_posts = User.find(params[:user_id]).likes.posts
    # ↑こちらはあくまでも予想のコード、実際に動作しません。
  end
end

Userモデルで、has_many :postsは既に使われていて、

models/user.rb(再掲)
class User < ApplicationRecord
  has_many :posts
  has_many :likes
  # has_many :posts, through: :likes とは書けない。
end

has_many :posts, through: :likesと書くと、名前が重複してしまうからです。
ちなみに、through:を使わないで、has_many :postsから「いいね!」した投稿を取得する方法もあるにはあるのですが、

難しい上に、データの読み出しにコストがかかるので、お勧めはしません。

アソシエーションに別名をつけて解決

そんな時は、userlikespostsという関係の流れに別名をつけて解決します。
つまり、下記の図の赤枠で囲ったアソシエーションに別名を付ければいいのです。

er.png

別名の付け方は下記の通りです。

models/user.rb
class User < ApplicationRecord
  has_many :posts
  has_many :likes
  has_many :favorites, through: :likes, source: :post
end

上記のコードではuserlikespostsというアソシエーションに、favoritesという別名をつけています。

ただし、そのままだとrailsはfavoritesというテーブルを探してしまうので、source: :postsというオプションをつけてpostsテーブルを参照するようにします。

上記のように書くことで、user.favoritesと記載すると、「ユーザーが『いいね!』した投稿」を取得することができます。(下記は、こんな感じで使えますよーというサンプルです:slight_smile:

controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    @posts = User.find(params[:user_id]).posts
    @favorite_posts = User.find(params[:user_id]).favorites
  end
end

早い人は、プログラミング初心者から数週間でこの内容を理解して卒業していくんですね。すごいなー。

私も自信持って教えられるように、次は「いいね!」をする、「やめる」のコードについても研究したいと思います。

追記:逆の関係(投稿に「いいね!」したユーザーを取得)

なお、投稿に「いいね!」したユーザーは次のような記述でアソシエーションを結ぶことができます。

models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  has_many :likes
  has_many :users, through: :likes
end

先ほどつけた別名とは関係なく、ユーザー(user)投稿(post)の関係は、
userが自分の作った投稿をもつpost belongs_to userの関係と、
postが自分に「いいね!」をしたユーザーを持つpost had_many users through likesという二つの関係があるので、それそのまま記載すればOKです^^

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

Rails Tutorial 第12章 完了

2020/8/22 1.5時間

12.1.2まで進めました。

2020/8/23 2.0時間

12.3.2まで進めました。

2020/8/24 0.5時間

リスト12.18の途中まで進めました。

2020/8/25 0.5時間

2020/8/26 0.5時間

リストの入力間違いでエラーになっていたのをデバッグしてやっと動きました。テキストから手打ちしていると、入力ミスで結構バグが出ることが分かります。

2020/8/27 0.5時間

2020/8/28 1.0時間

2020/8/29 4.0時間

うち1.5時間はSendGridのエラー調査です。

2020/8/30 0.5時間

Sendgridはあいからずエラーですが、先に進めることにしました。

12章を完了です。
所要時間は11.0時間です。

digestは作るたびに違う値になる

tokenが同じ値ならdigestは何度作っても同じなのかと思ったのですが、2回実行してみたところ違う値になっていました。

>> a.password_digest
=> "$2a$10$92Y.9S6JdU9HutSoZu6TdOSDFHsodZ5PJKZfzBxmflqIYNwuUA6Su"

>> a.password_digest
=> "$2a$10$yZ9mPXl/uWJF1czSLso9yejl7A4ALE9HvRWJOuozwLldjb98vT1yO"

SendGridのAddonの追加でエラーになった件は解消

8/19にSendGridのAddonの追加でエラーになった件は、8/29になぜか解消していました。
前回リンクを貼ったネットの他の方と同じです。

理由は以下のどちらかだと推測します。
1.時間が立ったことによりバグ?のようなエラーが修正された
2.第12章まで進めることでエラーを回避できる

ubuntu:~/environment/sample_app (master) $ heroku addons:create sendgrid:starter
Creating sendgrid:starter on ⬢ fathomless-mesa-1xxxx... free
Created sendgrid-crystalline-46xxx as SENDGRID_PASSWORD, SENDGRID_USERNAME
Use heroku addons:docs sendgrid to view documentation

ubuntu:~/environment/sample_app (master) $ heroku config:get SENDGRID_USERNAME
app173xxxx@heroku.com
ubuntu:~/environment/sample_app (master) $ heroku config:get SENDGRID_PASSWORD
uxxxxxxx78xxx

SendGridで違うエラー Authentication failed:

メールを送るところで、違うエラーになりました。

.. Completed 500 Internal Server Error in 606ms (ActiveRecord: 42.2ms)
..ArgumentError (SMTP-AUTH requested but missing secret phrase):
..app/models/user.rb:59:in send_activation_email'
..app/controllers/users_controller.rb:23:in `create'

入力間違いに気が付き修正しました。
誤:user_password
正:password

config/environments/production.rb(誤)
 ActionMailer::Base.smtp_settings = {
    :address          => 'smtp.sendgrid.net',
    :port             => '587',
    :authentication   => :plain,
    :user_name        => ENV['SENDGRID_USERNAME'],
    :user_password    => ENV['SENDGRID_PASSWORD'],
    :domain           => 'heroku.com',
    :enable_starttls_auto => true

再実行したところ、違うエラーになりました。

..Net::SMTPAuthenticationError (535 Authentication failed: account disabled

ネットで他の方を調べたところ、SendGridでアカウントを一時凍結された方がいたので、時間をおいて試してみることにしました。

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

ActiveRecord::NotNullViolation in Deviseエラー

【概要】

1.結論

2.ActiveRecord::NotNullViolationtとは何か

3.なぜActiveRecord::NotNullViolation in Deviseになるのか

4.どのように解決するか

5.補足

1.結論

db/migrate/""""devise_create""""".rbに
記載してある"t.string password(またはpassword_confirmation)"の記載を削除する!


2.ActiveRecord::NotNullViolationtとは何か

スクリーンショット 2020-08-29 20.47.37.png

この意味は、「DBの規則上、空欄(NULL)にしてはいけない項目がデータで保存されかけましたよ!そんなことしちゃいけませんよ!」という機械からのご指摘です!


3.なぜActiveRecord::NotNullViolation in Deviseになるのか

2.と掛け合わせると、「デバイスでDBの規則上、空欄(NULL)にしてはいけない項目がデータで保存されかけましたよ!コントローラーで保存して登録されたときだよ!」と行っています。

なぜこのようなことになるかというと、gem'devise'がわざわざpasswordを作ってくれたのに、
被せて作ろうとしているのでどっちのpasswordがいいかわからずNULLになったと思われます!

gem'devise'というものはpasswordとそれに対するpassword_confirmationを作ってくれるgemです!

なので

db/migrate/""""_devise_create_""""".rb
t.string :password null:false"(password_confirmation)

をプログラムしてカラムを作成する必要はないんです!


4.どのように解決するか

db/migrate/""""_devise_create_""""".rb
t.string password,  null:false"
(またはpassword_confirmation)

と記載されていると思うので削除しましょう!

5.補足

ちなみにgem'devise'にはpasswordに対してもう一つ付け加えれれている機能があります!

それはバリデーションです!

6文字以上にしなければ、入力ができないように制限が既に機能として備わっているので、

model/user
validates :password, length { minimum: 5 }

を記載する必要がなくなります!

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

【Rails】NoMethodError: undefined method `new' for BigDecimal:Classの対処法

はじめに

NoMethodError: undefined method 'new' for BigDecimal:Classの詳しい対処法が日本語であまり載ってなさそうなので、備忘録として残しておきます。

対処法

結論として
Gemfilebigdecimalを下記内容に修正したら、うまく行きました。

gem 'bigdecimal', '1.3.5'

gemの内容を書き換えたのでその内容を反映させるために、bundle installを行なってください。

$ bundle install

意味

バージョン 特徴 サポートされているRubyのバージョン範囲
2.0.0 BigDecimal.newとサブクラス化の使用ができない 2.4 ..
1.4.x BigDecimal.newとサブクラス化を使用の際、警告がいつも表示される 2.3 .. 2.6
1.3.5 警告なしでBigDecimal.newとサブクラス化を使用できる .. 2.5

参考
https://stackoverflow.com/questions/60226893/rails-nomethoderror-undefined-method-new-for-bigdecimalclass
https://github.com/ruby/bigdecimal#which-version-should-you-select

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

【Rails環境構築】Docker + Rails + MySQL (初心者も30分でOK!)

Dockerを使った環境構築

「環境構築はコードを書くよりも難しい」と聞いたことがあります。

Dockerは簡単!

何故かと言うと、、、
コピペするだけでいいから
ただし、理解するまで少し時間がかかるかも、、、
まずは簡単な概要から

Docker

仮想環境を構築するための道具

コンテナ

仮想環境そのもの
(DockerはDockerエンジンの上にコンテナが動く)

イメージ

Dockerコンテナを実行するために必要なもの。

環境構築の手順

①.プロジェクト(ディレクトリ)を作成し、移動する

$ mkdir アプリ名
$ cd アプリ名

②.Dockerfile、docker-compose.yml、Gemfile、Gemfile.lockを作成する

$ touch Dockerfile docker-compose.yml Gemfile Gemfile.lock

③.エディタを開き、Dockerfile、docker-compose.yml、Gemfileを下記からコピペする

Dockerfile

Dockerのイメージを自動で生成してくれるファイル

FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

RUN mkdir /myapp
WORKDIR /myapp

COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock

RUN bundle install
COPY . /myapp

FROM : 使用するイメージとコマンド
RUN : コマンドの実行
WORKDIR : 作業ディレクトリの設定
COPY : コピー元(ホスト側)とコピー先(仮想環境側)のファイルまたはディレクトリを指定

Gemfile

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

docker-compose.yml

version: '3'

services:
    db:
        image: mysql:5.7
        environment:
            MYSQL_USER: root
            MYSQL_ROOT_PASSWORD: password
        ports:
            - "3306:3306"
        volumes:
            - ./db/mysql/volumes:/var/lib/mysql

    web:
        build: .
        command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
        volumes:
            - .:/myapp
            - gem_data:/usr/local/bundle
        ports:
            - 3000:3000
        depends_on:
            - db 
        tty: true
        stdin_open: true

 volumes:
 gem_data:

version : docker-composeのバージョン。
depends_on : 依存関係を示していて起動順を制御できる。ここでは「db→web」へ起動する。

Docker composeとは

Docker composeとは、複数のコンテナから成るサービスを構築・実行する手順を自動化し、管理を容易にする機能。Docker composeでは、composeファイルを用意してコマンドを一回実行することで、そのファイルから設定を読み込んですべてのコンテナを起動することができる。

serviceについて

Docker composeでは、アプリケーションを動かすための各要素をserviceと呼んでいる。通常はweb(rails)とdb(mysql)と名付ける。

rm -f tmp/pids/server.pidについて

pidとはプロセスIDのことである。pidは、開発用Webサーバーを起動するときにtmp/pids/server.pidに書き込まれ、終了するときに削除される。server.pidにpidが書かれているとサーバーが起動中と判断されてしまう。

portsについて

ports:
  - 3000:3000

上記は、コンテナ内のポート3000番をホストの3000番にマッピングするという意味。これにより、コンテナ内のWebサーバーへhttp://localhost:3000でアクセスできるようになる。

volumeについて

volumes:
   - ./db/mysql/volumes:/var/lib/mysql

は、ホストの./db/mysql/volumesをコンテナ内の/var/lib/mysqlにマウントするという意味。簡単にいうと、Dockerのコンテナと、ローカルを同期している。

volumes :
   - .:/myapp

.がホストのディレクトリ全てを意味し、それをコンテナ内のmyappにマウントしている。

④.下記コマンドを実行する

$ docker-compose run web rails new .--force --database=mysql --skip-bundle

runコマンドではimageの構築から、コンテナの構築・起動までしてくれる。
引数にサービスを指定する必要あり。
docker-compose.ymlのvolumes:- .:/myappの部分でdockerとローカルのディレクトリを同期するように設定しているため、このコマンドを実行後、同時にローカルにも同様のファイルが生成される。

⑤.config/database.yml内の(password: password、host: db)に変更する

default: &default
   adapter: mysql2
   encoding: utf8
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
   username: root
   password: password   #passwordを追記しました
   host: db         #localhostからdbに変更しました

⑥.下記コマンドを実行する

$ docker-compose build

Gemfileが更新されたときbuildを実行する

⑦.下記コマンドを実行する

$ docker-compose up -d

docker-compose.yml通りにコンテナが起動する。
docker-compose.ymlの変更を反映させる際にも実行。
-dオプションはバックグラウンド起動。

⑧.下記コマンドを実行する

$ docker-compose run web rails db:create

データベースを作成する。
docker-compose run webでローカルからコマンドを実行できる。(コンテナに入る必要はない)

⑨.下記コマンドを実行し、2つのコンテナが立ち上がっているか確認する。(完成!)

$ docker ps

現在起動しているコンテナを表示するコマンド。
localhost:3000にアクセスするといつものYay!You're on Rails!が表示される。

 コンテナを削除したい場合は下記を実行する

$ docker-compose down

docker psでコンテナが削除されているか確認する。
またコンテナを生成・起動する場合はdocker-compose up -dを実行する。

まとめ

かなり簡単だったんじゃないかなと思います!
これでアプリ開発していきましょう!:thumbsup:

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

クラスの情報を継承したクラスの作り方

内容

複数のクラスに同じメソッドを定義したい場合、それぞれのクラス全てにそのメソッドを定義すると、同じ記述を何度も繰り返す事になってしまいます。
(例1)
クラス1  クラス2   クラス3
メソッドA メソッドA  メソッドA
メソッドB メソッドC  メソッドD
複数のクラスが同じメソッドを持つときにクラスが増えれば増えるほどコード量が多くなり、管理も難しくなります。これを防ぐためにクラスの継承について学んでいきましょう。

クラスの継承とは

あるクラスに定義されたメソッドを、別の新規クラスで利用できるようにした上でクラスを定義することを継承と言います。

クラスの継承には、親クラスと子クラスの関係があります。元となるクラスを親クラス、親クラスのメソッドを引き継ぎ新しく作成するクラスを子クラスと呼びます。
image.png
パトカーやトラックの上位概念である「車」のクラスを作り、そこへ共通の特徴を定義しておきます。車の特徴(親クラス)を継承することで、パトカー(子クラス)とトラック(子クラス)に車の特徴を記述する必要がなくなります。結果、パトカーとトラックだけの特徴を書くだけで済む上、それぞれの特徴が分かりやすくなっています。

クラスの継承をする際には、クラスを宣言する際に「<」を用いて、以下のように記述します。

class 子クラス名 < 親クラス名
end

それでは、先ほどの車の例を実際にクラスの継承を用いたコードで書いてみます。
まずは親となるクラスを定義します。

Carクラス(親クラス)

class Car
  def speed_up
    puts "加速します"
  end

  def speed_down
    puts "減速します"
  end

  def horn
    puts "プップー"
  end
end

親クラスには、車が持つ共通の動作を定義しています。
次に子クラスをそれぞれ定義します。今回はパトカーとトラックをそれぞれPatrolCar, TruckCarとして定義します。

PatrolCarクラス(子クラス)

class PatrolCar < Car  # クラスの継承
  def siren
    puts "ピーポーピーポー"
  end
end

TruckCarクラス(子クラス)

class TruckCar < Car  # クラスの継承
  def carry
    puts "荷物を載せます運びます"
  end
end

親クラスに共通のメソッドを定義することで、子クラスのコード量が少なくなり見やすくなっています。また、これは継承を使う大きなメリットですが、親クラスで定義された共通のメソッドを変更するだけで、変更を子クラスへ容易に反映できることが上記のコードから分かります。

最後に

クラスの継承を使うことによって共通のメソッドを繰り返し書くことなくスッキリとしたコードを書けるので皆さん書いてみてください!

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