20191115のRailsに関する記事は27件です。

Windows環境でVSCodeでのRuby on Railsの開発環境構築手順

※ Rubyの推奨環境はLinux?とはいえWindowsしか持ってないよ!って人向けです

1.Rubyのインストール

Rubyで開発する上で、最も必要なものはRubyのインストールなので
https://rubyinstaller.org/downloads/
からRuby + Devkitをダウンロードする

ダウンロードしたらexeファイルを実行してインストールをする

インストールしたら、

ruby -v

コマンドを実行、バージョンが表示されたならばOK
バージョンが表示されなければ、Rubyをインストールしたパスの下の/binフォルダにパスを通す

2.Git bashのインストール

https://gitforwindows.org/
からgitをダウンロードし、インストールする

3.VSCodeのインストール、環境整備

https://code.visualstudio.com/
より、VisualStudioCodeをダウンロードし
exeファイルを実行してインストールする

EXTENSIONSを選択し、Rubyを選択してこれもインストールする

rubydevenv.png

その後、VSCode左下の歯車をクリック

検索欄に、「terminal.integrated.shell.windows」と入力して
表示される「Edit in setting.json」をクリックする

rubydevenv2.png

    "terminal.integrated.shell.windows": "C:\\Program Files\\Git\\bin\\bash.exe"

と入力する

すると、VSCode下部のターミナルがGit Bashに変更される

ターミナルから、rails コマンドが実行できるので統合開発環境として活用可能に
rubydevenv4.png

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

やさしい図解で学ぶ 中間テーブル DB データベース joinモデル 概念【初級者用】

中間テーブルって?

中間テーブルとは
"join table もしくは junction table"とも呼ばれ、2つのテーブル(エンティティ)を結びつける、結合テーブルを指します。

・・・・・・?

とりあえず、下のER図を見てみましょう。

テーブルとテーブルの間にそれぞれを繋げるテーブルがあります。これが"中間テーブル"です。
(ER図の表記一覧はこちら)

スクリーンショット 2019-11-14 16.38.09.png

2つのテーブル、この場合coursesテーブルとusersテーブルの間に

" 多対多 "

の関係性が生まれた時、マルチカラムアトリビュートを避けるため、この中間テーブルを用いると言われています。

・・・・・・・・・?

現時点でなにをいってるかわからなくても大丈夫。
これから詳しく図解を使って紹介しますのでちょっとずつイメージしていきましょう。

1.まずはイメージ。

例えば、
下の画像のようなプログラミング学習サイトを製作しているとします。
このサイトではユーザーは学習したいプログラミングコースをカスタマイズして選ぶことができるとします。

1.png

ここでまずは2つのテーブル

①usersテーブル (ユーザー情報用のテーブル)

②coursesテーブル (学べる言語情報用のテーブル)

があるとします。

1userは複数のコースを持っている →→→ "対多"

1courseは複数のユーザーを持っている →→→ "対多"

スクリーンショット 2019-11-15 18.12.48.png

ということはusersテーブルcoursesテーブル

" 多対多 "

という関係が成り立ちます。

ちょっとまだ多対多の意味がよくわからないという方もいると思いますが、次の章で実際にテーブルをみてみましょう。

2."多対多"とは

(とりあえずユーザー3人と言語コース3つで表示しています。)

スクリーンショット 2019-11-15 17.16.30.png

1 userは複数の言語コースとつながり、
一方、
1 courseは複数のユーザーに繋がっています。

つまり、

多対多とは

対するテーブルのレコードを複数お互いに持っているということです。

そして、この関係性では図のカラムの他に外部キー用のカラムが必要です。
では外部キーを持たせたテーブルをみてみましょう。

3. 実際にテーブルをみてみよう

ちなみに1カラムの中にデータは1つずつしか入れられないので下の図のようなことはできません。

スクリーンショット 2019-11-15 18.41.10.png

★coursesテーブルの中身

coursesテーブルにはとりあえず5人分のuser_idカラムを持たせています。
(カラムのuser_id1~5というのはこの言語コースがもつ1~5人目のユーザーという意味です。)

スクリーンショット 2019-11-15 23.00.02.png

★usersテーブルの中身

usersテーブルには8コース分のcourse_idを持ちます。
(カラムのcourse_id1~8というのはユーザーがもつ1~8つ目の言語コースという意味です。)

スクリーンショット 2019-11-15 21.43.19.png

どこがいけないのかわからない場合はnilのカラムにとりあえず注目してみてください。

なんだかボコボコ、ガタガタで、使われていない空のカラムが多数ありますよね。

このような不要なカラムが多数、発生するようなテーブルは

Multi Column Attribute(マルチカラムアトリビュート)

とよばれ、DB設計においてアンチパターンであるとされています。

では、どうしたらいいのか。そんな時用いるのが

中間テーブルです。

4.中間テーブルの構造

では、実際に中間テーブルがどんな構造になっているのかもう一度最初のER図をみてみましょう。

スクリーンショット 2019-11-14 16.38.09.png

よく見ると

中間テーブル course_usersテーブル
それぞれのテーブルの外部キー
user_id
course_id
を持っています。

ちなみに中間テーブルの命名は関係性をみて自分で作ることもできます。言語によっても多少、可否が異なるかもしれませんが、

(基本的にはアルファベット順で〇〇〇__〇〇〇sテーブルとするようです。なので今回の場合は、course_usersテーブルとしています。)

2." 多対多 "で用いた図を使って中間テーブルを作成してみましょう。
スクリーンショット 2019-11-15 17.16.30.png

↓下のような構造になっています。

スクリーンショット 2019-11-15 22.11.25.png

例えばid:2のユーザーが「HTMLとCSSも学びたい!」となってもuser_idとcourse_idを図のように足していけば、nilカラムを生み出すことなく追加することができますよね。

要するに今後、userの数もしくがcourseの数が増えたとしても
この中間テーブルがあれば、nilを出すことなく、レコードを追加することができます。

以上

今回はコードについて触れると長くなってしまうので概念のみです。
後日、Railsの中間テーブル作成方法は記事にする....かもしれません。

もちろん中間テーブルを使わずに設計をしていく方法もあり、様々ですが、DB設計をしていて、
「あれ? もしかしてこのテーブルって中間テーブルとかいうやつかな?」と思ったらこの記事で概念を見直してみてはいかがでしょうか。またなにかいい例があれば修正、更新いたします。

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

やさしい図解で学ぶ 中間テーブル 多対多 DB データベース joinモデル 概念 【初級者用】

中間テーブルって?

"join table もしくは junction table"とも呼ばれ、2つのテーブル(エンティティ)を結びつける、結合テーブルを指します。

とりあえず、下のER図を見てみましょう。

テーブルとテーブルの間にそれぞれを繋げるテーブルがあります。
これが"中間テーブル"です。(ER図の見方はこちら)

スクリーンショット 2019-11-14 16.38.09.png

2つのテーブル、この場合coursesテーブルとusersテーブルの間に

" 多対多 "

の関係性が生まれた時、マルチカラムアトリビュートを避けるため、1つの解決方法としてこの中間テーブルを用います。

現時点でなにをいってるかわからなくても大丈夫。
これから詳しく図解を使って紹介しますのでちょっとずつイメージしていきましょう。

1.まずはイメージ。

例えば、
下の画像のようなプログラミング学習サイトを製作しているとします。
このサイトではユーザーは学習したいプログラミングコースをカスタマイズして選ぶことができるとします。

1.png

ここでまずは2つのテーブル

①usersテーブル (ユーザー情報用のテーブル)

②coursesテーブル (学べる言語情報用のテーブル)

があるとします。
簡単な図にすると以下のような関係性がイメージできます。

1userは複数のコースを持っている →→→ "対多"

スクリーンショット 2019-11-16 11.12.40.png

1courseは複数のユーザーを持っている →→→ "対多"

スクリーンショット 2019-11-16 11.11.16.png

ということはusersテーブルcoursesテーブル

" 多対多 "

という関係が成り立ちます。

ちょっとまだ多対多の意味がよくわからないという方もいると思いますが、次の章で実際にテーブルをみてみましょう。

2."多対多"とは

(とりあえずユーザー3人と言語コース3つで表示しています。)

スクリーンショット 2019-11-15 17.16.30.png

1 userは複数の言語コースとつながり、
一方、
1 courseは複数のユーザーに繋がっています。

つまり、

多対多とは

対するテーブルのレコードを複数お互いに持っているということです。

そして、この関係性では図のカラムの他に外部キー用のカラムが必要です。
では外部キーを持たせたテーブルをみてみましょう。
スクリーンショット 2019-11-15 18.12.48.png

多対多の関係性の一例ですが、こんな感じになるはずです。
ではもう少し詳しくみてみましょう。

3. 実際にテーブルをみてみよう

ちなみに前提として1カラムの中にデータは1つずつしか入れられません。
つまり、下の図のように1カラムがuser_idを複数もつことはできません。

スクリーンショット 2019-11-15 18.41.10.png

★coursesテーブルの中身

coursesテーブルにはとりあえず5人分のuser_idカラムを持たせています。
(カラムのuser_id1~5というのはこの言語コースがもつ1~5人目のユーザーという意味です。)

スクリーンショット 2019-11-15 23.00.02.png

★usersテーブルの中身

usersテーブルには8コース分のcourse_idを持ちます。
(カラムのcourse_id1~8というのはユーザーがもつ1~8つ目の言語コースという意味です。)

スクリーンショット 2019-11-15 21.43.19.png

どこがいけないのかわからない場合はnil(からっぽ)のカラムにとりあえず注目してみてください。

なんだかボコボコ、ガタガタで、使われていないからっぽのカラムが多数ありますよね。

このような不要なカラムが多数、発生するようなテーブルは

Multi Column Attribute(マルチカラムアトリビュート)

とよばれ、DB設計においてアンチパターンであるとされています。

では、どうしたらいいのか。そんな時用いるのが

中間テーブルです。

4.中間テーブルの構造

では、実際に中間テーブルがどんな構造になっているのかもう一度最初のER図をみてみましょう。

スクリーンショット 2019-11-14 16.38.09.png

よく見ると
中間テーブル course_usersテーブル
それぞれ接続先のテーブルの外部キー
user_id
course_id
を持っています。

ちなみに中間テーブルの命名は関係性をみて自分で作ることもできます。言語によっても多少、可否が異なるかもしれませんが、

(基本的にはアルファベット順で〇〇〇__〇〇〇sテーブルとするようです。なので今回の場合は、course_usersテーブルとしています。)*

[2.]で用いた図を再び使って中間テーブルを作成してみましょう。
スクリーンショット 2019-11-15 17.16.30.png

この図の関係性から中間テーブルは
↓下のような構造になります。

スクリーンショット 2019-11-15 22.11.25.png

例えば、
id:2のユーザーが「HTMLとCSSも学びたい!」となってもuser_idとcourse_idを図のように足すことができます。

要するに今後、userの数もしくがcourseの数が増えたとしても
この中間テーブルがあれば、nil(空)を出すことなく、レコードを追加することができます。

まとめ

中間テーブルの特徴まとめ

・多対多の関係性があるテーブルの間に設置
・接続する2テーブルの外部キーをもつ
・nilカラムを出さないDB設計をしたいとき
・命名は〇〇〇__〇〇〇s (基本的にはアルファベット順)もしくは新しく命名

状況によっては中間テーブルをつくったほうがいいケースばかりではありませんが、中間テーブルや多対多の概念について、もやもやしている方はそれがちょっとでもクリアになってもらえたなら嬉しいです。

今回はコードについて触れると長くなってしまうので概念のみです。
またなにかいい例があれば修正、更新いたします。

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

やさしい図解で学ぶ 中間テーブル 多対多 DB データベース joinモデル 概念編

中間テーブルって?

"join table もしくは junction table"とも呼ばれ、2つのテーブル(エンティティ)を結びつける、結合テーブルを指します。

↓ER図のテーブルとテーブルの間にそれぞれを繋げるテーブルがあります。
これが"中間テーブル"です。(ER図の見方はこちら)

スクリーンショット 2019-11-14 16.38.09.png

2つのテーブルの間に

" 多対多 "

という関係性が生まれた時、ある問題点を解決する1つの方法としてこの中間テーブルを配置します。

これから詳しく図解を使って紹介しますのでちょっとずつイメージしていきましょう。

1.まずは多対多のイメージ

例えば、
下の画像のようなプログラミング学習サイトを製作しているとします。
このサイトではユーザーは学習したいプログラミングコースをカスタマイズして選ぶことができるとします。

1.png

ここでまずは2つのテーブル

①usersテーブル (ユーザー情報用のテーブル)
②coursesテーブル (学べる言語情報用のテーブル)

があるとします。
各テーブルのレコードを基点に簡単な図にすると以下のようなテーブル間の関係性がイメージできます。

①usersテーブル

1userは複数のコースを持っている →→→ "対多"

スクリーンショット 2019-11-16 11.12.40.png

②coursesテーブル

1courseは複数のユーザーを持っている →→→ "対多"

スクリーンショット 2019-11-16 11.11.16.png

ということはusersテーブルcoursesテーブル

対多 + 対多 == " 多対多 "

という関係が成り立ちます。

まだよくわからないという方もいると思いますが、実際にテーブルの形で例をみてみましょう。

2. 実際に多対多のテーブルをみてみよう

とりあえずユーザー3人と言語コース3つで表示しています。
それぞれのテーブルのレコードどちらからも複数のつながりが見えます

スクリーンショット 2019-11-15 17.16.30.png

つまり、

" 多対多 "とは対するテーブルのレコードを複数お互いに持っているということです。

そして、この関係性では下図のように外部キー用のカラムが必要なので
外部キーを持たせたテーブルをみてみましょう。
スクリーンショット 2019-11-15 18.12.48.png

多対多の関係性の一例ですが、こんな感じになるはずです。
ではもう少し詳しくみてみましょう。

3. さらに詳しくみてみよう

*注意:ちなみに前提として1カラムの中にデータは1つずつしか入れられません。
つまり、下の図のように1カラムがuser_idを複数もつことはできません。

スクリーンショット 2019-11-15 18.41.10.png

★coursesテーブルの中身

coursesテーブルにはとりあえず5人分のuser_idカラムを持たせています。
(カラムのuser_id1~5というのはこの言語コースがもつ1~5人目のユーザーという意味です。)

スクリーンショット 2019-11-15 23.00.02.png

★usersテーブルの中身

usersテーブルには8コース分のcourse_idを持ちます。
(カラムのcourse_id1~8というのはユーザーがもつ1~8つ目の言語コースという意味です。)

スクリーンショット 2019-11-15 21.43.19.png

どこがいけないのかわからない場合はnil(からっぽ)のカラムにとりあえず注目してみてください。

なんだかボコボコ、ガタガタで、使われていないからっぽのカラムが多数ありますよね。

このような不要なカラムが多数、発生するようなテーブルは

Multi Column Attribute(マルチカラムアトリビュート)

とよばれ、DB設計においてアンチパターンであるとされています。

では、どうしたらいいのか。そんな時用いるのが

中間テーブルです。

4.中間テーブル

では、実際に中間テーブルがどんな構造になっているのかもう一度最初のER図をみてみましょう。

スクリーンショット 2019-11-14 16.38.09.png

よく見ると
中間テーブル course_usersテーブル
それぞれ接続先のテーブルの外部キー
user_id
course_id
を持っています。

[2.]で用いた図を再び使って中間テーブルを作成してみましょう。
スクリーンショット 2019-11-15 17.16.30.png

この2つのテーブルの間にの中間テーブルを設置すると下のような構造になります。

中間テーブル: course_usersテーブル

スクリーンショット 2019-11-15 22.11.25.png

例えば、
id:2のユーザーが「HTMLとCSSも学びたい!」となってもuser_idとcourse_idを図のように足すことができます。

要するに今後、userの数もしくがcourseの数が増えたとしても
この中間テーブルがあれば、nil(空)を出すことなく、レコードを追加することができます。

まとめ

中間テーブルの特徴まとめ

・多対多の関係性があるテーブルの間に設置
・接続する2テーブルの外部キーをもつ
・nilカラムを出さないDB設計をしたいとき
・命名は〇〇〇__〇〇〇s (基本的にはアルファベット順)もしくは新しく命名

状況によっては中間テーブルをつくったほうがいいケースばかりではありませんが、中間テーブルや多対多の概念について、もやもやしている方はそれがちょっとでもクリアになってもらえたなら嬉しいです。

またなにかいい例があれば修正、更新いたします。

補足

ちなみに中間テーブルの命名は関係性をみて自分で作ることもできます。言語によっても多少、可否が異なるかもしれませんが、

(基本的にはアルファベット順で〇〇〇__〇〇〇sテーブルとするようです。なので今回の場合は、course_usersテーブルとしています。)*

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

gem 'devise'に苦戦 プログラミング 37日目

gem'devise'使う上で考慮すべきこと

1.URLかぶり

例えばdeviseをuserモデルに紐づけてinstallしたりすると以下のような(一部)routesが自動的にできます。

URLかぶりとは??
userのアカウント情報を表示させるusers/showを作ろうと、get "users/:id" => "users#show"をroutesの設定しました。そうするとそれまでできていたlogoutがうまくいかなくなりました。その原因として考えた結論が、users/:idとdeviseで設定した他のusers/~~~が被ってしまっているということです。(あっているかは定か)

usersはやめpages/:idにすることで解決しました。

                   new_user_session GET    /users/sign_in(.:format)                                                                 devise/sessions#new
                         user_session POST   /users/sign_in(.:format)                                                                 devise/sessions#create
                 destroy_user_session DELETE /users/sign_out(.:format)                                                                devise/sessions#destroy
                    new_user_password GET    /users/password/new(.:format)                                                            devise/passwords#new
                   edit_user_password GET    /users/password/edit(.:format)                                                           devise/passwords#edit
                        user_password PATCH  /users/password(.:format)                                                                devise/passwords#update
                                      PUT    /users/password(.:format)                                                                devise/passwords#update
                                      POST   /users/password(.:format)                                                                devise/passwords#create
             cancel_user_registration GET    /users/cancel(.:format)                                                                  devise/registrations#cancel
                new_user_registration GET    /users/sign_up(.:format)                                                                 devise/registrations#new
               edit_user_registration GET    /users/edit(.:format)                                                                    devise/registrations#edit
                    user_registration PATCH  /users(.:format)                                                                         devise/registrations#update
                                      PUT    /users(.:format)                                                                         devise/registrations#update
                                      DELETE /users(.:format)                                                                         devise/registrations#destroy
                                      POST   /users(.:format)                                                                         devise/registrations#create
                                 root GET    /   

2.いちいち編集ページなどは作らず既存のやつを使う方がいい

そしてviewも手順どうりに進めるとdeviseフォルダができます。deviseのフォルダにはメールアドレス情報やパスワードを変更するページもあるためいちいち編集ページを作る必要はありません。ただ名前を登録するところがないのが不便です。

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

Model名とController名が異なっていてもデータ移動はできるらしい 学習37日目

〇〇モデルのデータは□□コントローラー上でも使える

UserモデルのデータはUsersコントローラーで,Pageモデルのデータはpagesコントローラーでという大きな思い込みをしていた。(冷静に考えれば、application.html.erbはいろんなモデルからデータを取ってきている)

勘違いしていたこと

1.モデルとコントローラーのデータのやり取りは、同じ名前同士で行う。←そんなことない!

UserモデルのデータはUsersコントローラーで,Pageモデルのデータはpagesコントローラーでという大きな思い込みをしていた。(冷静に考えれば、application.html.erbはいろんなモデルからデータを取ってきている)

2.controllerの内容を継承したければコピペする←それだけではない!

確かにuserscontrollerの内容をそのままpagescontrollerでも使いたければ、コピペも1つの手段になる。しかしめんどくさいのでclassを継承をすれば良い(pagescontroller<userscontroller)

下記の例はpagesと名付けたshowのviewである。ここには@userを用いている。そしてその下はpagesコントローラーである。

show.html.erb(pages)
<div class="container">
<div class="profile-card">
  <div class="profile-card__inner">
  <div class="profile-thumb">
    <img src="<%="/user_images/#{@user.image_name}" %>" class="user-image" alt="アイコン">  

    </div>
    <div class="profile-content">
    <span class="profile-name"><%=@user.name%></span>
    <span class="profile-job"><%=@user.email %></span>
    <span class="profile-intro"><%=@user.intro %></span>
    <%=link_to("編集する",edit_user_registration_path, method: :get) %>
    </div>
    </div>

</div>
</div>
class PagesController < UsersController
def index
    @users = User.all
end

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


def update 
    @user = User.find_by(id: params[:id])
    @user.name = params[:name]
    @user.intro = params[:intro]

    if params[:image]
        @user.image_name = "#{@user.id}.jpg"
        image = params[:image]
        File.binwrite("public/user_images/#{@user.image_name}", image.read)
      end

      if @user.save
        flash[:notice] = "ユーザー情報を編集しました"
        redirect_to("/pages/#{@user.id}")
      else
        render("users/edit")
      end


end

def edit
    @user = User.find_by(id: params[:id])
  end

end

↑↑Pagescontroller<Userscontrollerしてるからコピペしなくていい(しちゃってる悪い例)
@user = User.allは別にpagescontrollerでも使える

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

【AWS EC2】bundler -v -bash: bundler: コマンドが見つかりません

rbenvうっかり入れ忘れなので、下記のコマンドを入力すればOK。
バージョンはご自身の環境に合わせてください。

$ rbenv install 2.5.1
$ rbenv global 2.5.1
$ rbenv rehash  #rehashを行う
$ ruby -v # バージョンを確認

上記のコマンドでダメだった場合は、下記の記事を試してみてください。

【rails】PCからrvenvだけを間違ってアンインストールしてしまって鼻血が出た話
https://qiita.com/kaorioka09jm/items/77f3b64027d8ed05bb21

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #14 ユーザ投稿表示, ページネーション編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#13 パスワード再設定編
次回:準備中

今回の流れ

  1. 投稿用のモデルをつくる
  2. サンプル投稿を表示する
  3. テストをつくる

Tutorial13章(第4版)に突入しました。
今回はユーザ投稿の表示とページネーションを実装します。
(投稿機能は#15(準備中)で紹介します。)

投稿用のモデルをつくる

ここでの手順は以下の通りです。

  • Micropostモデルをつくる
  • Userモデルを編集する
  • バリデーションを追加する
  • Micropostモデルを改良する
  • エラー時の日本語化を行う

Micropostモデルをつくる

まずは投稿のためのMicropostモデルをつくりましょう。
ユーザが投稿できる項目は以下の通りです。

  • 動画視聴時間
  • メモ
  • 画像

加えて投稿はユーザがいて初めて成立するので、Userモデルに所属させます。
そのためには生成時にuser:referencesを加えます。
以上を踏まえてMicropostモデルを生成しましょう。

bash
$ rails g model Micropost time:integer memo:text picture:string user:references

投稿を表示する際、twitterのように最新のものが上部に来てほしいものです。
その準備として、マイグレーションにインデックスを加えます。

db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[5.2]
  def change
    create_table :microposts do |t|
      t.integer :time
      t.text :memo
      t.string :picture
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

user_idは投稿したユーザ、create_atは投稿時間を管理しています。
これらを複合キーとすることで、望み通り取り出すことが可能です。

Userモデルを編集する

MicropostモデルはUserモデルに所属しました。
この実装はbelongs_to :userにより、user_idとして形になりました。
今度はUserモデルにMicropostモデルを所有してもらいましょう。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  # 中略
end

has_manyによりUserはMicropostと1対多の関係になりました。
こうすることでmicropostsを指定する時、こんな書き方が可能です。

user = User.new
user.microposts

慣習的にも正しいので、以上の作業は忘れずに行いましょう。

バリデーションを追加する

ここでは以下のバリデーションを追加します。

  • user_idが空の場合、Micropostを生成しない
  • いずれの値も空の場合のみ(user_idを除く)Micropostを生成しない
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :memo, length: { maximum: 255 }
  validates :only_user_id, presence: true

  private
    def only_user_id
      time.presence or memo.presence or picture.presence
    end
end

いずれかという条件を実装するためにonly_user_idメソッドを定義しました。

バリデーションで『いずれか』を実装する詳しい方法↓
「いずれかのカラムが空でなければ良い」というバリデーション

Micropostモデルを改良する

ここでは以下の機能を追加します。

  • 新しい投稿を先にソートする
  • ユーザが削除されたら投稿も削除する
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope { order(created_at: :desc) }
  # 中略
end
app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  # 中略
end

エラー時の日本語化を行う

あとはエラー時の言語を日本語にしましょう。
日本語化にはgemと設定が必要です。#6Railsのバリデーションエラーのメッセージの日本語化を参考に設定を行なってください。

お済みの方は以下のようにファイルを編集してください。

config/locales/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザ
      micropost: 投稿
    attributes:
      user:
        name: 名前
        email: メールアドレス
        password: パスワード
        password_confirmation: パスワード(再入力)
      micropost:
        time: 記録時間
        memo: メモ
        picture: 画像
        user_id: ユーザID
        only_user_id: 入力欄

後ほどビューをつくります。
その際エラー表示を確認するので、ひとまずここで終了です。
最後にデータベースを更新します。

bash
$ rails db:migrate

サンプル投稿を表示する

Micropostモデルは整いました。次は投稿を表示しましょう。
ここでの手順は以下の通りです。

  • サンプル投稿を生成する
  • サンプル投稿を表示する
  • ページネーションを実装する

サンプル投稿を生成する

サンプル投稿を生成するにはfakerを使用します。

gemfile
+ gem 'faker'
bash
$ bundle install

それではサンプル投稿を生成します。
ついでにサンプルユーザも生成しておきます。

db/seeds.rb
User.create!(
  name: "Example User",
  email: "example@railstutorial.org",
  password: "foobar",
  password_confirmation: "foobar",
  activated: true
)

5.times do |n|
  name = Faker::Name.name
  email = "example_#{n+1}@railstutorial.org"
  password = "password"
  User.create!(
    name: name,
    email: email,
    password: password,
    password_confirmation: password,
    activated: true
  )
end

users = User.order(:created_at).take(3)
50.times do
  memo = Faker::Lorem.sentence(6)
  users.each { |user| user.microposts.create!(memo: memo) }
end

最後にデータベースを再構築しましょう。

bash
$ rails db:migrate:reset
$ rails db:seed

これでサンプルユーザとサンプル投稿が生成されました。

サンプル投稿を表示する

生成を終えたら投稿を表示しましょう。
ここでの手順は以下の通りです。

  • usersコントローラを編集する
  • ビューを編集する
  • ページネーションを追加する

usersコントローラを編集する

投稿を表示させるビューはshow.html.erbです。
つまりこれに対応するコントローラにMicropostを渡す必要があります。
対応するのはusersコントローラです。編集しましょう。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.page(params[:page]).per(10)
  end

この1文はページネーションを実装する記述です。

@microposts = @user.microposts.page(params[:page]).per(10)

ページネーションしたい対象に.page〜以下を加えることで動作します。
per(10)は1ページの表示数を表します。今回は10にしました。

ビューを編集する

続いてはビューです。
パーシャルを使用しつつ実装しましょう。

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="container show-container">
  <div class="row">
    <div class="col bg-primary">
      form
    </div>
    <div class="col bg-secondary">
      figure
    </div>
  </div>
  <div class="row">
    <div class="col">
      <%= render 'layouts/log' %>
    </div>
  </div>
</div>
app/views/layouts/_log.html.erb
<h1 class="log-title col-2">Logs</h1>
<% if @user.microposts.any? %>
  <div class="container">
    <ol class="microposts">
    <% @microposts.each do |micropost| %>
        <li id ="micropost-<%= micropost.id %>">
          <span class="row log-list">
            <span class="col-2 log-timestamp d-none d-md-inline-block log-timestamp-block">
              <span class="log-timestamp"><%= time_ago_in_words(micropost.created_at) %></span>
            </span>
            <span class="col-md-10 col-log-memos">
              <div class="log-time-and-edit">
                <div class="row">
                  <span class="log-time col-3"><%= micropost.time %></span>
                  <span class="col-7 log-timestamp log-timestamp-inline"><%= time_ago_in_words(micropost.created_at) %></span>
                  <span class="log-edit col-2"><%= link_to image_tag('edit.png', class: "log-edit-image"), '#' %></span>
                </div>
              </div>
              <span class="log-memo"><%= micropost.memo %></span>
              <span class="log-picture"><%= micropost.picture %></span>
            </span>
          </span>
        </li>
    <% end %>
    </ol>
  </div>
  <%= paginate @microposts %>
<% else %>
  <span>まだ投稿がありません</span>
<% end %>
app/assets/stylesheets/application.scss.erb
// max-width = 767px
@media (max-width: 767px) {
  .nav-item-extend {
    margin-top: 0.6rem;
  }

  .log-title {
    padding: 0;
  }

  .log-list {
    border-left: 1px solid $lantern-dark-white;
  }
}

@media (min-width: 768px) {
  .log-timestamp-inline {
    visibility: hidden;
  }
}

// 中略

// logs
ol {
  padding: 0;
  list-style: none;
}

.log-title {
  margin: 1.5rem 0;
  text-align: center;
}

.log-list {
  margin-bottom: 1rem;

  .log-timestamp {
    color: gray;
  }

  .log-timestamp-block {
    text-align: center;
    padding-bottom: 2rem;
    margin-top: 0.1rem;
    border-right: 1px solid $lantern-dark-white;
  }

  .col-log-memos {
    padding: 0 1rem 0 1rem;

    .log-time-and-edit {
      .log-time {
        font-size: 1.2rem;
        color: $lantern-yellow;
      }

      .log-timestamp-inline {
        text-align: right;    
        margin-top: 0.1rem;
        padding: 0;
      }

      .log-edit {
        text-align: right;
        margin-top: 0.1rem;

        .log-edit-image {
          width: 1rem;
        }
      }
    }

    .log-memo {
    }

    .log-picture {
    }
  }
}

// paginate
.pagination {
  margin-top: 1.6rem;

  .page-item {
    .page-link {
      border: 1px solid $lantern-light-white;
      background-color: $lantern-light-white;
      color: $lantern-blue;
    }
  }

  .page-item.active .page-link {
    background-color: $lantern-blue;
    border-color: $lantern-blue;
    color: $lantern-light-white;
  }
}
// 中略

_log.html.erbのこの部分もページネーションを実装するための記述です。

<%= paginate @microposts %>

コントローラに引き続き、2つの記述が見受けられました。
これらはkaminariというgemにより動作します。
準備は以上です。後はgemを挿れるだけです。

ページネーションを追加する

それではkaminariを追加し、ページネーションを動作させましょう。
Bootstrap4と日本語化の適用も行います。

gemfile
+ gem 'kaminari'
+ gem 'kaminari-bootstrap'
bash
$ bundle install
$ rails g kaminari:views bootstrap4
config/locales/models/ja.yml
ja:
  # 中略
  views:
    pagination:
      first: "&laquo; 最初"
      last: "最後 &raquo;"
      previous: "&lsaquo; 前"
      next: " &rsaquo;"
      truncate: "..."

これらを終えるとビューはこんな感じになります。
PC版↓
lantern_logs_pc.png
スマホ版↓
lantern_logs_iphoneX.png

参考になります↓
【Ruby on Rails】gem(Kaminari)でページネーション機能を追加してBootstrapを適用する。
【Rails初心者】ページネーションを実装して自分好みにデザインを変える

テストをつくる

最後にテストを完成させます。
ここでの手順は以下の通りです。

  • FactoryBotを整える
  • テストを書く

FactoryBotを整える

まずはテストを行う前の準備をしましょう。
今回新たにMicropostモデルが生成されました。
それに伴うテストを行いたいので、FactoryBotで導入しましょう。

ここでの手順は以下の通りです。

  • テスト用のMicropostモデルを生成する
  • テスト用のUserモデルを編集する
spec/factories/microposts.rb
FactoryBot.define do
  factory :memos, class: Micropost do
    trait :memo_1 do
      time { 240 }
      memo { "I just ate an orange!" }
      picture { nil }
      user_id { 1 }
      created_at { 10.minutes.ago }
    end

    trait :memo_2 do
      time { 180 }
      memo { "Check out the @tauday site by @mhartl: http://tauday.com" }
      picture { nil }
      user_id { 1 }
      created_at { 3.years.ago }
    end

    trait :memo_3 do
      time { 59 }
      memo { "Sad cats are sad: http://youtu.be/PKffm2uI4dk" }
      picture { nil }
      user_id { 1 }
      created_at { 2.hours.ago }
    end

    trait :memo_4 do
      time { 207 }
      memo { "Writing a short test" }
      picture { nil }
      user_id { 1 }
      created_at { Time.zone.now }
    end
    association :user, factory: :user
  end
end
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    sequence(:email) { |n| "michael_#{n}@example.com" }
    password { "password" }
    password_confirmation { "password" }
    activated { true }
  end

  factory :other_user, class: User do
    name { "Sterling Archer" }
    sequence(:email) { |n| "duchess_#{n}@example.gov" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { true }
  end

  factory :no_activation_user, class: User do
    name { "No Activation" }
    sequence(:email) { |n| "no_#{n}@activation.co.jp" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { false }
  end
end

microposts.rbではtraitを使って、Micropostモデル内を区切りました。
users.rbではsequenceを使って、メールアドレスの一意性を保つよう番号をつけました。
以上で準備は完了です。

※ 変更により他のテストが失敗する可能性があるので、適宜変更を加えてください。

Micropostモデルのテスト

それではいよいよテストに入りましょう。
このテストでは以下を確認します。

  • モデルが正しく生成されているか
  • いずれの値も空の場合(user_idを除く)、Micropostは存在しないか
  • カラムが最新のものから順に並んでいるか
  • user_idが存在しないMicropostは存在しないか
  • memoが255文字を超えないか
spec/models/micropost_spec.rb
require 'rails_helper'

RSpec.describe Micropost, type: :model do

  let(:user) { create(:user) }
  let(:micropost) { user.microposts.build(time: 240, memo: "Lorem ipsum", user_id: user.id) }

  describe "Micropost" do
    it "should be valid" do
      expect(micropost).to be_valid
    end

    it "should not be valid" do
      micropost.update_attributes(time: 1, memo: "  ", picture: nil, user_id: user.id)
      expect(micropost).to be_valid
      micropost.update_attributes(time: nil, memo: "  ", picture: nil, user_id: user.id)
      expect(micropost).to be_invalid
    end

    it "should be most recent first" do
      create(:memos, :memo_1, created_at: 10.minutes.ago)
      create(:memos, :memo_2, created_at: 3.years.ago)
      create(:memos, :memo_3, created_at: 2.hours.ago)
      memo_4 = create(:memos, :memo_4, created_at: Time.zone.now)
      expect(Micropost.first).to eq memo_4
    end
  end

  describe "user_id" do
    it "should not be present" do
      micropost.user_id = nil
      expect(micropost).to be_invalid
    end
  end

  describe "memo" do
    it "should not be at most 255 characters" do
      micropost.memo = "a" * 255
      expect(micropost).to be_valid
      micropost.memo = "a" * 256
      expect(micropost).to be_invalid
    end
  end
end

Userモデルのテスト

このテストでは以下を確認します。

  • ユーザが削除されたら投稿も削除されるか
spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # 中略
  it "destroys assosiated microposts" do
    user.microposts.create!(memo: "Lorem Ipsum")
    expect{ user.destroy }.to change{ Micropost.count }.by(-1)
  end

以上でテストは終了です。
次回は投稿機能を実装します。

前回:#13 パスワード再設定編
次回:準備中

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

メモ:minitest-reporters導入方法

書くことが無いので、rails testの結果が見やすくなるgem、「minitest-reporters」の導入方法をメモしておこうと思う。

導入方法

Gemfileの「group :test do」の中に以下のように導入。

group :test do
  gem 'minitest'
  gem 'minitest-reporters'
end

加えて、test_helper.rbに以下の二行を追加。

test_helper.rb
require 'rails/test_help'
require "minitest/reporters" #1行目
Minitest::Reporters.use!     #2行目

gem 'minitest'にどういう役割があるのか、調べてもイマイチよくわからなかったが...
どうやらRailsのテストの方法はいくつかあるようで、minitestというのはそのうちの一つらしい。
今のところはminitest-reportersの前提modならぬ前提gem的な存在だと思っておくことにする(適当)。

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

#Rails + #rspec OPTIONS request test by ActionDispatch::Integration::Session.proccess

example

  describe 'options' do
    subject { process :options, '/' }

    before { subject }

    it { ... } 
  end

Ref

Perform HTTP OPTIONS request from Rails 5 for CORS pre-flight or otherwise - Stack Overflow

ActionDispatch::Integration::Session

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2718

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

eager_load + find_by でクエリが2回発行される件の対策

結論

よくある1:NのRelationがある時

class User < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :user
end
User.eager_load(:comments).find_by(id: 1)

とかすると
1. User.find_by(id: 1) のSQL
2. User.eager_load(:comments) のSQL
の2回SQLが発行される

User.eager_load(:comments).where(id: 1).load.first

とかするとクエリが1回になる。

(.load.to_a とかでもOK)

ruby 2.5.1
rails 5.0.6/6.0.1(下記ログは6.0.1のものです)

で確認済み。

詳細

普通にやってみたりしたけど、どうしてもただのfindっぽいクエリと、
まとめて情報をとってくるクエリの2回が発行された

User.eager_load(:comments).find(1)
# SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 LIMIT 1
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1

User.eager_load(:comments).find_by(id: 1)
# SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 LIMIT 1
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1

User.eager_load(:comments).where(id: 1).first
# SELECT DISTINCT `users`.`id` AS alias_0, `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1 ORDER BY `users`.`id` ASC

WHERE `users`.`id` = 1 AND `users`.`id` = 1 とかめっちゃ冗長そうなクエリがある。
2回目のクエリだけでほしい情報は取れるはず
なんとかして1回のクエリにしたい :thinking:

User.eager_load(:comments).where(id: 1) の場合は、クエリが1回なので、
これの時点でクエリを無理やり発火させてから、firstを取ればいいのでは?

User.eager_load(:comments).where(id: 1).load.first
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1

キタ━━━━(゚∀゚)━━━━!!

おわり。

クエリ整形ver
User.eager_load(:comments).find(1)
# SQL (4.0ms)  SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 LIMIT 1
# SQL (2.8ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1
# -- pretty format.
# SELECT
#     DISTINCT `users`.`id`
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
# LIMIT
#     1;
#
# SELECT
#     `users`.`id` AS t0_r0,
#     `users`.`name` AS t0_r1,
#     `users`.`created_at` AS t0_r2,
#     `users`.`updated_at` AS t0_r3,
#     `comments`.`id` AS t1_r0,
#     `comments`.`user_id` AS t1_r1,
#     `comments`.`created_at` AS t1_r2,
#     `comments`.`updated_at` AS t1_r3
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
#     AND `users`.`id` = 1;

#=> #<User id: 1, name: "hogehoge", created_at: "2019-11-15 04:54:42", updated_at: "2019-11-15 04:54:42">

User.eager_load(:comments).find_by(id: 1)
#  SQL (1.0ms)  SELECT DISTINCT `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 LIMIT 1
#  SQL (2.7ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1
# SELECT
#     DISTINCT `users`.`id`
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
# LIMIT
#     1;
#
# SELECT
#     `users`.`id` AS t0_r0,
#     `users`.`name` AS t0_r1,
#     `users`.`created_at` AS t0_r2,
#     `users`.`updated_at` AS t0_r3,
#     `comments`.`id` AS t1_r0,
#     `comments`.`user_id` AS t1_r1,
#     `comments`.`created_at` AS t1_r2,
#     `comments`.`updated_at` AS t1_r3
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
#     AND `users`.`id` = 1;
#
# SELECT
#     `users`.`id` AS t0_r0,
#     `users`.`name` AS t0_r1,
#     `users`.`created_at` AS t0_r2,
#     `users`.`updated_at` AS t0_r3,
#     `comments`.`id` AS t1_r0,
#     `comments`.`user_id` AS t1_r1,
#     `comments`.`created_at` AS t1_r2,
#     `comments`.`updated_at` AS t1_r3
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
#     AND `users`.`id` = 1;
#=> #<User id: 1, name: "hogehoge", created_at: "2019-11-15 04:54:42", updated_at: "2019-11-15 04:54:42">

User.eager_load(:comments).where(id: 1).first
#  SQL (0.6ms)  SELECT DISTINCT `users`.`id` AS alias_0, `users`.`id` FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
#  SQL (0.6ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 AND `users`.`id` = 1 ORDER BY `users`.`id` ASC
#
# SELECT
#     DISTINCT `users`.`id` AS alias_0,
#     `users`.`id`
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
# ORDER BY
#     `users`.`id` ASC
# LIMIT
#     1;
#
# SELECT
#     `users`.`id` AS t0_r0,
#     `users`.`name` AS t0_r1,
#     `users`.`created_at` AS t0_r2,
#     `users`.`updated_at` AS t0_r3,
#     `comments`.`id` AS t1_r0,
#     `comments`.`user_id` AS t1_r1,
#     `comments`.`created_at` AS t1_r2,
#     `comments`.`updated_at` AS t1_r3
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1
#     AND `users`.`id` = 1
# ORDER BY
#     `users`.`id` ASC;
#=> #<User id: 1, name: "hogehoge", created_at: "2019-11-15 04:54:42", updated_at: "2019-11-15 04:54:42">

User.eager_load(:comments).where(id: 1).load.first
#  SQL (1.0ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `comments`.`id` AS t1_r0, `comments`.`user_id` AS t1_r1, `comments`.`created_at` AS t1_r2, `comments`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
# SELECT
#     `users`.`id` AS t0_r0,
#     `users`.`name` AS t0_r1,
#     `users`.`created_at` AS t0_r2,
#     `users`.`updated_at` AS t0_r3,
#     `comments`.`id` AS t1_r0,
#     `comments`.`user_id` AS t1_r1,
#     `comments`.`created_at` AS t1_r2,
#     `comments`.`updated_at` AS t1_r3
# FROM
#     `users`
#     LEFT OUTER JOIN `comments` ON `comments`.`user_id` = `users`.`id`
# WHERE
#     `users`.`id` = 1;
#=> #<User id: 1, name: "hogehoge", created_at: "2019-11-15 04:54:42", updated_at: "2019-11-15 04:54:42">

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

AWS Elastic Beanstalkで定期処理実行(cron)

Heroku Schedulerでできたことを、Elastic Beanstalkでも行いたい

運用しているRailsアプリの本番環境を、HerokuからElastic Beanstalkに移行させました。
その際タスクの定期処理を行う方法で少し苦労したので、メモ的にシェアしておきます。

以前はHeroku Schedulerでrakeコマンドを呼び出せましたが、EBでそれを行うにはEC2内でcronを使うか、ワーカー環境でPOSTメソッドを呼び出す方法があるみたいです。今回は安く済ませたいので、前者を選びました。
後者の方法はこちらが参考になります。
https://qiita.com/tomoeine/items/38a9b2123e3afa1d5cd0

方法

このファイルを.ebextensionsに置くだけでOKです。あとはeb deployすれば、自動でcronのファイルを作成し自動実行してくれます。
eb sshで/etc/cron.dを確認してみてください。

.ebextensions/cron-sample.config
files: # filesは、EC2上にファイルを作成する。パスはアプリのルートではなくLinuxのルート。
    "/etc/cron.d/hello_world":
        mode: "000644"
        owner: root
        group: root
        content: |
            0 4 * * * root /usr/local/bin/hello_world.sh
       # 指定した頻度で、↓で作成するシェルスクリプトを実行。これは毎日午前4時

    "/usr/local/bin/hello_world.sh":
        mode: "000755"
        owner: root
        group: root
        content: |
            #!/bin/bash

            . /opt/elasticbeanstalk/support/envvars # アプリの環境変数読み込み
            cd /var/app/current

            /opt/rubies/ruby-2.5.7/bin/bundle exec /opt/rubies/ruby-2.5.7/bin/rake hello_world >> /var/log/hello_world.log 2>&1
            # rakeコマンド打つ。例ではhello_worldというrakeメソッドをすでに定義しているとする。
            # ログも出力。

            exit 0

commands:
    remove_old_cron:
        command: "rm -f /etc/cron.d/hello_world.bak" # バックアップを削除

参考

Linux サーバーでのソフトウェアのカスタマイズ
https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/customize-containers-ec2.html#linux-files

Ruby on Railsの環境構築をElastic Beanstalkで行う
https://qiita.com/yuyasat/items/4d93b4ad4f86a6e13d50#cron%E3%81%A7rails-runner%E3%82%84rake%E3%82%BF%E3%82%B9%E3%82%AF%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99

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

またお前か mysql2 gemのインストール失敗

問題

これと関連がある記事を今まで2回作成しましたが、また新たな環境でmysql2 gemのインストールに失敗しました。
既存の方法ではないやり方で解決したので、記事を作成しました。

Installing mysql2 0.5.2 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /Users/jhpark/works/practice/js/react_with_rails/vendor/bundle/ruby/2.6.0/gems/mysql2-0.5.2/ext/mysql2
/usr/local/var/rbenv/versions/2.6.5/bin/ruby -I /usr/local/var/rbenv/versions/2.6.5/lib/ruby/2.6.0 -r ./siteconf20191115-56779-10cp3mw.rb extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_wait_for_single_fd()... yes
-----
Using mysql_config at /usr/local/bin/mysql_config
-----
checking for mysql.h... yes
checking for errmsg.h... yes
checking for SSL_MODE_DISABLED in mysql.h... yes
checking for SSL_MODE_PREFERRED in mysql.h... yes
checking for SSL_MODE_REQUIRED in mysql.h... yes
checking for SSL_MODE_VERIFY_CA in mysql.h... yes
checking for SSL_MODE_VERIFY_IDENTITY in mysql.h... yes
checking for MYSQL.net.vio in mysql.h... yes
checking for MYSQL.net.pvio in mysql.h... no
checking for MYSQL_ENABLE_CLEARTEXT_PLUGIN in mysql.h... yes
checking for SERVER_QUERY_NO_GOOD_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_NO_INDEX_USED in mysql.h... yes
checking for SERVER_QUERY_WAS_SLOW in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_ON in mysql.h... yes
checking for MYSQL_OPTION_MULTI_STATEMENTS_OFF in mysql.h... yes
checking for my_bool in mysql.h... no
-----
Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load
-----
-----
Setting libpath to /usr/local/Cellar/mysql/8.0.18_1/lib
-----
creating Makefile

current directory: /Users/jhpark/works/practice/js/react_with_rails/vendor/bundle/ruby/2.6.0/gems/mysql2-0.5.2/ext/mysql2
make "DESTDIR=" clean

current directory: /Users/jhpark/works/practice/js/react_with_rails/vendor/bundle/ruby/2.6.0/gems/mysql2-0.5.2/ext/mysql2
make "DESTDIR="
compiling client.c
compiling infile.c
compiling mysql2_ext.c
compiling result.c
compiling statement.c
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in /Users/jhpark/works/practice/js/react_with_rails/vendor/bundle/ruby/2.6.0/gems/mysql2-0.5.2 for inspection.
Results logged to /Users/jhpark/works/practice/js/react_with_rails/vendor/bundle/ruby/2.6.0/extensions/x86_64-darwin-18/2.6.0/mysql2-0.5.2/gem_make.out

An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  mysql2

肝心のところはここ。

ld: library not found for -lssl

mysql2のコンパイルに必要なopensslのライブラリーを探せないということです。

brewでopensslをインストールしようとしたんですが、なぜかインストールできませんでした。
代わりにopenssl@1.1をインストールしましたが、問題は解決できず。

解決

.zshrcファイルでLIBRARY_PATHがopensslのままだったので、openssl@1.1で修正したら無事にコンパイルされてインストールできました。

export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl@1.1/lib/

この世界は色々と変化が早いですね。
opensslも後ろにバージョンが付いたformulaが出たんですから、また他のバージョンが付いたformulaも出ると思います。

要はmysql2 gemはopensslが必要でopensslのlibrary pathをシェルの設定ファイルに書かなければならない。
opensslはバージョンアップによってlibrary pathが変更される可能性があるため、その都度正しいパスで修正しなければならない。

以上。

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

link_to をブロックで囲って each で回す方法(link_to do ~ end)

user/index.html.rb

<% @users.each |user| do %>
  <%= link_to user_path(user) do %>
      <%= @user.name %>
  <% end %>
<% end %>

上記のように、link_to do〜end で囲えば良い。

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

[メモ]2019/11/15 Ruby on Rails

Railsでページを表示するのには以下の3つのファイルが必要

ページの作成に必要なもの
・ビュー(View)
・コントローラ(Controller)
・ルーティング(routing)

  1. ビュー(View)
     ビュー(View)とはページの見た目を作るHTMLファイル。
     ブラウザとのやり取りの中で,Railsからビューが返されページが表示される。

  2. コントローラ(Controller)
    ページを表示するときに、Railsではコントローラを経由してビューをブラウザに返す。
    「rails g controller home top」を実行したとき、「home_controller.rb」というコントローラファイルが生成され、ファイルの中に「topメソッド」が追加される。
    コントローラーのメソッドを「アクション」と呼ぶ。

メソッドとは、「オブジェクト(もの)の操作を定義したもの」
例)オブジェクト:マリオとすると
メソッド 操作
・ジャンプする。
・走る
・しゃがむetc...
3. ルーティング(routing)
ブラウザとコントローラを繋ぐ。

ページが表示されるまでに、
ルーティング→コントローラ→ビューという順番で処理が行われる。

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

railsの復習

Railsでの処理のポイント

  • ルーティング
  • コントローラー
  • モデル
  • ビュー

モデル、ビュー、コントローラーをまとめて
MVCという。

HTTPメソッド

  • GET ページを表示する操作
  • POST データを登録する操作
  • PUT データを変更する操作
  • DELETE データを削除する操作

コントローラ アクション

  • index 一覧表示リクエストアクション
  • new 新規投稿リクエストアクション
  • create データ投稿リクエストアクション
  • show 個別詳細表示リクエストアクション
  • edit 投稿編集表示リクエストアクション
  • update データ編集リクエストアクション
  • destroy データ削除リクエストアクション

部分テンプレート

renderメソッドを使う。
renderメソッドのpartialオプションでファイルの指定を行う。
部分テンプレートのファイル名の先頭は「_(アンダーバー)」で始める。

★以下、省略表示が鬼。順々に教えてもらいたいところ。部分テンプレートの理解が浅いうちから最終省略形見せられたら大混乱間違いなしだわ・・・。でも改めて見ると、やりたいことをササッとやってくれ、って考えると最終形の方が直感的に分かるか、って気がしないでもないが、経緯を理解した上で使ってかないと、部分テンプレートがどれなのか分からなくなりそう。

<% @users.each do |user| %>
  <%= render partial: "user", locals: { user: user } %>
<% end %>
<%= render partial: "user", collections:  "@users" %>
<%= render @users%>

フォーム入力データの取得

form-tagに入力されたデータは、params.permit(フォームの名前定義)で取得。
form-withに入力されたデータは、params.require(モデル名).permit(フォームの名前定義)で取得。
form-withの場合、form-tagに比較して深い階層に保存される為、require()が必要になる。フォームの入力データは、プライベートメソッドにて取得することで、Classの外部から呼び出されないようにする。

バリデーション 一定の制約をかける

空のデータを登録できないようにするような場合に使用。
validates フォームの名前定義, presence: true

link_to

link_to 表示するテキスト, prefixもしくは、URIパターン, HTTPメソッド
HTTPメソッドを省略した場合は、GETで実行されるので注意する。

ridirect_to

コントローラのビューに関わらず、特定のページにとばす。
ridirect_to アクション名 (実行条件?)

コードの記述(試験対策)

スコープの意識は・・・まだあんまりしなくていいのかな。
コードの書き順はよく確認すること。routes.rbのコードの書き順に注意。
いつもは頭に書いてるのが末尾にあったり、
先に定義していないといけないコードを、後ろに書いてあったりする。


railsのエラー修正問題ヤベェな。
どないしょ。

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

ApplicationControllerでStandardErrorをrescue_fromするときに少しでも開発しやすく

背景

Ruby on RailsでAPIサーバーを実装するとき、予期しないバグでアプリケーションに例外が起きたときに、500エラーレスポンスで固定のメッセージを返したいときがあります。

ApplicationControllerに、次のような関数呼び出しを書いて、アプリケーションで共通の例外処理を実装するでしょう。

rescue_from StandardError, with -> do
  render json: { message: 'Something wrong' }, status: :internal_server_error
end

問題

問題1: 開発中も例外が表示されない

開発中に出た例外が起きたときも、固定メッセージしか情報が得られません。
デバッグのためにいちいち上記の実装をコメントアウトするのも手間です。

解法: 環境変数を見て例外処理をスキップ

Ruby on Railsは環境変数で開発環境と本番環境を切り替えます。
このルールに乗り、次のように開発中の場合は例外処理をスキップします。

rescue_from StandardError, with -> do
  # 開発中は例外をキャッチしません。
  raise exception if Rails.env.development?

  render plain: 'Something wrong', status: :internal_server_error
end

問題2: テストではレスポンスコードやメッセージを本番と同じにしたい

RSpecなどのテストコードでは本番の振る舞いを確認したいです。
ですので開発中環境のように、例外処理そのものをスキップする実装は望ましくありません。

解法: 例外の内容を標準出力に

例外処理の実装はそのままに、例外の内容を標準出力に書き出します。
本場環境では余計な情報が出力されないように、ここでも環境変数を見て出力するかどうかを切り替えます。

rescue_from StandardError, with ->(exception) do
  puts exception.backtrace if Rails.env.test?

  render json: { message: 'Something wrong' }, status: :internal_server_error
end

問題3: 例外のバックトレースが長すぎて読みにくい

通常RSpecを実行する時は、複数のテストケースを実行します。
予期しない例外が起きる時は、複数のテストケースで起きることがあるでしょう。
そのときにバックトレースをすべて表示すると標準出力の表示が流れすぎて見るのが大変です。

解法: ActiveSupport::BacktraceCleanerで出力するバックトレースの情報を減らす

Ruby on RailsにはActiveSupport::BacktraceCleanerというバックトレースの情報を減らすためのクラスが用意されています。
これを使って、gem(依存ライブラリ)とRSpecに関するバックトレースの表示をしません。

rescue_from StandardError, with ->(exception) do
  if Rails.env.test?
    bc = ActiveSupport::BacktraceCleaner.new
    bc.add_silencer { |line| line =~ %r{gems|/rspec} }
    puts bc.clean exception.backtrace
  end

  render json: { message: 'Something wrong' }, status: :internal_server_error
end

問題3: 例外の色が黒字だと出力から例外情報をみつけられない

通常RSpecを実行する時は、複数のテストケースを実行します。
予期しない例外が起きる時は、複数のテストケースで起きることがあるでしょう。
たくさんの標準出力の内容から例外を目grepするのは大変です。

解法: 例外表示の1行目を赤くする

多くのターミナルはANSI escape codeを使うと出力文字に色をつけられます。
これを使って例外名に色を付けます。
またRSPecのエラーと合わせて赤字にすると、情報が必要以上に増えず読みやすくなります。

rescue_from StandardError, with ->(exception) do
  if Rails.env.test?
    puts "\e[31m", exception.class, "\t#{exception.message}\e[0m"

    bc = ActiveSupport::BacktraceCleaner.new
    bc.add_silencer { |line| line =~ %r{gems|/rspec} }
    puts bc.clean exception.backtrace
  end

  render json: { message: 'Something wrong' }, status: :internal_server_error
end

問題4: 独自例外が出力される

アプリケーション固有の例外処理を独自例外を使って共通化することがあります。
例外処理の対象をStandardErrorにしていると、これらの独自例外も標準出力に表示します。
また、共通の例外処理が上手く実装できているか確認するためのテストコードを書くこともあります。
その場合は、テストを実行すると常に独自例外が標準出力に表示されます。
バックトレースまで表示するので、馬鹿になりません。
偽陽性に慣れると、本当の例外まで見逃すようになります。

解法: 独自例外の時は標準出力に表示しない

例外の出力処理に、キャッチした例外が自作の独自例外か確認するガード条件を追加します。

rescue_from StandardError, with ->(exception) do
  if Rails.env.test?
    # MyErrorは、意図した例外なので出力しません。
    puts_detail_of exception unless exception.is_a? MyError
  end

  render json: { message: 'Something wrong' }, status: :internal_server_error
end

def puts_detail_of(exception)
  bc = ActiveSupport::BacktraceCleaner.new
  bc.add_silencer { |line| line =~ %r{gems|/rspec} }

  puts "\e[31m", exception.class, "\t#{exception.message}\e[0m"
  puts bc.clean exception.backtrace
end

おわりに

良き開発を、例外とともに

参考文献リスト

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

独学でプログラミング学習を始めて1.5か月の振り返りと独学者へのアドバイス

独学でプログラミング学習を始めて1.5か月ほどがたちました。転職、ポートフォリオ等なにも成果がない状態ですが、私自身の振り返りとアウトプットの習慣付けのために学習内容や反省点、これから独学される方へのアドバイスなどを投稿します。技術的なことは書いていません。

対象読者

・独学でプログラミング学習を始める方
・独学で挫折しそうな方

自己紹介

・24歳、新卒1年目、メーカー営業職で工場研修中(長い)
・過去2度プログラミングの独学挫折(HTML,CSS,PHP)
・2109年9月末から3度目の独学スタート

なぜ独学か

・お金がないからです。

学習内容

・HTML(復習)
・CSS(復習)
・Ruby
・Ruby on Rails
・MySQL
・Bootstrap
・AWS
・Docker
チュートリアルやドットインストール、YouTubeなどの教材を利用し、手を動かしながら学習しています。現在はVM上のcentOS環境にDocker×Rails×MySQLでポートフォリオ製作中です。

学習時間

・平日2時間
・休日4~10時間
平日は仕事前に0~1時間、帰宅後1~2時間という感じです。睡眠時間を削ることもありましたが翌日の体調が著しく悪化するため睡眠優先してます。その他、通勤・休憩などでPCを触れない自由時間が4時間以上あるので、その時間に技術書を読んだり、用語の理解に努めたりしています。
土曜日は朝から晩まで。日曜日は最低半日(約4時間)で後はリフレッシュに時間を使うようにしています。

成長したこと

・独学で1.5か月継続している!!
過去2回は2週間ほどで辞めてます。「エラーが出ることと向き不向きは関係ない」と考えるようになったのが一番の要因だと思います。

・コンピューター、プログラムとは何かということを学んだ
「コンピューターにはどんな機能があるのか」、「プログラムとは何か」を理解しないままプログラミングやってみたいと言っていた自分から卒業しました。

・公式ドキュメントを参考にするようになった。
Qiitaファーストでコピペとかしてましたが、用語や概念を少しずつ理解していくうちに公式ドキュメントに抵抗がなくなりました。(幸い英語のリーディングには抵抗がありません。)

反省

・プログラムを書く時間が少ない。
一番の反省点は、肝心のRuby, Rails, SQLへの理解が深まる前からDockerやAWSにも手を出してしまったことです。ただでさえ学習効率が悪いにも関わらず、DockerやAWSでの環境構築に時間がかかるうちに学習したことが頭から抜けていくという状態でした。

・アウトプット不足
エラーを解決した経験、隙間時間に学んだ用語などをQiitaに投稿する習慣が大切だと分かっていながらなかなかできませんでした。

今の悩み

・ポートフォリオがRailsチュートリアルのサンプルに寄せられていく。デザインに気を遣わないとチュートリアルと何が違うのかわからなくなります。

今後の方針

・基礎固め
ポートフォリオ製作を通して今まで学んだ技術への理解を深めていきます。新しい技術には手を出しません。

・アウトプットの習慣化
独学の最大のデメリットはアウトプット→フィードバックの機会が少ないために学習効率が悪いことだと感じています。(自走というより自歩です。)
自らアウトプット+フィードバックを得られる場を確保しなければインプットの質も下がり、自己満の勉強になってしまいます。
MENTAなどのサービスでメンターお願いします。

独学で学習を始めた方へアドバイス(自戒を込めて)

・エラーは解決できる
個人的に過去の挫折の原因は「エラーが出た→プログラミングに向いてない...」と考えてしまうことがでした。
「エラー発生→対応→解決しない」ということがよくあると思います。「対応して解決しなかった」ということは、プログラミングの向き不向きとは関係がありません(少なくとも始めて数か月は)。「うまくいかない方法を1つ見つけた」ということです。つまり解決に近づいているのです。初心者が出すエラーなど大したことはありません。検索して、ひたすら試せば解決できます。
「初心者がやるエラーはだいたい経験したかな」と思えるまでやってみてください。

・とりあえずPC立ち上げましょう
高すぎる目標やハードルを定めることは挫折のもとです。初めはとりあえず毎日PC立ち上げればオッケーぐらいの気持ちでいいかもしれません。私はたまにサボりそうになりますが、そのときはPC立ち上げることを目標にします。結局触りだすと楽しくなります。時にはハードルを思いっきり下げることも継続の秘訣です。

おわりに

スクールに通わないと決めた以上、自分がもつリソースを最大限活用して学習効率を上げていくのみです。少しずつ技術的なこと、サンプルアプリなどもアウトプットしていきます。
アドバイスいただけると幸いです。

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

Ruby on Rails チュートリアル第8章 一時cookieによるログイン機構

一時cookieによるログイン機構

 この記事ではログインの基本的な仕組みを実装する。ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))である。

セッション

 HTTPはステートレス (Stateless) なプロトコルである。文字通り「状態 (state)」が「ない (less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない。この特性のため、ブラウザのあるページから別のページに移動したときに、ユーザーのIDを保持しておく手段がHTTPプロトコル内「には」まったくない。ユーザーログインの必要なWebアプリケーションでは、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定する。
 
 Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法である。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータあり、あるページから別のページに移動した時にも破棄されないので、ここにユーザーIDなどの情報を保存できる。アプリケーションはcookies内のデータを使って、例えばログイン中のユーザーが所有する情報をデータベースから取り出すことができる。今回はsessionというRailsのメソッドを使って一時セッションを作成する。

 HTTPはリクエストの情報を全く保存出来ないので、ユーザーのブラウザに保存されるcookiesという小さなテキストデータにユーザーIDなどの情報を保存する。このcookieを用いて、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に設定する。今回はsessionというRailsのメソッドを使ってブラウザを閉じると破棄される一時セッションを作成する。
 
 セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと合わせて理解しやすい。ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった風に。ただしUsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使うという違いがある。

セッションコントローラ

 ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。ログインのフォームは、newアクションで処理する。createアクションにPOSTリクエストを送信すると、実際にログインし、destroyアクションにDELETEリクエストを送信すると、ログアウトする。

$ rails generate controller Sessions new

まず、上のコードでセッションコントローラとnewアクションを作成する。このnewアクションに対応するビューでログインフォームのページを作成する。
image.png

Usersリソースのときは専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、以下のように「名前付きルーティング」だけを使う。

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end

これで、getリクエストを/loginに発行すればセッションコントローラのnewアクションを呼び出せる(名前つきルートはlogin_path)などといった操作が実現される。

ログインフォーム

 コントローラとルーティングを定義したので、今度は新しいセッションで使うビュー、つまりログインフォームを整える。ログインフォームとユーザー登録フォームの違いは、4つあったフィールドが [Email] と [Password] の2つに減っていることだけである。
ユーザー登録フォームでform_forヘルパーを使い、ユーザーのインスタンス変数@userを引数にとっていた。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

しかし、セッションにはSessionモデルというものがなく(ユーザー登録においてはユーザーモデルが存在した)、そのため@userのようなインスタンス変数に相当するものもない。したがって、新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。

form_for(@user)

Railsではユーザーモデルがある場合は上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定する、セッションの場合はリソースの名前とそれに対応するURLを以下のように具体的に指定する必要がある。

form_for(:session, url: login_path)

以上を踏まえると、ログインフォームのコードは以下のようになる。

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

ユーザーがすぐクリックできるように、ユーザー登録ページのリンクを追加してある。フォームに入力した値が、paramsハッシュのparams[:session][:email]とparams[:session][:password]となる。
なお、paramsは次のような入れ子ハッシュになっている。ハッシュの中にハッシュがある構造である。

{ session: { password: "foobar", email: "user@example.com" } }

ユーザーの検索と検証

最初に、セッションコントローラーのcreateアクションを完成させていく。createアクションではユーザー認証に必要なあらゆる情報をparamsハッシュから取り出せる。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase) #フォームに入力したメールアドレスと同じメールアドレスをもつユーザーをデータベースから探す
    if user && user.authenticate(params[:session][:password]) #フォームに入力したパスワードをハッシュ化したものと、先ほど探したユーザのpassword_digestと一致するかを確認する
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

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

Ruby on Rails チュートリアル第8章 一時cookieによるログイン機構

一時cookieによるログイン機構

 この記事ではログインの基本的な仕組みを実装する。ログインの基本的な仕組みとは、ブラウザがログインしている状態を保持し、ユーザーによってブラウザが閉じられたら状態を破棄するといった仕組み (認証システム (Authentification System))である。

セッション

 HTTPはステートレス (Stateless) なプロトコルであり、リクエストの情報を全く保存出来ないので、ユーザーのブラウザに保存されるcookiesという小さなテキストデータにユーザーIDなどの情報を保存する。このcookieを用いて、セッション (Session) と呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に設定する。今回はsessionというRailsのメソッドを使ってブラウザを閉じると破棄される一時セッションを作成する。
 
 セッションをRESTfulなリソースとしてモデリングできると、他のRESTfulリソースと合わせて理解しやすい。ログインページではnewで新しいセッションを出力し、そのページでログインするとcreateでセッションを実際に作成して保存し、ログアウトするとdestroyでセッションを破棄する、といった風に。ただしUsersリソースではバックエンドでUserモデルを介してデータベース上の永続的データにアクセスするのに対し、Sessionリソースでは代わりにcookiesを保存場所として使うという違いがある。

セッションコントローラ

 ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。ログインのフォームは、newアクションで処理する。createアクションにPOSTリクエストを送信すると、実際にログインし、destroyアクションにDELETEリクエストを送信すると、ログアウトする。

$ rails generate controller Sessions new

まず、上のコードでセッションコントローラとnewアクションを作成する。このnewアクションに対応するビューでログインフォームのページを作成する。
image.png

Usersリソースのときは専用のresourcesメソッドを使ってRESTfulなルーティングを自動的にフルセットで利用できるようにしたが、Sessionリソースではフルセットはいらないので、以下のように「名前付きルーティング」だけを使う。

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end

これで、getリクエストを/loginに発行すればセッションコントローラのnewアクションを呼び出せる(名前つきルートはlogin_path)などといった操作が実現される。

ログインフォーム

 コントローラとルーティングを定義したので、今度は新しいセッションで使うビュー、つまりログインフォームを整える。ログインフォームとユーザー登録フォームの違いは、4つあったフィールドが [Email] と [Password] の2つに減っていることだけである。
ユーザー登録フォームでform_forヘルパーを使い、ユーザーのインスタンス変数@userを引数にとっていた。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

しかし、セッションにはSessionモデルというものがなく(ユーザー登録においてはユーザーモデルが存在した)、そのため@userのようなインスタンス変数に相当するものもない。したがって、新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。

form_for(@user)

Railsではユーザーモデルがある場合は上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定する、セッションの場合はリソースの名前とそれに対応するURLを以下のように具体的に指定する必要がある。

form_for(:session, url: login_path)

以上を踏まえると、ログインフォームのコードは以下のようになる。

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

ユーザーがすぐクリックできるように、ユーザー登録ページのリンクを追加してある。フォームに入力した値が、paramsハッシュのparams[:session][:email]とparams[:session][:password]となる。
なお、paramsは次のような入れ子ハッシュになっている。ハッシュの中にハッシュがある構造である。

{ session: { password: "foobar", email: "user@example.com" } }

ユーザーの検索と検証

最初に、セッションコントローラーのcreateアクションを完成させていく。createアクションではユーザー認証に必要なあらゆる情報をparamsハッシュから取り出せる。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase) #フォームに入力したメールアドレスと同じメールアドレスをもつユーザーをデータベースから探す
    if user && user.authenticate(params[:session][:password]) #フォームに入力したパスワードをハッシュ化したものと、先ほど探したユーザのpassword_digestと一致するかを確認する
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end

入力されたメールアドレスを持つユーザーがデータベースに存在し、かつ入力されたパスワードがそのユーザーのパスワードである場合のみ、if文がtrueになる。

ログイン

 この節では、cookiesを使った一時セッションでユーザーをログインできるようにする。このcookiesは、ブラウザを閉じると自動的に有効期限が切れるものを使う。
 sessionを実装するには、Sessionsコントローラを生成した時点で既に自動生成されているセッション用ヘルパーモジュールを、Applicationコントローラにこのモジュールを読み込ませる必要がある。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

log_in メソッド

Railsで事前定義済みのsessionメソッドを使って、単純なログインを行えるようにする (なお、これは先ほど生成したSessionsコントローラとは無関係である)。このsessionメソッドはハッシュのように扱えるので、次のように代入する。

session[:user_id] = user.id

上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。この後のページで、session[:user_id]を使ってユーザーIDを元通りに取り出すことができる。sessionメソッドで作成された一時cookiesは、ブラウザを閉じた瞬間に有効期限が終了する。

 同じログイン手法を様々な場所で使い回せるようにするために、Sessionsヘルパーにlog_inという名前のメソッドを定義する。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

sessionメソッドで作成した一時cookiesは自動的に暗号化され、上のコードは保護される。そして重要な事には、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできない。ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当せず。cookiesメソッドで作成した「永続的セッション」には当てはまらない。
以下にlog_inメソッドを含むcreateアクションを示す。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

このコードにより、ユーザーが認証に成功すると、log_inメソッドにより一時セッションにユーザーIDが保存されログインし、ユーザーのプロフィールページにリダイレクトされる。

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

enumで、キーの文字列に数字を含める方法

*前提*

schema.rb
t.integer "sex"
t.integer "age"

通常のenum(文字列のみ)の書き方は以下の通り。

user.rb
enum sex: { 未選択: 0, 男性: 1, 女性: 2 }

enumで数字を含めるには以下のように

user.rb
enum age: { 未選択: 0, "10代": 1, "20代": 2, "30代": 3, "40代": 4, "50代": 5, "60代": 6, "70代": 7, "80代": 8, "90代": 9 }

キーをダブルクオーテーション("")で囲えば良い。
むしろ本来こんなふうに書くべきなのかもしれない。

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

【初学者向け】 Railsのform_forでプルダウン(セレクトボックス)を実装してみる

概要

今回はrailsアプリケーションにおいてフォームを送信する際に、以下のような簡単なプルダウンを実装できるようにします。

スクリーンショット 2019-11-15 3.34.17.png

スクリーンショット 2019-11-15 3.35.25.png

上記の例は以下のようなコードで実装できます。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :faculty, [["理工学部", "理工学部"], ["経済学部", "経済学部"], ["法学部", "法学部"], ["医学部", "医学部"]], include_blank: "選択して下さい" %>
<% end %>

一般形

コードを一般形に直すと以下のようになります。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :保存先のカラム名, [["選択肢1", "実際にDBに保存させる内容"], ["選択肢2", "実際にDBに保存させる内容"]], include_blank: "選択して下さい" %>
<% end %>

「保存先のカラム名」には、プルダウンで選択した値を送信する際に保存したいカラムの名前を記述します。

「選択肢」と「実際にDBに保存させる内容」は基本一緒にして大丈夫です。
(「実際にDBに保存させる内容」を変えれば、"経済学部"を選択して送信した際に保存される内容を例えばtrueや2などの整数にする、といったことができます。)

参考記事

【開発メモ】Ruby on Railsのform_forでドロップダウンリストの選択ボックスを設置する方法

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

Railsのform_forでプルダウン(セレクトボックス)を実装してみる

概要

今回はrailsアプリケーションにおいてフォームを送信する際に、以下のような簡単なプルダウンを実装できるようにします。

スクリーンショット 2019-11-15 3.34.17.png

スクリーンショット 2019-11-15 3.35.25.png

上記の例は以下のようなコードで実装できます。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :faculty, [["理工学部", "理工学部"], ["経済学部", "経済学部"], ["法学部", "法学部"], ["医学部", "医学部"]], include_blank: "選択して下さい" %>
<% end %>

一般形

コードを一般形に直すと以下のようになります。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :保存先のカラム名, [["選択肢1", "実際にDBに保存させる内容"], ["選択肢2", "実際にDBに保存させる内容"]], include_blank: "選択して下さい" %>
<% end %>

「保存先のカラム名」には、プルダウンで選択した値を送信する際に保存したいカラムの名前を記述します。

「選択肢」と「実際にDBに保存させる内容」は基本一緒にして大丈夫です。
(「実際にDBに保存させる内容」を変えれば、"経済学部"を選択して送信した際に保存される内容を例えばtrueや2などの整数にする、といったことができます。)

参考記事

【開発メモ】Ruby on Railsのform_forでドロップダウンリストの選択ボックスを設置する方法

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

【Rails】 form_for でプルダウン(セレクトボックス)を実装してみる

概要

今回はrailsアプリケーションにおいてフォームを送信する際に、以下のような簡単なプルダウンを実装できるようにします。

スクリーンショット 2019-11-15 3.34.17.png

スクリーンショット 2019-11-15 3.35.25.png

上記の例は以下のようなコードで実装できます。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :faculty, [["理工学部", "理工学部"], ["経済学部", "経済学部"], ["法学部", "法学部"], ["医学部", "医学部"]], include_blank: "選択して下さい" %>
<% end %>

一般形

コードを一般形に直すと以下のようになります。

hoge/view/new.html.erb
<%= form_for @hoge do |f| %>
   # 以下の一行がプルダウンを実装するためのコード
   <%= f.select :保存先のカラム名, [["選択肢1", "実際にDBに保存させる内容"], ["選択肢2", "実際にDBに保存させる内容"]], include_blank: "選択して下さい" %>
<% end %>

「保存先のカラム名」には、プルダウンで選択した値を送信する際に保存したいカラムの名前を記述します。

「選択肢」と「実際にDBに保存させる内容」は基本一緒にして大丈夫です。
(「実際にDBに保存させる内容」を変えれば、"経済学部"を選択して送信した際に保存される内容を例えばtrueや2などの整数にする、といったことができます。)

参考記事

【開発メモ】Ruby on Railsのform_forでドロップダウンリストの選択ボックスを設置する方法

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

フォロー機能 User.rbとRelationship.rb

Relationship.rb
class Relationship < ApplicationRecord
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
end

Relationshipモデル(中間モデル)には、
:boy:follower (フォローする人)
:two_women_holding_hands:followed(フォローされる人)
が存在している。

user.rb
  has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
  # フォロー取得。Relationshipモデルのfollower_idにuser_idを格納

  has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
  # フォロワー取得。followed_idにuser_idを格納

上2行は、user主体で得る情報。
特定のアクションが起きた際、user_idが格納される場所をforeign_keyで指定することで、follower_id および followed_id の適切な方にuser_idを格納。

また、フォロー数等のときは、ユーザ情報の中身を必要としないので

<% current_user.follower.each do |user| %>
<%= user.count %>
<% end %>

のように、followerをとってよい(フォロー数取得)。

(フォロワー数を取得する場合は、followed)

user.rb
  has_many :following_user, through: :follower, source: :followed
  #following_userを命名。自分がフォローしているユーザ情報を取得。

  has_many :follower_user, through: :followed, source: :follower
  #自分をフォローしているユーザ情報を取得。

ユーザ情報を詳しく必要とする場合は、こちら(emailなど)。

<% current_user.following_user.each do |user| %>
<%= user.email %>
<% end %>

following_userで、followerテーブルを通じて、followedテーブルを参照。

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

ユーザ一覧で"フォローする"ボタンを押したときの挙動 備忘録

user/index/html.erb
      <% if user != current_user %>

        <% if current_user.following?(user) %>
            <%= link_to 'フォロー解除', unfollows_path(user.id), method: :POST %>
        <% else %>
            <%= link_to 'フォローする', follows_path(user.id), method: :POST %>
        <% end %>

      <% end %>

上の 'フォローする' ボタンを押すと、follows_path が読み込まれる

follows_pathは

routes.rb
post 'follows/:id' => 'relationships#follows', as: "follows" # フォローする。follows_pathの作成。

上の as "follows"で名前付きパスをつけたもの。
follows_path が呼び出されると、
relationships#follows(relationshipsコントローラのfollowsアクション)
が呼び出される

relationships.controller.rb
  # ユーザーをフォローする
  def follows(user_id)
    follower.create(followed_id: user_id)
  end

フォロー完了。

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

【rails】jQueryでページカレントをつけようとして読み込み問題でハマった話

まずは完成図gif

a7abc847458f31a52675d3b296bed1fc.gif
なんの変哲もなく、しっかりとページカレントできています。

書き加えたコードはこれ

sidebar.js
$(window).load(function () { //.loadでページが完全に読み込まれたら実行されるようにする

  var href = $(".main-header__left-box__current-group").data("group-id")
  $(".group").each(function (i, ele) {
    if ($(ele).data("group-id") === href) { //group__side_currentが色の変わるclass
      $(this).addClass("group__side_current") //data-id一致でclassをつける
    } else {
      $(this).removeClass("group__side_current")//data-id一致しなければclass削除
    }
  });

});

上記のjqueryの動きを図解?するとこんな感じです。
タイトル部分にお勉強中にdata-idをグループ名部分につけるくだりがあり、再利用しました。
5b43aaaf86bab5bfb706ea37c9f932a8.png

問題はこのコード

application.js
//= require turbolinks

このコードがあることによって、
な ぜ か 挙動が不安定になりました。
その時のgifがこちら

f8291d5fd08a791fb8e71e374fa32a08.gif

  • クリックすると何もおこらない
  • リロードするとなぜかカレントがつく

この時コンソールに入れて確認しましたが、これは普通に動作しました。

turbolinks = Ajaxによるページ遷移の高速化のためのライブラリ
~中略~
遷移にあたって今のページと共有するリソース(css/img/js)の再読込が不要となるため、読み込みが速くなる。
なお、フックイベントを使って遷移をキャンセルできる。
turbolinksチートシートより抜粋

再読み込みを不要にしてるため、ページが変わったことによるcssの付与やjsの再試行が行われてなく、うまく動作してないみたいですね。

turbolinksにおいて、リンクがクリックからページ表示までの一連の流れをVisitと呼ぶ。
Visitは大きく二種類:
Application Visits = リンククリックによるページ遷移
Restoration Visits = ブラウザの戻る/進むボタンによるページ遷移
turbolinksチートシートより抜粋

リロードすると動作しているのは、「turbolinks」を通したブラウザの動作でないため、
読み込みの挙動は通常通りに行われている。だからカレントがついたわけなんですね。

うわぁ。なんてことだ。
犯人確定の証言記事じゃないか...たまげたな。

今後の開発で気をつけようと思った点

現時点では「turbolinks」を削除しても動作に問題はない。
世に出回るサービスのように複雑でないし、アクセスがたくさんあるわけでもない、画像もあまり表示されていない。
大したことしてないから「速い」ので「turbolinks」の恩恵が正直わかりませんでした。

しかしサービスを作るとなると、素早くスムーズに動くことは必須なので、
プロジェクトで「これを使おう!」ってなった際には、個別に読み込みさせて影響範囲を絞ってあげることが必要かと思いました。

Railsで、任意のJavaScriptやCSSだけを読み込む
https://qiita.com/Oakbow/items/2e712e05bb4bbf68faf5

↑こういうのとかで

参考記事

ナビゲーションをカレント表示する方法まとめ【jQueryとかWordPress】
https://webkcampus.com/201904/1598/

【jQuery】「$(document).ready()」と「$(window).load()」の違いを極める!
https://on-ze.com/archives/1851

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