- 投稿日:2020-09-25T23:32:12+09:00
【rails】他テーブルのDBの情報をビューで表示させる
はじめに
この記事は、学んだことを復習するために投稿者が理解できる言葉まで落とし込んで書いております。まだまだ初心者ですが、この記事が読んでくださった方の悩みが解決すれば嬉しいです。
1.結論
- アソシエーションが定義されていること
- ルーティングが入れ子構造(ネスト)になっていること
- 表示させたい側のコントローラーで値を取得していること
- ビューで表示させる記述がなされていること
2.コード
2-1.テーブル
- itemsテーブル
class CreateItems < ActiveRecord::Migration[6.0] def change create_table :items do |t| t.string :name , null: false # 商品名 t.text :explanation , null: false # 商品の説明 t.integer :category_id , null: false # カテゴリー t.integer :status_id , null: false # 商品の状態 t.integer :delivery_fee_id , null: false # 配送料の負担 t.integer :shipping_region_id , null: false # 発送地域 t.integer :shipping_day_id , null: false # 発送までの日数 t.integer :selling_price , null: false # 販売価格 t.references :user , null: false, foreign_key: true # 外部キー(user情報) t.timestamps end end end
- ordersテーブル
class CreateOrders < ActiveRecord::Migration[6.0] def change create_table :orders do |t| t.references :user, null: false, foreign_key: true # 外部キー(ユーザー情報) t.references :item, null: false, foreign_key: true# 外部キー(商品情報) t.timestamps end end end2-2.モデル
- itemモデル
class Item < ApplicationRecord belongs_to :user has_one :order # ポイント① has_one_attached :image extend ActiveHash::Associations::ActiveRecordExtensions belongs_to_active_hash :category belongs_to_active_hash :status belongs_to_active_hash :delivery_fee belongs_to_active_hash :shipping_region belongs_to_active_hash :shipping_day # 共通で、空の投稿を保存できないようにする with_options presence: true do validates :image validates :name validates :explanation validates :selling_price end # 共通で、選択が「---」の時は保存できないようにする with_options numericality: { other_than: 1 } do validates :category_id validates :status_id validates :delivery_fee_id validates :shipping_region_id validates :shipping_day_id end # 販売価格の範囲が、¥300以上~¥9,999,999未満であること validates :selling_price, numericality: { greater_than_or_equal_to: 300, less_than_or_equal_to: 9_999_999 } # 販売価格は半角数字のみ入力可能 VALID_DELIVERY_FEE_REGEX = /[0-9\d]/.freeze validates :selling_price, format: { with: VALID_DELIVERY_FEE_REGEX } end
- orderモデル
class Order < ApplicationRecord belongs_to :user belongs_to :item # ポイント① has_one :address end2-3.ルーティング
Rails.application.routes.draw do devise_for :users root 'items#index' resources :items do resources :orders, only:[:index, :create] end endアソシエーション先のレコードのidをparamsに追加してコントローラーに送るためにルーティングのネストを使用します。
2-4.コントトーラー
class OrdersController < ApplicationController def index @item = Item.find(params[:item_id]) end endコントローラーですでに保存してある商品情報を、Item.find(params[:item_id])で取得し、@itemに代入します。
2-5.ビュー
@item.表示したいカラム名 # 上記のように記述することで、他テーブル(items)の情報をビューで表示させることが可能です。app/views/orders/index.html.erb/
<div class='buy-item-info'> <%= image_tag @item.image, class: 'buy-item-img' %> <div class='buy-item-right-content'> <h2 class='buy-item-text'> <%= @item.explanation %> </h2> <div class='buy-item-price'> <p class='item-price-text'>¥<%= @item.selling_price %></p> <p class='item-price-sub-text'>(税込)<%= @item.delivery_fee.name %></p> </div> </div> </div>以上です。
- 投稿日:2020-09-25T23:05:17+09:00
【Rails6.0】突然CSSが読み込まれなくなった、、【怪奇現象?】
現在個人開発でRailsアプリを作っているのですが、正常に動いていたRailsアプリのCSSが突然読み込まれなくなり(画像ファイルも)無駄に時間を食ってしまいました。
結局はっきりとした原因はわかりませんが、解決するまでの過程をここに書いていこうと思います。開発環境
- windows10 Pro
- Rails: 6.0.3.2
- ruby: 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
- Docker for windows
- MySQL 5.7
- nginx:1.15.8
エラーが発生した経緯
rspecのシステムスペックでテストを書いて実行、失敗を10回ほど繰り返してlocalhost(root_path)にアクセスしたら
CSSが反映されなくなっていた。
そのときのConsoleのエラー内容↓Failed to load resource: the server responded with a status of 500 (Internal Server Error)このエラー自体はよく見るやつで、タイトル通りCSSファイルが読み込まれていないというものでした。
解決手順(?)
この解決策が正しいのか正直微妙ですが、治るまでの経緯をここに書かせて頂きます。
結論から言いますと、nignx.confの内容を適当に書き換えて、dockerコンテナをビルドし直して起動しなおしてエラーを起こしてもう一度ビルドし直してコンテナ起動したら治ったという感じです。普通に再ビルドしただけでは治りませんでした。これのせいで時間がかかりましたね、、たまたまエラー起こせたんで治せましたが、、
これだとわかる人にしかわからないでの以下で詳しく説明します。1.nginx.confの内容を書き換える
私の開発環境ではなるべく本番環境に近づけるためにWEBサーバーとしてnginxコンテナを起動させています。
そのnginx.confはその設定ファイルです↓# プロキシ先の指定 # Nginxが受け取ったリクエストをバックエンドのpumaに送信 upstream webapp { # ソケット通信したいのでpuma.sockを指定 server unix:///webapp/tmp/sockets/puma.sock; } server { listen 80; # ドメインもしくはIPを指定 server_name webapp ; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # ドキュメントルートの指定 root /webapp/public; client_max_body_size 100m; error_page 404 /404.html; error_page 505 502 503 504 /500.html; try_files $uri/index.html $uri @webapp; keepalive_timeout 5; # リバースプロキシ関連の設定 location @webapp { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://webapp; } }この設定ファイルのproxy_passを適当に書き換えてわざとエラーをおこします。(場所はどこでもいいかも)
2.dockerを再ビルドして起動する。
$ docker-compose down $ docker-compose up -d --build-dオプションはバックグランドで実行してくれます。
--buildオプションをつけるとビルドから起動まで一気にやってくれます。そうすると、
$ docker-compose ps Name Command State Ports ---------------------------------------------------------------------------- webapp_app_1 bundle exec puma -C config ... Up webapp_db_1 docker-entrypoint.sh mysqld Up 3306/tcp, 33060/tcp webapp_web_1 /bin/sh -c /usr/sbin/nginx ... Exit 1エラーが起きて webapp_web_1コンテナがExitするはずです。(コンテナ名は、docker-compose.ymlの設定で変わるので各自置き換えてください。)
3.書き換えたnginx.confを元に戻して再ビルド
ここで適当に書き換えてわざとエラーを起こしたnginx.confのファイルを元に戻します。
そして2を再び実行します。
そしたらなぜか反映されるようになりました!嬉しい!!最後まで読んでいただきありがとうございます!
正直今回の方法がどういうロジックで治ったのか?そもそも正しいのか?というのはわかりません。もし、わかるかたがいましたらコメントいただけると幸いです。
とりあえず、これをやったら治ったというのは事実です。production環境でプリコンパイル時にCSS読み込みがうまくいかないとかならまだわかるんですが、今回は突然のエラーだったので治すのに時間がかかってしまいました。同じエラーに遭遇している方に、こちらが役に立てばうれしいです!
- 投稿日:2020-09-25T22:59:38+09:00
RSpec - 'users validation'のテストコードをレビューしてもらった結果
はじめに
みなさん、こんにちわ!Qiitaの投稿、ruby、rails、rspec初心者の J です!
つい先日、「プロから見た僕のspecはどうなの?」というのが気になり、
RSpec - 'users validation'のテスト を質問として、投稿しました。
そして、rubyやrspecについての動画をyoutubeで配信されていて、
僕の先生である@jnchito さんにコードレビューをしてもらいました!
テストコードは、そこそこ長かったのですが、一つ一つ徹底的に指摘してもらえて、
「僕ならこう書く!」を丁寧に解説してくださいました!
おかげさまで、「ルールに従ったテストコードがなんなのか」と迷子になり、
なかなかcommitできず、地獄を彷徨っています。から開放されて今は天にも上ような気分です!、、レビュー頂いて本当にありがとうございます。。泣
@jnchito さんについて
有名なので、みなさん知ってるかもですが、、
- プロを目指す人のためのRuby入門
rubyについての書籍を出していたり、、- Everyday Rails - RSpecによるRailsテスト入門
rspecについての書籍の和訳をしたり、、- 正規表現でテキスト中のTwitterアカウントだけを抜き出してみる
youtubeで動画配信したり、、勉強会を開催している動画もありました!- (1/2)Qiitaに投稿されていたRSpecのテストコードをレビューしてみた
今回のレビューしてもらった動画はこちら!めっちゃかっこいいダンディな先生です!!
という訳でこの記事は、僕自信がスペッカーになるためのアウトプットでもありますが、
まだ僕と同じくらいのスキル感の同志たちの参考になるべく、
指摘・解説してもらった内容から再度、自分自身のコードを見返してダメなところをガチガチに直してみました!
よかったら参考にしてみてください!修正前の僕のゴミコード
前回のコードとのbefore、afterテストコードを載せるとQiitaのデータベースがパンクしてしまうかも ←(・・。)
なので、 修正前の僕のゴミコード を比較する場合は、タブを複製してどうぞ!Specのネストイメージ
「ネストが深すぎなくていいんじゃないか?」って訳で、なるべく浅く修正!
RSpec.describe User, type: :model do describe 'attribute: name' context 'when present' it 'is valid' # ...省略 end end end end上の内容は、、
User > name > when present > it is valid
英訳的にはおかしな感じになりますが、どのネストも、、
「ユーザー名が存在する場合は有効です」的なニュアンスでわかりやすくネストし直しました。
これはあくまでイメージです!修正後
Userモデル
app/models/user.rbclass User < ApplicationRecord before_save :downcase_email validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } private def downcase_email self.email = self.email.downcase end end補足
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
railstutorialより 「2つの連続したドットはマッチさせないようにする」こちらの正規表現に修正しました。self.email.downcase!
と、破壊的メソッドはあまり使用しない。
将来的にいつどこで参照されるか不明なので、破壊的メソッドを使用しない方が無難。
、、、たしかに。(ということで修正済みが以下。)def downcase_email self.email = self.email.downcase endFactoryBot
spec/factories/users.rbFactoryBot.define do factory :user do name { 'Tony Stark' } email { 'tony@example.com' } end end補足
- 修正前:
name { 'hoge' }
、email { "foo@bar.com" }
ありえない思っていたメールアドレスのドメイン部分。。あ、あれ?
bar.com ポチッ!
、、、@example.com
を使用すると誓います。Spec
spec/models/user_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do let(:user) { FactoryBot.build(:user) } describe 'attribute: name' do context 'when present' do # nameが存在している場合は、有効であること it 'is valid' do user.name = 'Tony Stark' expect(user).to be_valid end end context 'when blank' do # nameが空白の場合は、無効であること it 'is invalid' do user.name = ' ' expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") end end context 'when empty' do # nameが空の場合は、無効であること it 'is invalid' do user.name = '' expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") end end context 'when nil' do # nameが存在していない場合は、無効であること it 'is invalid' do user.name = nil expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") end end context 'when length is 50 characters or less' do # nameが50文字以下の場合は、有効であること it 'is valid' do user.name = 'a' * 50 expect(user).to be_valid end end context 'when length is more than 50 characters' do # nameが50文字を超える場合は、無効であること it 'is invalid' do user.name = 'a' * 51 expect(user).to be_invalid expect(user.errors[:name]).to include('is too long (maximum is 50 characters)') end end end describe 'attribute: email' do context 'when present' do # emailが存在している場合は、有効であること it 'is invalid' do user.email = 'tony@example.com' expect(user).to be_valid end end context 'when blank' do # emailが空白の場合は、無効であること it 'is invalid' do user.email = ' ' expect(user).to be_invalid expect(user.errors[:email]).to include("can't be blank") end end context 'when empty' do # emailが空の場合は、無効であること it 'is invalid' do user.email = '' expect(user).to be_invalid expect(user.errors[:email]).to include("can't be blank") end end context 'when nil' do # emailが存在していない場合は、無効であること it 'is invalid' do user.email = nil expect(user).to be_invalid expect(user.errors[:email]).to include("can't be blank") end end context 'when length is 50 characters or less' do # emailが255文字以下の場合は、有効であること it 'is valid' do user.email = 'a' * 243 + '@example.com' expect(user).to be_valid end end context 'when length is more than 50 characters' do # emailが255文字を超える場合は、無効であること it 'is invalid' do user.email = 'a' * 244 + '@example.com' expect(user).to be_invalid expect(user.errors[:email]).to include('is too long (maximum is 255 characters)') end end context 'when correct format' do # emailの形式が正しい場合は、有効であること it 'is valid' do user.email = 'user@example.com' expect(user).to be_valid user.email = 'USER@foo.COM' expect(user).to be_valid user.email = 'A_US-ER@foo.bar.org' expect(user).to be_valid user.email = 'foo.bar@baz.jp' expect(user).to be_valid user.email = 'foo+bar@baz.cn' expect(user).to be_valid end end context 'when is incorrect format' do # emailの形式が正しくない場合は、無効であること it 'is invalid' do user.email = 'user@example,com' expect(user).to be_invalid user.email = 'user_at_foo.org' expect(user).to be_invalid user.email = 'user.name@example.' expect(user).to be_invalid user.email = 'foo@bar_baz.com' expect(user).to be_invalid user.email = 'foo@bar+baz.com' expect(user).to be_invalid end end context 'when already taken' do # 同一のemailが既に登録されている場合は、無効であること it 'is invalid' do FactoryBot.create(:user, email: 'tony@example.com') user.email = 'tony@example.com' expect(user).to be_invalid expect(user.errors[:email]).to include('has already been taken') end end context 'when case insensitive and not unipue' do # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること it 'is invalid' do FactoryBot.create(:user, email: 'tony@example.com') user.email = 'TONY@EXAMPLE.COM' expect(user).to be_invalid expect(user.errors[:email]).to include('has already been taken') end end # emailが小文字で保存されていること it 'is saved in lowercase' do user.email = 'TONY@EXAMPLE.COM' user.save! expect(user.reload.email).to eq 'tony@example.com' end end end補足
- 先生:必要ないデバックコードは消しましょう。
僕:あ、はい!でもテストしてるときにpry
は便利です!
- 先生:
FactoryBot
のユーザーを見にいかないといけないのはよくないなぁ
僕:FactoryBot
でユーザーを定義している場合でも、エクスペクテーションを書く前に明確にnameに値を入れます!
- 僕:
context
、it
の文章を修正しました!
- context: 'when ~' ~である時をグループ化
- it: validパターンといinvalidパターンを検証
イメージはネストイメージの部分の文脈のニュアンスです。
- 先生:saveの検証まではいらないかなぁ。。
validationが通ってsaveで落ちることは、滅多にないし、もしあったらrailsのバグか何か。
僕:、、、なるほど!(削除済み以下。)
修正前のコードを抜粋:expect(user.save).to be_falsey
- 先生:ここのテストをまとめよう。
「nameが存在していない場合は、無効であること」
name = ' '
(スペースのみ)
name = ''
(空)
name = nil
(nil)
context 'when name is blank' do # nameが存在しない場合は、無効であること it 'is invalid' do user.name = ' ' expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") user.name = '' expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") user.name = nil expect(user).to be_invalid expect(user.errors[:name]).to include("can't be blank") end end僕:ここはなんとなくまとめずに書きたいと思ったので
blank
、empty
、nil
で区分けしました!
- 僕:
user.valid?
と聞いてあげないとエラーメッセージにmessagesが入ってこないと思ってました、、
ということでuser.valid?
と聞かずにexpect
をそのまま記載!
- 先生:正規表現のテスト、ここはいいですね!
僕:あざっす!
- 先生:dryを捨てて、変数に格納してあれやこれやしない!
ロジックを組むと、そのロジックにミスが発生して思わぬ事故になる。。。
- 先生:
uniqueness: { case_sensitive: false }
をテストした方がいいのでは?
僕:(2/2)Qiitaに投稿されていたRSpecのテストコードをレビューしてみたで解説してもらったように、、
- emailアドレスが小文字の有効なuserを保存。
- 上で登録したuserの大文字emailをuserのemailに入れ直す。
- userは無効であること。
- また、エラーメッセージに
has already been taken
が含まれていること。 が以下のコード。context 'when case insensitive and not unipue' do # emailの大文字と小文字を区別せず、一意ではない場合は、無効であること it 'is invalid' do FactoryBot.create(:user, email: 'tony@example.com') user.email = 'TONY@EXAMPLE.COM' expect(user).to be_invalid expect(user.errors[:email]).to include('has already been taken') end endまとめ
もっとたくさんの指摘もらいましたが、まだプロの考えに及んでいない自分がいるので、
レビュー&解説して頂いた動画を見直して理解した部分をもっと明確に記載しようと思います!
テスト書き始めの方は、僕と同じで「どこまでテスト」していいのかわからないとなる同志も少なくなさそうです。
それぞれ意見は異なると思いますが、初心者的にズバリ!'railsのバリデーションを信じて、自分や他の人の作ったロジックをテストしよう!'
ってことですね!
railsの検証をわざわざテストするのは、もったいないと思いました、、
また、先生も言っていた通り、「言い出したらキリがないもの」もあるということを考えれば、
テストポイントを絞る力の方が重要だと思います。
今回のバリデーションに対するテストは、ほぼ100%と言える正解のテストコードが存在すると思うので、
自分やチームメンバーが作ったロジックをテストする未来の練習にはなりそうです!それからみなさん!
必要でない限り、テストはdryを捨てましょう!
みんながコード読みやすくてハッピーになれるし、
何より、、
TESTが間違ってたら意味ないですからね、、wでは、また!
テストのアウトプット
User attribute: name when present is valid when blank is invalid when empty is invalid when nil is invalid when length is 50 characters or less is valid when length is more than 50 characters is invalid attribute: email is saved in lowercase when present is invalid when blank is invalid when empty is invalid when nil is invalid when length is 50 characters or less is valid when length is more than 50 characters is invalid when correct format is valid when is incorrect format is invalid when already taken is invalid when case insensitive and not unipue is invalid
- 投稿日:2020-09-25T22:19:06+09:00
クラスとモデル
- 投稿日:2020-09-25T20:42:29+09:00
(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第14章】
前提
・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者基本方針
・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。
ラストじゃ!!!!!最後まで駆け抜けろ!!!!!
ラストを飾るBGMはこちら。
My Bloody Valentine "Loveless"
始まりにして終わりみたいな一枚。もはやオチ担当。大音量で脳汁たらしながら聴きましょう。
【14.1.1 データモデルの問題(および解決策) 演習】
1. 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
→ これって、この時点でコンソール上で実行してもnomethoderrorでいいですよね?following定義してないし。(なんで演習解答まとめにみんな載せてないんだ?)
想像としては、id:1のユーザーがフォローしているユーザーのidの配列が返される。
2. 図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
→ ユーザー2がフォローしているユーザーのid(ここでは1)を返す。後者はその配列。
【14.1.2 User/Relationshipの関連付け 演習】
1. コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
→ 下記>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil> >> other_user = User.second User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil> >> user.active_relationships.create(followed_id: other_user.id) (0.1ms) SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] SQL (3.4ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-09-23 12:37:03.376561"], ["updated_at", "2020-09-23 12:37:03.376561"]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2020-09-23 12:37:03", updated_at: "2020-09-23 12:37:03">
2. 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
→ 上の解答の一番下に出てます。
【14.1.3 Relationshipのバリデーション 演習】
1. リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
→ GREENでした。
【14.1.4 フォローしているユーザー メモと演習】
has_many :配列の名称, through: :テーブル名称, source: :配列の元の集合 …なんかややこしいな。元から呼び名で使う名称の方で定義付けしとけばよかったのでは?疑問。
1. コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
→ 下記。うまくいってます。>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil> >> other_user = User.second User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil> >> user.following?(other_user) User Exists (0.6ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]] => false >> user.follow(other_user) (0.1ms) SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] SQL (6.4ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-09-23 14:03:47.235565"], ["updated_at", "2020-09-23 14:03:47.235565"]] (0.1ms) RELEASE SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil>]> >> user.following?(other_user) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]] => true >> user.unfollow(other_user) Relationship Load (0.2ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]] (0.1ms) SAVEPOINT active_record_1 SQL (0.2ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 1]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2020-09-23 14:03:47", updated_at: "2020-09-23 14:03:47"> >> user.following?(other_user) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]] => false
2. 先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
→ 上のとおり。INSERTしたりDELETEしたり。
【14.1.5 フォロワー メモと演習】
あーそうか。前節でわざわざthroughとsource使ってたのは、一個のテーブルを別々の側面から扱うためか。それにしてもfollowed_idは分かりにくいけど。
1. コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
→ 下記>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil> >> other1 = User.second User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<User id: 2, name: "Ms. Jerry Hermann", email: "example-1@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$4n7IPw3AcdhW6IzNuLygIuVLA26qlTNneHXDXIqW0zp...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QNHMG3qKng0pFdQdDfGNMeFZaDiddcT0z3ovdEVcOcn...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil> >> other2 = User.third User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 2]] => #<User id: 3, name: "Bernice Rippin", email: "example-2@railstutorial.org", created_at: "2020-09-21 06:47:52", updated_at: "2020-09-21 06:47:52", password_digest: "$2a$10$fsftEGHfcujlrAy4h.X2VelOSKNXNDnk71MbkBPOqSA...", remember_digest: nil, admin: false, activation_digest: "$2a$10$4DlqqHWVesXipOA4xC/XAOlA70S8T6PjkH3/T4RAI7M...", activated: true, activated_at: "2020-09-21 06:47:52", reset_digest: nil, reset_sent_at: nil> >> other1.follow(user) (0.1ms) SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] SQL (0.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 2], ["followed_id", 1], ["created_at", "2020-09-23 22:04:23.011442"], ["updated_at", "2020-09-23 22:04:23.011442"]] (0.1ms) RELEASE SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 2], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>]> >> other2.follow(user) (0.1ms) SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] SQL (0.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 3], ["followed_id", 1], ["created_at", "2020-09-23 22:04:33.583218"], ["updated_at", "2020-09-23 22:04:33.583218"]] (0.1ms) RELEASE SAVEPOINT active_record_1 User Load (0.1ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 3], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-21 06:47:51", updated_at: "2020-09-21 06:47:51", password_digest: "$2a$10$nAPmDn2RaEHJcHlMrK2PK.nOxUN4ULh7yUHchZRZtSZ...", remember_digest: nil, admin: true, activation_digest: "$2a$10$l9hNDaXUmopBnprlT0H7J.YFieEB8U9OoNgA0mzcrPS...", activated: true, activated_at: "2020-09-21 06:47:51", reset_digest: nil, reset_sent_at: nil>]> >> user.followers.map(&:id) User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => [2, 3]
2. 上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
→ 下記>> user.followers.count (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 2
3. user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
→ SQLは上のとおり。配列作る手間分、時間と負荷がかかるのでは。なのでcountのみの方が良い。>> user.followers.to_a.count => 2
【14.2.1 フォローのサンプルデータ 演習】
1. コンソールを開き、User.first.followers.countの結果がリスト 14.14で期待している結果と合致していることを確認してみましょう。
→ 3~48なんで期待どおり。>> User.first.followers.count User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 38
2. 先ほどの演習と同様に、User.first.following.countの結果も合致していることを確認してみましょう。
→ こちらもOK>> User.first.following.count User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 49
【14.2.2 統計と[Follow]フォーム メモと演習】
メンバールーティング:RESTfulなルーティングに新たなルーティングを追加できる。memberはブロックの形をとる。ユーザーidが含まれたURLを扱う。idを指定しない場合はcollectionを使う。
1. ブラウザから /users/2 にアクセスし、フォローボタンが表示されていることを確認してみましょう。同様に、/users/5 では [Unfollow] ボタンが表示されているはずです。さて、/users/1 にアクセスすると、どのような結果が表示されるでしょうか?
→ プロフィールページが表示される
2. ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認してみましょう。
→ 両方ともstatsが表示されています。
3. Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。
→ 下記。これ調べてみると、@user.active(またはpassive)_relationshipsって書いてるのが出てきたんですが、どっちもいけるんでしょうか?コンソールで試したところ、両方とも同じ結果が返されましたが、relationshipsの方が処理が少ないからいいのかも?saite_layout_tesr.rbtest "stats" do log_in_as(@user) get root_path assert_match @user.following.count.to_s, response.body assert_match @user.followers.count.to_s, response.body enduser_profile_test.rbtest "profile display" do get user_path(@user) assert_template 'users/show' assert_select 'title', full_title(@user.name) assert_select 'h1', text: @user.name assert_select 'h1>img.gravatar' assert_match @user.following.count.to_s, response.body assert_match @user.followers.count.to_s, response.body assert_match @user.microposts.count.to_s, response.body assert_select 'div.pagination', count: 1 @user.microposts.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end end>> user.active_relationships.count (0.1ms) SELECT COUNT(*) FROM "relationships" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 49 >> user.following.count (0.2ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 49
【14.2.3 [Following]と[Followers]ページ 演習】
1. ブラウザから /users/1/followers と /users/1/following を開き、それぞれが適切に表示されていることを確認してみましょう。サイドバーにある画像は、リンクとしてうまく機能しているでしょうか?
→ おーけーでーす
2. リスト 14.29のassert_selectに関連するコードをコメントアウトしてみて、テストが正しく red に変わることを確認してみましょう。
→ あれ?REDにならない?…あ、そうか。renderもコメントアウトしなきゃでした。@usersのブロックだけコメントアウトしてた。下記。show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= gravatar_for @user %> <h1><%= @user.name %></h1> <span><%= link_to "view my profile", @user %></span> <span><b>Microposts:</b><%= @user.microposts.count %></span> </section> <section class="stats"> <%= render 'shared/stats' %> <% if @users.any? %> <div class="user_avatars"> <%# @users.each do |user| %> <%#= link_to gravatar_for(user, size: 30), user %> <%# end %> </div> <% end %> </section> </aside> <div class="col-md-8"> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <%#= render @users %> </ul> <%= will_paginate %> <% end %> </div> </div>
【14.2.4 [Follow]ボタン(基本編) 演習】
1. ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
→ うまくいってます!
2. 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
→ Rendering users/show.html.erb within layouts/application
【14.2.5 [Follow]ボタン(Ajax編) メモと演習】
Ajaxの実装:
①form_forにremote: trueを記入。
②コントローラのアクション内にrespond_toメソッドを記入。Ajaxリクエストを受け取る。
③ブラウザ側でJSが無効になっている場合用に、config/application.rbファイルにそれ用の記述を追記。
④アクション名.js.erbにjQueryを記入。
jQuery忘れたなぁ……progateで復習してくるか。1. ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
→ うん、動いてます。
2. 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
→ Rendering relationships/create.js.erb
Rendering relationships/destroy.js.erb
【14.2.6 フォローをテストする メモと演習】
xhr: true を入れるだけでAjaxの動作をテストできる。
1. リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?
→ format.html { redirect_to @user } のところでunknownformatが出ました。
2. リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。
→ どこまで削除すればいいのか分からないけど、文法が変にならないように削除すればGREENです。
JSが無効なときに表示するフォーマットがないからでしょうか。(先ほどの演習というのは、演習1のことなのか、それともこの直近の削除のことをいっているのか、あいまいですね…)
【14.3.1 動機と計画 演習】
1. マイクロポストのidが正しく並んでいると仮定して (すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。
→ 投稿日時が新しいもの順で、各投稿のユーザーidの配列が返される?(自分自身とフォローしているユーザーのもののみ)
【14.3.2 フィードを初めて実装する 演習】
1. リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
→ ("user_id IN (?) OR user_id = ?", following_ids, id) の構成が分かれば解けますね。先の(?)に対応するのがfollowing_ids、後の?に対応するのが、idです。ということで、それぞれセットで消せば望んだエラーが返ってきます。今回は後者のセットを削除すると、# 自分自身の投稿を確認 のコメントアウト以下3行のFAILが返ってきます。user.rbdef feed Micropost.where("user_id IN (?)", following_ids) end
2. リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
→ 1の逆。前者のセットを消します。# フォローしているユーザーの投稿を確認 以下の3行のFAILが返ってきます。
3. リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
→ これって、要は全マイクロポストってことですよね。ということで下記。FAILは最後の# フォローしていないユーザーの投稿を確認 です。user.rbdef feed Micropost.all end
【14.3.3 サブセレクト 演習】
1. Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
→下記。assert_matchにお約束のresponse.body、となればマイクロポストの中身が表示されているかどうかをテスト。following_test.rbtest "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end
2. リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。
→ うわ、テストしたらえげつない量のエラー文出てきた。sorryで検索すると、「Your words made sense, but your sarcastic tone did not.」=「あなたの言葉は理にかなっていますが、あなたの皮肉な口調はそうではありませんでした。」 ジョークみたいなこと言われてますが、つまりエスケープしないと表示できないものがあるよってことでしょうか。
第14章まとめ
・モデルにhas_manyを設定してテーブルの要素を柔軟に名付けて扱える。
・ルーティングはネストして、リソースに新たなものを追加できる。
・jQueryを使ってフォームにAjaxを採用。
・必要に応じてSQLも使える。
・チュートリアルはチュートリアルにすぎない。やっとスタートラインに立てたぐらいだと心得よ(自分への戒め)。
2周半完走です!!ありやした!!!ちょうど9月1日から始めて、今日が25日。なんとか目標としていた1ヶ月以内に終わりました。チュートリアルで学んだ内容はだいぶ頭に入ったと思いますが、つどつど疑問点を調べていると、まだまだ知らないこと99%あるんじゃないかって感じます。ゆえにまとめの最後の一文というわけです。というかまだスタートラインに立つためにアップしてるぐらいの感覚です。オレはようやくのぼりはじめたばかりだからな、このはてしなく遠い男坂をよ…
さて、 Railsチュートリアルについては一旦ここで終了です。次のステージはスクールに通うことです。ここまでやってきて、正直スクールうんぬんよりも、いかに自分で調べられるか、自走力・独学力が大事だなあと実感しました。が、記事タイトルどおり、自分には悠長なことを言っていられる時間はないのです。世の中に挑んでいくために、ぎゅっと集中して背水の陣で学習に臨む必要があるのです。
というわけで次の一手は、スクールを利用しての学習です。どっちにしろ自分で調べて考えてコードを書いていくことに変わりはないと思いますが、講師に自分のコードをみてもらい必要なことを教えてもらえるのはきっと価値があるし、時間短縮に繋がると考えています。金にモノをいわせる大人のやり方です。たいむいずまねーです。とかグダグダ書いてるヒマがあったら勉強しろって話!それではまた!
記事書くのは好きなので、いつか有益な記事が書けるように精進します!!
⇦ 第13章はこちら
学習にあたっての前提・著者ステータスはこちら
なんとなくイメージを掴む用語集
・Ajax
Asynchronous JavaScript + XML の略。ページを移動する・読み込み直すといったことをせずに、ページの内容をいろいろ動かしたりする技術。・非同期
データを転送する際に、送信側と受信側のタイミングの一致(同期)を気にせずにデータをやり取りすること。・XML(Extensible Markup Language)
拡張できるマークアップ言語。書き方のルールの一つ。主にデータのやりとりや管理を簡単にする目的で使われる。・DOM(Document Object Model)
ホバリングしている黒いやつじゃないよ。プログラムからHTML等のWebページを自由に操作するための仕組みで、階層構造(ツリー構造)をとる。
- 投稿日:2020-09-25T19:38:44+09:00
【Rails】devise, devise_token_authで、ユーザー作成ログインの初期設定
新規ユーザーモデルを作成する時
deviseの設定
$ rails g devise:install create config/initializers/devise.rb create config/locales/devise.en.yml
routes.rbRails.application.routes.draw do devise_for :users, :controllers => { :registrations => 'users/registrations', :sessions => 'users/sessions' } devise_scope :user do get "sign_in", :to => "users/sessions#new" get "sign_out", :to => "users/sessions#destroy" end end
devise_for :モデル名
認証に必要なルーティングを自動で設定devise_token_authの設定
$ rails g devise_token_auth:install User auth create config/initializers/devise_token_auth.rb insert app/controllers/application_controller.rb gsub config/routes.rb create db/migrate/20200919181950_devise_token_auth_create_users.rb create app/models/user.rb
$ rails db:migrate
User...
モデル名
auth...
認証ルーティングをマウントするパスroutes.rbRails.application.routes.draw do mount_devise_token_auth_for 'User', controllers: { registrations: 'users' } enddeviseコントローラ
$ rails g devise:controllers users create app/controllers/users/confirmations_controller.rb create app/controllers/users/passwords_controller.rb create app/controllers/users/registrations_controller.rb create app/controllers/users/sessions_controller.rb create app/controllers/users/unlocks_controller.rb create app/controllers/users/omniauth_callbacks_controller.rbすでにUserモデルある場合
マイグレーション内容をいじる
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.0] def change change_table(:users) do |t| ## Required t.string :provider, :null => false, :default => "email" t.string :uid, :null => false, :default => "" ## Database authenticatable t.string :encrypted_password, :null => false, :default => "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at t.boolean :allow_password_change, :default => false ## Rememberable t.datetime :remember_created_at ## Trackable t.integer :sign_in_count, default: 0, null: false t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip ## Confirmable t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at ## User Info # t.string :name t.string :nickname t.string :image # t.string :email ## Tokens t.text :tokens # t.timestamps end add_index :users, :email, unique: true add_index :users, [:uid, :provider], unique: true add_index :users, :reset_password_token, unique: true add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end end注意すべきことは、2つ
change_table(:users)
にすること。(元は、create_table
)- 既存のカラムはコメントアウトすること
- 投稿日:2020-09-25T18:14:16+09:00
DBから特定のカラムの数値を取り出したい(自分メモ)
Aテーブル:user
Bテーブル:user_info目的:
Bテーブルの特定カラム(ここではuser_main_id)の数値を返したいやり方:
用意されているのは、Aテーブルの@user_sessionのみ。
- Aテーブルの@user_sessionの中のidを探し出す。
- Aテーブルのidと、Bテーブルのuser_idの数値を一致させる
- Bテーブルの中でidで一致した後、目的のカラム(user_main_id)を取得する
RubyAPI表示名: user.find_by(user_id: @user_session.record.id).user_main_id@user_sessionの中身が何かが重要!
参考URL
https://qiita.com/tsuchinoko_run/items/f3926caaec461cfa1ca3
- 投稿日:2020-09-25T16:23:07+09:00
form_withによるname属性とid属性の自動付与
動作環境
Ruby 2.6.5
Rails 6.0.3.2form_withを使った場合、name属性とid属性が自動で付与されていたことに気づかずにエラーをよく起こしていたので、投稿してみました。
form_withにmodelを指定し、name属性とid属性が自動で付与される具体例
new.html.erb<%= form_with model: @hoge, local: true do |f| %> <%= f.text_field :fuga %> <%= f.submit "投稿する" %> <% end %>上記のように、modelを@hogeに指定し、検証ツールにてname属性とid属性を確認すると、name="hoge[fuga]"、id="hoge_fuga"となります。nameとidを指定していないのに、自動でname属性とid属性が付与されています。
先ほどのコードでは、name属性とid属性を指定しない場合は自動で付与されるという話でしたが、name属性とid属性を指定した場合はどうなるのかを見ていきましょう。
form_withを使いname属性とid属性を指定した具体例
new.html.erb<%= form_with model: @hoge, local: true do |f| %> <%= f.text_field :fuga, name:"hogera", id:"piyo" %> <%= f.submit "投稿する" %> <% end %>先ほどと同様に、検証ツールにてname属性とid属性を確認すると、name="hogera"、id="piyo"となります。つまり、name属性とid属性は指定した通りになります。
次に、modelを指定するのではなく、urlを指定した場合はどうなるのかを見ていきましょう。
form_withにurlを指定し、name属性とid属性が自動で付与される具体例
new.html.erb<%= form_with url: hoges_path, local: true do |f| %> <%= f.text_field :fuga %> <%= f.submit "投稿する" %> <% end %>先ほどと同様に、検証ツールにてname属性とid属性を確認すると、name="fuga"、id="fuga"となります。ここで気を付けたいのが、modelを指定した場合とname属性とid属性が異なっていることです。基本的に、controllerで行いたいactionが同じの場合、modelを指定してもurlを指定しても結果は変わらないのですが、name属性とid属性は異なります。
ちなみに、modelとurlの両方を指定すると、name属性とid属性はmodelを指定した場合と同じになります。
私はname属性とid属性は指定しないと付与されないものだと思っていたので、このことを知ったときには驚きました。また、JavaScriptを用いてname属性を取得することがあったのですが、その際に、modelを指定した時とurlを指定した時のname属性の違いに気づかずに何時間もエラーと闘ってしまうということがありました。
こういったエラーを防ぐために最初からname属性もid属性も指定してあげれば良いのでは?と考えたのですが、そもそも少しでも記述量を減らすために自動で付与しているので、自分で指定せずに自動で付与されたものを使うことが多いようです。
- 投稿日:2020-09-25T15:42:47+09:00
[Rails]simple_calendarの導入方法
はじめに
アプリ開発でカレンダーを用いた実装をしたかったので簡単にまとめました。
simple_calendarとは
simple_calendarとは、簡単にカレンダー機能を付け加えれるgemです。
月間カレンダー、週間カレンダーなど日付指定をしてカレンダーを作成することができます。
今回は月間カレンダーを用いた方法となっております。目次
- simple_calendarのインストール
- simple_calendarのビューの生成
- カレンダーを表示
- カレンダーのレイアウトの変更
- おまけ
1. simple_calendarのインストール
gemファイルに以下を追記し、アプリケーションのディレクトリで「bundle install」を実行します。
gem.filegem "simple_calendar", "~> 2.0"2. simple_calendarのビューの生成
simple_calendarのビューファイルを生成するために、以下のコマンドを実行します。
レイアウトをカスタマイズしたいときはこのコマンドでファイルを生成することで編集ができるようになります。ターミナル
rails g simple_calendar:views3. カレンダーを表示
モデルの編集
カレンダーにイベントを表示させるために以下をモデルに追記します。
「date」の部分はカラム名を記述します。model.rbdef start_time self.date endビューファイルの編集
カレンダーを表示させるために、以下を記述します。
「events」の部分は、コントローラーで設定したインスタンス変数を置くことでデータを引っ張ってきます。
これでカレンダー上にイベントを表示させることができます。view.html.erb<%= month_calendar events: @all do |date, all| %> <%= date.day %> //カレンダー上の日程の表示の仕方 <% all.each do |i| %> <div> <%= i.price %> </div> <% end %> <% end %><%= date %>だと2020-01-01のように出力されます。
今回は日付だけを表示させたいので<%= date.day %>と記述しています。その他ファイルの編集
simple_calendarのCSSを適用させるために、application.cssに「*= require simple_calendar」を追記します。
/app/assets/stylesheets/application.css/* *= require simple_calendar #ここに追記します *= require_tree . *= require_self */
4. カレンダーのレイアウトの変更
以下のようにファイルを作成しCSSを自分で記述することで、カレンダーのデザインをカスタマイズすることができます。
/app/assets/stylesheets/_simple_calendar.scss.simple-calendar { .day {} .wday-0 {} .wday-1 {} .wday-2 {} .wday-3 {} .wday-4 {} .wday-5 {} .wday-6 {} .today {} .past {} .future {} .start-date {} .prev-month {} .next-month { } .current-month {} .has-events {} }5. おまけ
inputタグでカレンダーを使いたいときは以下のように 「f.date_field」で表示させることができます。
view.html.erb<%= form_with model: @income, local: true do |f| %> <%= f.date_field :date, id:"date" %> <% end %>参考リンク
https://qiita.com/isaatsu0131/items/ad1d0a6130fe4fd339d0
https://github.com/excid3/simple_calendar
- 投稿日:2020-09-25T14:35:50+09:00
【Rails6】Devise+SNS認証で登録&ログイン(複数連携可)
はじめに
複数のSNSと連携可能な認証をつくります。
Deviseは実装済みとして進めます。
各APIキーの取得等にも触れませんので適宜お調べ下さい。
今回はfacebookとtwitterで実装しますが他のSNSでも基本的には同じだと思います。環境
環境Windows10 ruby 2.6.6 Rails 6.0.3.1
下準備
gem追加
Gemfilegem 'omniauth' gem 'omniauth-facebook' gem 'omniauth-twitter'保存後bundle installして下さい。
credentials.ymlにAPIキーを記述
Windowsの方は前回の記事を参考にしてみてください。
credentials.ymlfacebook: api_key: pk_test_~ secret_key: sk_test_~ twitter: api_key: pk_test_~ secret_key: sk_test_~Deviseの設定
config/initializer内。
devise.rb#以下を追加 config.omniauth :facebook, Rails.application.credentials.facebook[:api_key], Rails.application.credentials.facebook[:secret_key], scope: 'email', info_fields: 'email,name' config.omniauth :twitter, Rails.application.credentials.twitter[:api_key], Rails.application.credentials.twitter[:secret_key], scope: 'email', callback_url: 'https://localhost:3000/users/auth/twitter/callback'twitterの方はコールバックURLを明示する必要があるみたいです。
Socialモデルを作成
今回はDeviseでUserモデルを作成済みとします。
Userモデルへカラムを追加してもいいですが、今回はUserモデルに紐づくSocialモデルを新たに作成します。
(UserモデルとSocialモデルは一対多の関係となります。)consolerails g model social
Socialモデルにはuser_id、provider("facebook"や"twitter"などが入る)、uid(各SNSアカウントと紐づけるためのidが入る)を持たせます。
2020~create_socials.rbclass CreateSocials < ActiveRecord::Migration[6.0] def change create_table :socials do |t| t.references :user, null: false, foreign_key: true t.string :provider, null: false t.string :uid, null: false t.timestamps end end end保存したらrails db:migrateする。
関連付け
user.rbhas_many :socials, dependent: :destroy#追加 devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable#追加social.rbbelongs_to :user validates :uid, uniqueness: {scope: :provider}#同一provider内で多重登録できないようにする同じSNSアカウントが複数のユーザーに紐付かないようにします。
「{scope: :provider}」は多分なくても平気だと思いますが念のため。実装
コールバック処理をつくる
routes.rbdevise_for :users, controllers: { sessions: 'users/sessions', password: 'users/password', registrations: 'users/registrations', omniauth_callbacks: 'users/omniauth_callbacks'#追加 }以下app/controllers/users/omniauth_callbacks_controller.rb内。
(rails routes等で確認し、この中のメソッドを各リダイレクト先に設定して下さい。)omniauth_callbacks_controller.rbclass Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def facebook if request.env['omniauth.auth'].info.email.blank?#Facebookでメール使用を許可しているか redirect_to '/users/auth/facebook?auth_type=rerequest&scope=email' end callback_from :facebook end def twitter callback_from :twitter end private def callback_from(provider) provider = provider.to_s if user_signed_in? @user = current_user User.attach_social(request.env['omniauth.auth'], @user.id)#後でattach_social作る。SNSからの情報とログイン中のUserのidを渡す。 else @user = User.find_omniauth(request.env['omniauth.auth'])#後でfind_omniauth作る。SNSからの情報を渡す。 end if @user.persisted?#登録済みor登録できたら flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize) sign_in_and_redirect @user, event: :authentication else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end end endリダイレクト元のSNSの種類を「callback_from」へ渡して切り分けます。
「callback_from」内では「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」か「ログイン中ユーザーのSNS連携」かを切り分けました。
「登録後」「ログイン後」「連携後」の処理は今回は分けずに、全てユーザーページにリダイレクトさせています。次にUser.rb内に各処理を書きます。
user.rbdef self.find_omniauth(auth)#SNS認証での新規登録またはsnsログイン social = Social.where(uid: auth.uid, provider: auth.provider).first unless social.blank?#sns認証済み(ログイン) user = User.find(social.user.id) else#sns認証での新規登録 temp_pass = Devise.friendly_token[0,20]#今回は取り敢えずランダムなパスワードを作ります user = User.create!( username: auth.info.name, email: auth.info.email, password: temp_pass, password_confirmation: temp_pass, ) social = Social.create!( user_id: user.id, provider: auth.provider, uid: auth.uid, ) end return user end def self.attach_social(auth, user_id)#sns連携追加時 social = Social.create!( user_id: user_id, provider: auth.provider, uid: auth.uid, ) end「SNS認証での新規ユーザー登録またはSNS認証済みユーザーのログイン」時はさらに「新規登録」か「ログイン」かを切り分けます。
「新規登録」時はUserとSocialを両方作成し(同時にUserとSocialの紐づけもする)、Userを返します。
「ログイン」時はSNSから送られてきた情報(auth.uidとauth.provider)からSocialを探し、紐づくUserを返します。
「ログイン中ユーザーのSNS連携」時はSocialのみ作成します。ログイン中のUserと紐づけるため、user_idを受け取ってます。一応これで処理は終わりです。あとは「user_facebook_omniauth_authorize_path」などでリンクを作成すれば、Userの状況に応じて新規登録やログイン、連携を行ってくれます。
おしまい
実際には連携したSNSへのリンクを作ったり、SNSでの新規登録時のパスワード再設定の仕組み、Deviseのメール認証、連携の解除ボタンなどもあったほうが良いかと思いますが、ひとまず最小構成で作ってみました。
Rails勉強中なのでスマートでない所が多々あるかと思います。もしおかしなことをしてたら教えて頂けると嬉しいです!
- 投稿日:2020-09-25T13:45:18+09:00
seed-fuを使ってseedデータを作成する方法
はじめに
railsにはデフォルトでseedが使えますが、初期データを入れる際にはseed-fuを使う方が便利なようです。
sedd-fuでは一部のレコードを更新したり、指定のモデルだけのデータを作成したりできます。
今回は、seed-fuの導入方法と基本的な使い方をご紹介します。seed fuをインストール
Gemfileに記載して、bundle installします。
Gemfilegem 'seed-fu'ターミナル$ bundle installディレクトリ構成
db/fixtures
は最低限必要なディレクトリなのでこちらは作成してください。
db/fixtures
に作成した seedファイルを置くこともできますが、db/fixtures/development
、db/fixtures/production
のようにseedファイルを作成すると環境ごとに異なるデータを作成することもできます。また、
db/fixtures
以下のseedファイルはアルファベット順に読み込まれます。
以下のようにファイル名に数字を入れると読み込む順番を指定することが可能になります。db
├ fixtures
├ 01_group.rb
├ 02_user.rb
├ 03_tweet.rb例として
02_user.rb
を記載します。02_user.rb10.times do |n| User.seed do |s| s.name = "name-#{n}" s.email = "user-#{n}@test.com" end endrails db:seedでseed fuが使えるようになる設定
rails db:seed_fu
で全てのデータを順に作成することができますが、
下記のようにseeds.rbに一文書くことで、通常のseedのようにデータを作成することもできます。
seed-fuを知らない人でもデフォルトのseedと同じようにデータを作成できるので設定しておくと良いと思います。seeds.rb# rails db:seed で Seed Fu 呼び出せるように設定 SeedFu.seedファイルを指定してデータを作成する場合のコマンド
下記のコマンドでモデルを指定してデータを作成可能です。
ターミナルrake db:seed_fu FILTER=01_group, 02_user # 複数指定する場合はカンマ区切り終わりに
seed-fuではいろいろと便利な機能があるので、詳細は下記を参考にしてください。
railsで初期データを入れる(seed-fuの使い方)
seedデータってどうやって入れる?seed-fuを使った便利なSeedデータ挿入法
- 投稿日:2020-09-25T11:17:26+09:00
FizzBuzz問題を解いてみよう!
【概要】
1.結論
2.FizzBuzz問題とは何か
3.どのようにプログラムするのか
4.ここから学んだこと
1.結論
eachメソッドとif..elsifを組み合わせよう!
2.FizzBuzz問題とは何か
結論としては、コードが書けないプログラマー志願者を分けるために作られたプログラムです。
具体例としては、
(i)1~100の数字を出力する際に、
(ii)3の倍数は”Fizz"
(iii)5の倍数は"Buzz"
(iv)15の倍数は"FizzBuzz"
を表示させるというプログラムです!
基本的なことが理解できているかを試せる問題として有名です!
3.どのようにプログラムするのか
今回は使用する言語は"Ruby"になります。
def fizz_buzz num = 1 #---❶ (1..100).each do |i| #---❷ if num % 15 == 0 #---❸ puts "FizzBuzz" elsif num % 3 == 0 puts "Fizz" elsif num % 5 == 0 puts "Buzz" else puts num #---❹ end num = num + 1 #---❺ end end fizz_buzz❶まずここでnumに"1"を代入しないとどの変数のどの値からか、わかりません。今回は1~100なので"1"を代入しています。
❷"2.FizzBuzz問題とは何か"で説明した(i)を満たすには、繰り返し処理+条件が必要です。ここは"4.ここから学んだこと"に後述しますが、each以外のメソッドでもコーディングできます。
❸"2.FizzBuzz問題とは何か"で説明した(ii)~(iv)を満たすための条件式です。倍数の場合分けは、剰余演算子を利用して3/5/15の倍数を判別しています。注意としては3の倍数の条件式からコーディングしてしまうと、"15"の数字が出た際に"3の倍数"の条件で認識されてしまいます。プログラムは上から下に読み込まれるのが基本なので、一度条件に当てはまると以降のプログラムは無視されます。
剰余の書き方については自分の記事でも紹介しているので、探す手間が省けます!
剰余とべき乗(冪乗)の演算子❹"2.FizzBuzz問題とは何か"で説明した(i)で"1~100"を表示させるので倍数以外の条件も出力します。なので条件以外数字はそのままの数字を出力するようにしています。
❺これがないと、”1”以降の数字を生み出せません。eachに1~100と書いてあっても"1"を100回出力する繰り返し処理になってしまいます。"num += 1" でもOKです。
4.ここから学んだこと
❷の部分は
繰り返し処理メソッドと条件の式(100以下)を組み合わせればいけるのではと思いwhileメソッド使ったところ動きました。while num <= 100 doいろいろ検索していたところ、
繰り返し処理は下記URLのようにたくさんの方法がありました!自分が知っていたのはtimes/each/whileメソッドでした。また条件をどう組み込むかもメソッドによって違います!参考にしたURL:
while文
いろいろな方法で1から100までを出力する
- 投稿日:2020-09-25T11:02:55+09:00
Railsプロジェクトを作成したあとにActiveRecordを使わないようにする
rails new
するときに--skip-active-record
オプションを付けなかった場合から付けた場合への差分です。Railsプロジェクトを作成したあとに、DBを使わなくなった場合などの参考に。
Railsバージョン6.0.0.3
.gitignore- # Ignore the default SQLite database. - /db/*.sqlite3 - /db/*.sqlite3-journal - /db/*.sqlite3-* - # Ignore uploaded files in development. - /storage/* - !/storage/.keepGemfile- # Use sqlite3 as the database for Active Record - gem 'sqlite3', '~> 1.4' - # Use Active Storage variant - # gem 'image_processing', '~> 1.2'Gemfile.lock省略
app/javascript/packs/application.js- require("@rails/activestorage").start()
app/models/application_record.rb削除
bin/setup- # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' - # end - puts "\n== Preparing database ==" - system! 'bin/rails db:prepare'config/application.rb- require 'rails/all' + require "rails" + # Pick the frameworks you want: + require "active_model/railtie" + require "active_job/railtie" + # require "active_record/railtie" + # require "active_storage/engine" + require "action_controller/railtie" + require "action_mailer/railtie" + # require "action_mailbox/engine" + # require "action_text/engine" + require "action_view/railtie" + require "action_cable/engine" + require "sprockets/railtie" + require "rails/test_unit/railtie"config/database.yml削除
config/environments/development.rb- # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load - # Highlight code that triggered database queries in logs. - config.active_record.verbose_query_logs = trueconfig/environments/production.rb- # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = falseconfig/environments/test.rb- # Store uploaded files on the local file system in a temporary directory. - config.active_storage.service = :test
config/initializers/wrap_parameters.rb- # To enable root element in JSON for ActiveRecord objects. - # ActiveSupport.on_load(:active_record) do - # self.include_root_in_json = true - # end
config/storage.yml削除
db/seeds.rb削除
package.json- "@rails/activestorage": "^6.0.0",
test/test_helper.rb- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all
実際にrails newしたものの差分です。
https://github.com/hid3h/diff-rails-active-record/pull/1/commits/725b4147da5b94db4d5f5ded0c7bf5c27c3d4489
- 投稿日:2020-09-25T10:58:00+09:00
【rails】enumのレコード登録数をバリデーションで制限する方法
はじめに
開発を行っているとenumを使って、データを分ける場合があると思います。
enumで分けたデータに対してそれぞれレコードの登録数を制限する実装をしたのでメモとして残しておきます。開発環境
Rails 6.0.3
Ruby 2.7.1テーブル
今回は下記のようなUsersテーブルを想定しています。
性別(sex)はenumでman/femaleのどちらかしか選択できないようにしています。
name sex(enum) ユーザー1 man ユーザー2 female レコード数を制限する方法
models/user.rbにメソッドを作成し、man/femaleそれぞれのレコード数を確認して、もし制限数(10件)を超えていればエラーを追加します。
on: :create
を記載しないと毎回バリデーションが適用され、編集できなくなってしまうので忘れないよう!
(私はここに気づかず編集の際にエラーが発生してしまいました。)user.rbclass User < ApplicationRecord REGISTER_LIMIT_COUNT = 10 # 定数として登録数を管理 enum sex: { man: 0, female: 1 } validate :limit_user_register_count, on: :create # 作成したメソッドでバリデーション def limit_user_register_count if self.sex == "man" && man.count >= REGISTER_LIMIT_COUNT # enumがman かつ manのレコード数が制限を超えている errors.add(:user, "man count is over") # エラーを追加 elsif self.sex == "female" && female.count >= REGISTER_LIMIT_COUNT errors.add(:user, "female count is over") end end endRspecでバリデーションを確認する方法
user_spec.rbrequire 'rails_helper' RSpec.describe Guide, type: :model do context 'データを11個以上登録する場合' do before { create_list(:user, 10, sex: 'man') } # manのデータをを10人分作成 it 'エラーメッセージを返すこと' do man = build(:user, sex: 'man') # 11人目をbuildする(バリデーションがかかるのでcreateは不可) man.valid? # validすることでcreateしようとしてvalidationが走り、エラーが追加される expect(man.errors[:user]).to eq('man count is over') end end endfactories/users.rbFactoryBot.define do factory :user do name { Faker::Name.name } sex { 0 } end end
- 投稿日:2020-09-25T08:35:57+09:00
【Rails】on: :actionの条件付きで設定したコールバックはskip_callback、set_callbackを用いて回避することができない
skip_callbackとset_callbackを用いて一時的にコールバックを回避しようとしたらset_callbackがうまくいかなかった。その時の状況と回避策を記録。
やりたいこと
こちらのコールバックを一時的にスキップしたい。
ruby
before_validation :func, on: :action
最初にやったこと
skip_callback
、set_callback
を用いて実装した。skip_callback(:validation, :before, :func) ------------------------------------------ # 処理を実行 ------------------------------------------ set_callback(:validation, :before, :func)しかし、このあと他の動作に不具合が出てしまい、原因を調査した。
skip_callback、set_callbackとは
skip_callback
は設定されているコールバックをスキップさせることができる。ただし、一度実行するとずっとskipしたままになってしまうため、set_callback
を用いて戻す必要がある。うまくいかない原因
on: :action
の再設定ができていないため。つまり、以下のようにコールバックの設定が変わってしまっていたため、挙動が変わってしまった。
# スキップ前 before_validation :func, on: :action # スキップ後 before_validation :func対処
最初に行った
set_callback
は、on: :action
に関して特に指定をしていない。
ruby
set_callback(:validation, :before, :func)
set_callbackでは、
on: action
の設定をしておらず、on: action
の設定が無くなってしまっていた。
on: :action
の設定をしなければいけないため、onで設定している場合はその設定も引数に入れなければならないらしい。set_callback(:validation, :before, :func, on: :action)しかし、こちらをやってみても改善されず。
ソースコードにコメントしてある引数には
if
,unless
,prepend
しか無いためon: :action
の設定は難しそう。最終的な対処法としては、コールバックを回避して処理を行うメソッドが存在するため、こちらを用いることにした。
https://railsguides.jp/active_record_callbacks.html#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%82%92%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97%E3%81%99%E3%82%8Bまとめ
skip_callback
、set_callback
はコールバックを回避するための便利なメソッドですが、on
を用いた条件付けがされている場合かつ全てのコールバックを回避しても問題ない場合はそもそもコールバックを回避できるメソッドを使った方が良いのかなと今のところ考えています。
- 投稿日:2020-09-25T07:59:23+09:00
【Rails】ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきこと
はじめに
以下のQiita記事を拝見しました。
エラーが解決しない時は、元を断つという考え方 - Qiita
詳しくは上記記事を読んでもらいたいのですが、簡単に要約すると「ローカル環境のデータがおかしくなったら、(データを消しても問題ないことを確認したうえで)データベースを再作成しよう」という内容です。
ただ、個人的には「うーん、あまりよくないな」と思ってしまったので、返信コメントを書こうと思いました。
コメントを書き始めたらかなり長くなってしまったので、独立した記事にしてしまおうと思いました。
それが本記事になります。というわけで、ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきことを以下にまとめます。
ローカル環境のデータがおかしいから全部リセットしたい!と思うその前にすべきこと
(注:以下の内容はもともと冒頭に載せたQiita記事への返信コメントとして書いたものです。その前提で読み進めてください)
データベースのリセットは「データを全部消しても問題ない」というときは有効ですが、そうでないときは使えません。
個人の趣味で開発しているRailsアプリなら良いかもしれませんが、仕事で開発するRailsアプリはローカル環境といえど、「今までよく使ってきたテストデータ」がたくさん蓄積されているので、往々にして「これを全部消すわけにはいかない」ということになります。
また、開発環境でなく、本番環境で同じ問題が起きたときは「データを全部消す」という作戦は、まず使えません。さらに言うと、データを全部消してしまうとなぜエラーを引き起こすデータが発生したのか、という原因調査もしづらくなります。
上記のような理由から、「データを全部消す」というのは本当に最後の手段として残しておき、データを消す前にできる限りの調査と対処を実施すべきです。
僕であれば以下のような対処法をとります。
不具合の原因を調査し、データ異常かそうでないかを見極める
まず、不具合の原因を見極めます。
今回の記事であれば、post.user
がnil
になっているのが原因のようです。次に、その状態が妥当かどうかを判断します。
post.user
がnil
ということは、「投稿に紐付く投稿者がいない」という状態です。
これは常識的に考えると、まずありえない状況(=データの異常)だと思います。データ異常が発生した原因を突き止め、必要に応じてコードを修正する(再発防止)
それから、なぜ「投稿者がいない投稿が生まれてしまったのか?」という原因を考えます。
その原因を突き止めないと、もしデータベースを再作成してエラーを出なくしても、しばらくするとまた「投稿者がいない投稿」がひょっこり現れるかもしれません。ふつうに画面を操作していて投稿者無しで投稿できる仕組みになっているのであれば、それは不具合ですので、不具合が再発しないよう、必ず投稿者が設定される仕組みにコードを修正しましょう。
以下に想定できそうな原因と対処法を載せます。
optional: true
が付いていた場合最近のRailsだと
belongs_to
で定義した関連レコードはデフォルトで必須になっているので、可能性は低いと思いますが、もしoptional: true
が付いていたらこれを外します。こうすれば、user
がnil
の状態でPostを保存しようとするとバリデーションエラーが発生します。app/models/post.rbclass Post < ApplicationRecord - belongs_to :user, optional: true + belongs_to :user endあとからpostsテーブルに
user_id
列を追加した場合もしくは、最初にPostモデルが作成され、その後時間を空けて別のmigrationで
user_id
がpostsテーブルに追加されたのかもしれません。
この場合はmigration実行後に、既存のPostレコードに対して何らかのUserレコードを紐付けて保存する対応(既存データの修正)が必要になります。
この対応を忘れていると、「投稿者がいない投稿」が生まれてしまいます。Userレコードが削除されていた場合
もしかすると一番ありえるのは、「Postレコードを作成したあとに、投稿者のUserレコードを削除してしまったこと」が原因かもしれません。
この場合は、
- 関連するPostレコードを持つUserレコードは削除できないようにする
- または、Userレコードを削除したら関連するPostレコードも一緒に削除する
- または、投稿者がいない投稿を正常なデータとして受け入れた上で、「退会済みユーザー」のような表示を画面に出す
といった対処方法が考えられます。
最初の2つについてはRailsのdependent
オプションで設定可能です。
詳しくは以下の記事をご覧ください。dependent: :restrict_with_error と :restrict_with_exception の違い - Qiita
ぱっと思いつく原因はこれぐらいですが、他にも「これが原因だった」という原因(=不具合)を見つけたら、今後二度と「投稿者がいない投稿」が生まれないようにプログラムを修正します。
再発防止策がとれたら、既存の異常データを修正する
おかしなデータが生まれない状況を作れたら、今度は既存の異常データを修正します。
たとえば、以下のようなスクリプトを実行すれば、「投稿者がいない投稿」に対して投稿者を設定できます。(あくまで例ですので、適宜スクリプトを修正してください)user = User.first Post.all.each do |post| if post.user.nil? post.user = user post.save! end endもしくは、「投稿者がいない投稿」を削除する、というデータ修正も選択肢のひとつになります。
Post.all.each do |post| if post.user.nil? post.destroy! end endこうすれば「すべての投稿に投稿者が紐付いている状態」になるので、
post.user.image_name
を呼びだしてもエラーは出なくなるはずです。データ異常ではなかった場合は、コードを適切に修正する
もし仮に「投稿に紐付く投稿者がいない」という状況が不具合ではない(仕様として妥当である)場合は、
post.user
がnil
かどうかでviewの処理を分ける必要があります。<% if post.user %> <%# 投稿者がいる場合の処理 %> <% else %> <%# 投稿者がいない場合の処理 %> <% end %>まとめ
このように対処すれば、「データを全部消す」という手段をとらずに済みます。
というか、「ローカル環境で個人的な趣味で作ってるだけです」という場合を除き、通常はこのような対応を取るのが適切だろうと僕は考えます。
- 投稿日:2020-09-25T03:16:55+09:00
【Rails】開始時刻と終了時刻を保存する
開始時刻と終了時刻を保存する
railsで、タスクの開始時刻と終了時刻を保存するプログラムを作ります。
自己流の部分が多いので、あくまでやり方の1つとして参考にしてくださると幸いです。
※今回は、開始、終了時刻を保存することをメインに扱います。削除、編集機能や見た目は勘弁してください。考え方
開始、終了フォームを1度だけしか押せない仕組みを目指します。
具体的には、タスクの開始時刻のデータが入っている場合は開始ボタンを表示しないように、ビューの中にif文を書きます。
この考えに至った経緯は、以下の通りです。・タスク(Task)に、開始時刻(StartTime)と終了時刻(StopTime)を紐付ける
→問題点:何回でも開始し、終了できてしまう。
・StartTimeのデータが入っている時(開始している時)、ボタンを表示させないようにする。
・StopTimeも上と同様にする。準備
ターミナル
rails new _6.0.0_ アプリ名 -d mysqlcd アプリ名bundle installモデルの作成
rails g model task rails g model start_time rails g model stop_timeモデルの修正
間違いがありましたら、ご指摘お願いします。
php;task.rbclass Task < ApplicationRecord has_one :start_time has_one :stop_time endphp;stop_time.rbclass StopTime < ApplicationRecord belongs_to :task endphp;start_time.rbclass StartTime < ApplicationRecord belongs_to :task endマイグレーションファイル
class CreateTasks < ActiveRecord::Migration[6.0] def change create_table :tasks do |t| t.string :title t.timestamps end end endphp;202..._create_start_times.rbclass CreateStartTimes < ActiveRecord::Migration[6.0] def change create_table :start_times do |t| t.references :task, foreign_key: true t.timestamps end end endphp;202..._create_stop_times.rbclass CreateStopTimes < ActiveRecord::Migration[6.0] def change create_table :stop_times do |t| t.references :task, foreign_key: true t.timestamps end end endターミナル
rails db:createrails db:migrateルーティング
Rails.application.routes.draw do root "tasks#index" resources :tasks, only: [:index, :create] do resources :start_times, only: :create resources :stop_times, only: :create end end使うものしか指定していません。
また、今回は、ネストしていますが、実はしなくても作れます。コントローラー
ターミナル
rails g controller tasks rails g controller start_times rails g controller stop_timesphp;start_times_controller.rbclass StartTimesController < ApplicationController def create StartTime.create(start_time_params) redirect_to root_path end private def start_time_params params.permit(:task_id) end endphp;stop_times_controller.rbclass StopTimesController < ApplicationController def create StopTime.create(stop_time_params) redirect_to root_path end private def stop_time_params params.permit(:task_id) end endphp;tasks_controller.rbclass TasksController < ApplicationController def index @tasks = Task.all end def create task = Task.create(task_params) redirect_to root_path end private def task_params params.permit(:title) end endビュー
urlは、rails routesで確かめて記入してください。
php;views/tasks/index.html.haml-# タスク保存フォーム = form_with url: tasks_path, method: :post do |f| = f.text_field :title = f.submit "send" -# タスク一覧表示 - @tasks.each do |task| %br タイトル = task.title -# 時間を確認したい場合は記載してください。 - if task.start_time != nil %br 開始時刻 = task.start_time.created_at %br - if task.stop_time != nil 終了時刻 = task.stop_time.created_at %br %br -# ここまで - if task.start_time == nil = form_with url: task_start_times_path(task_id: task.id), method: :post do |f| = f.submit "開始" %br - if task.start_time != nil && task.stop_time == nil = form_with url: task_stop_times_path(task_id: task.id), method: :post do |f| = f.submit "終了" %br今回は、new.html.hamlを全く作っていません。
しかしform_withで、createアクションに対応するurlを指定し、method: :postをつけることで直接保存できるようになります。
うまくいかない場合は、binding.pryなどを使い、送っている値、受け取っている値を確認すると良いと思います。まとめ
力技で作った感じがするので、もっと良い方法がある場合は、ぜひコメントしてください!
- 投稿日:2020-09-25T02:33:06+09:00
エラーが解決しない時は、元を断つという考え方
どうも、三町哲平です。
プログラミングをしていたら必ずエラーに出会す場面が出てきます。
簡単に解決できるエラーもあれば、何時間、下手したら数日掛かっても解決しないそんなやる気すら失ってしまうエラーも存在します。そういうやる気を失ってしまう時に役立つエラーに使えるかも知れないテクニックを一つ紹介します。
尚、今回はRuby on Railsで発生したエラーですが、データベース絡みのエラーだとRails問わず、どのプログラミング言語での開発環境においても効果的です。今回は、エラー発生から3日掛かって解決しましたので、短くわかりやすくまとめてみました。
まず、結論
この記事の概略を説明すると、
データベースが元の異常ぽかったら、データベースを再作成した方が早いよ。
ていう話。では、3日間の闘いをどうぞ!
1日目. Template::Error
undefined method `image_name' for nil:NilClass上記の文をピックアップしてfor nil:NilClassの部分を無視して考えた時に
image_nameというメソッドが定義されていないという意味です。1. メソッドの定義忘れ
2. シンプルに誤字脱字この辺りを疑い色々試してみましたが、
メソッドの定義悪でも、誤字脱字でもない...ググってみても正解には辿り着けない...
とりあえず初日は疲れて寝てしまいました。
2日目. データベースっぽい
誤字脱字がないか再調査したが、問題なし。
ここで余り気に留めていなかった
for nil:NilClassの部分についてググってみるようになります。
ちなみにRubyを使ったことない方は、
nilって何?となるかもしれませんが、nilは、nullと同じ意味です。
つまり、「何もない」ということです。何もないという内容で思い付くのは1つ...
データベース絡みっぽいなということにそして気付けば深夜1時、またすっかり日付が変わっていました。
3日目.ググり続けた結果
Ruby on Rails 5 - undefined method `image_name' for nil:NilClass といエラーがでています|teratail
上記のサイトに辿り着きました。
ここからは、引用が続きますが、このやり取りが全てです。
文字多いし小さしで読みたくない方に向けて結論だけ簡単に要約すると、
原因はおおよそ掴めたけど、もうデータベースを作り替えた方がいいよね。
という話。postsテーブルを作成してデータを複数入力
↓
nilの状態(何もない)のカラムを作成してはいけないルールに途中からプログラミングする
↓
新しいカラム(user_id)を作成する
↓
今までのuser_idカラムは全てnilの状態である
↓
エラー発生さいごに
原因はデータベースに入れてはいけないデータが含まれている。それならばそのデータを削除すれば良い。
もしくは開発環境のデータベースで消しても問題ないのならば今回のように再作成すればそれで復旧できます。仮にRuby on Railsの場合ですと、
Rails$ rails db:drop # データベースを削除する $ rails db:create # データベースを作成する $ rails db:migrate # データベースにマイグレートするこれと同じようにあなたの開発環境にあったソースコードを入力したら、良い訳ですね^^
- 投稿日:2020-09-25T00:32:26+09:00
DockerでRuby on Rails + Vue + MySQLの環境構築をする方法【2020/09最新版】
はじめに
フロントにVue、APIサーバにRailsという組み合わせは日本ではメジャーな構成の一つです。今回はこの構成をDockerを使って構築していきます。
この記事を読むことで、以下の構成をDocker作成出来るようになります!
API: Rails 6系
フロント:Vue(TypeScript) 2系
DB: MySQL 5.7環境構築の流れ
- Rails、VueのDockerfile作成
- docker-compose.yaml作成
- Rails、Vueのプロジェクト作成
最終的なディレクトリ構成は以下のようになります。
[project_name] ├── api // Rails │ ├── Dockerfile │ ├── ... ├── docker-compose.yaml └── front // Vue ├── Dockerfile ├── ...では環境構築に入っていきましょう!
1. ディレクトリ作成
プロジェクトディレクトリとその配下にAPI、フロントのディレクトリを作成します
$ mkdir project_name $ cd project_name $ mkdir api front以降のコマンドはカレントディレクトリがプロジェクトディレクトリ想定で記載しています。
2. Dockerfile作成
RailsとVueそれぞれのDockerfileを作成します。
2-1. Rails
指定しているバージョン
- Ruby 2.7.1
- Rails 6.0.xapi/DockerfileFROM ruby:2.7.1 RUN apt-get update -qq && \ apt-get install -y build-essential \ libpq-dev \ nodejs \ && rm -rf /var/lib/apt/lists/* RUN mkdir /app ENV APP_ROOT /app WORKDIR $APP_ROOT ADD ./Gemfile $APP_ROOT/Gemfile ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install
rm -rf /var/lib/apt/lists/*
はaptのキャッシュを削除しています。これはDockerのイメージファイルサイズを軽量化するためです。このDockerfileではRailsを含むGemfileを必要とするので、以下のファイルを
api/
に作成します。api/Gemfilesource 'https://rubygems.org' gem 'rails', '~> 6.0.3'空のGemfile.lockも作成します。
$ touch api/Gemfile.lockこの時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile <- New! │ ├── Gemfile <- New! │ └── Gemfile.lock <- New! └── front2-2. Vue
指定しているバージョン
- node 12.18.3
- Vue 2系 <- これは後述のコンテナ内でVueプロジェクトを作成する際に指定しますfront/DockerfileFROM node:12.18.3-alpine ENV APP_HOME /app RUN mkdir -p $APP_HOME WORKDIR $APP_HOME RUN apk update && npm install -g @vue/cliこの時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile │ ├── Gemfile │ └── Gemfile.lock └── front └── Dockerfile <- New!3. docker-compose.yaml作成
以下のdocker-compose.yamlをプロジェクトディレクトリに作成します
docker-compose.yamlversion: '3' services: web: build: ./api command: bundle exec rails s -p 3000 -b '0.0.0.0' ports: - '3000:3000' depends_on: - db volumes: - ./api:/app - bundle:/usr/local/bundle tty: true stdin_open: true db: image: mysql:5.7 volumes: - mysql_data:/var/lib/mysql/ environment: MYSQL_ROOT_PASSWORD: password ports: - '3306:3306' front: build: ./front volumes: - ./front:/app ports: - '8080:8080' tty: true stdin_open: true command: npm run serve volumes: mysql_data: bundle:MySQLとbundleのデータはボリュームにマウントし永続化しています。これによってコンテナを削除しても、データは消えません。
bundleはマウントしなくても、Gemを追加するたびにイメージのビルドすれば良いのですが時間がかかります。bundleをマウントすることでGemの追加がdocker-compose run api bundle install
で済むようにです。この時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile │ ├── Gemfile │ └── Gemfile.lock ├── docker-compose.yaml <- New! └── front └── Dockerfile4. プロジェクトの作成
4-1. Rails
Railsプロジェクトの作成
rails new
でRailsプロジェクトを作成します$ docker-compose run web rails new . --force --database=mysql --api --skip-bundle
rails new
の引数について
--force
:Gemfileを強制的に上書き更新する
--database
:使用するデータベースをMySQLにする
--api
:APIモードでプロジェクトを作成。APIモードではUIに関係するファイルが省略されます。
--skip-bundle
:bundle install
を省略します。次のdockerイメージのビルドでbundle install
をするためです。dockerイメージ更新
Gemfileが更新されたので、buildしてdocker imageを更新します。$ docker-compose build
DBの設定ファイルを修正
RailsのDB設定ファイル
api/config/database.yml
を修正します。api/config/database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root - password: + password: password - host: localhost + host: db
password
はdocker-compose.yamlの環境変数MYSQL_ROOT_PASSWORD
で指定したものhost
はDBのサービス名 に対応しています。DBの作成
$ docker-compose run web rails db:create
これでRailsの環境構築は完了です!
4-2. Vue
vue-cliでVueプロジェクトを作成
コンテナ内に入り、vue-cliを使ってVueプロジェクトの作成を対話的にします。以下の設定項目はvue-cli v4.5.6のものです。設定内容は一例なのでお好みでどうぞ。
Vueコンテナでシェルを実行
$ docker-compose run front sh
以下フロントコンテナ内で対話的に設定していきます。
$ vue create . # 現在のディレクトリ(/app)に作成するかの確認 ? Generate project in current directory? (Y/n) Yes # プリセットを使用するかどうか ? Please pick a preset: (Use arrow keys) Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) ❯ Manually select features # TypeScriptをインストールするためこちらを選択 # プロジェクトにインストールするライブラリの選択 ? Check the features needed for your project: ◉ Choose Vue version # ◉ Babel ❯◉ TypeScript # TypeScriptをインストールする場合はこれを選択 ◯ Progressive Web App (PWA) Support ◯ Router ◯ Vuex ◯ CSS Pre-processors ◉ Linter / Formatter ◯ Unit Testing ◯ E2E Testing # Vueバージョンの選択 ? Choose a version of Vue.js that you want to start the project with (Use arrow keys) ❯ 2.x 3.x (Preview) # Class styleを使用するかどうか。私はObject styleを使うため No ? Use class-style component syntax? (Y/n) No # TypeScriptと一緒にbabelを使うか ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Yes # LintとFormatterの設定に何を使うか ? Pick a linter / formatter config: ESLint with error prevention only ESLint + Airbnb config ESLint + Standard config ❯ ESLint + Prettier TSLint (deprecated) # Lintの実行タイミング ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection) ❯◉ Lint on save # 保存時にLintを実行 ◯ Lint and fix on commit (requires Git) # Babel, ESLintなどの設定をどこに記述するか ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) ❯ In dedicated config files # 各設定ファイルにする In package.json # 今回設定した内容をプリセットで保存するか。基本的にはプロジェクトを以降作成することはないため No ? Save this as a preset for future projects? No # パッケージマネージャーに何を使うか ? Pick the package manager to use when installing dependencies: (Use arrow keys) ❯ Use Yarn Use NPMインストール完了後、
Ctrl+D
でコンテナを停止します。5. 動作確認
コンテナを立ち上げる
$ docker-compose up -d
-d
: バックグラウンドでプロセスを実行する5-1. Rails
localhost:3000
にアクセスし以下のページが表示されることを確認
5-2. Vue
localhost:8080
にアクセスし以下のページが表示されることを確認
おわりに
お疲れさまでした。
今回はDockerでRuby on Rails + Vue + MySQLの環境構築する方法について書きました。VueとRailsはそこまで学習コストが高くないため、初心者にもオススメ出来る構成です。ぜひお試しください!
- 投稿日:2020-09-25T00:32:26+09:00
DockerでRails + Vue + MySQLの環境構築をする方法【2020/09最新版】
はじめに
フロントにVue、APIサーバにRailsという組み合わせは日本ではメジャーな構成の一つです。今回はこの構成をDockerを使って構築していきます。
この記事を読むことで、以下の構成をDocker作成出来るようになります!
API: Rails 6系
フロント:Vue(TypeScript) 2系
DB: MySQL 5.7環境構築の流れ
- Rails、VueのDockerfile作成
- docker-compose.yaml作成
- Rails、Vueのプロジェクト作成
最終的なディレクトリ構成は以下のようになります。
[project_name] ├── api // Rails │ ├── Dockerfile │ ├── ... ├── docker-compose.yaml └── front // Vue ├── Dockerfile ├── ...では環境構築に入っていきましょう!
1. ディレクトリ作成
プロジェクトディレクトリとその配下にAPI、フロントのディレクトリを作成します
$ mkdir project_name $ cd project_name $ mkdir api front以降のコマンドはカレントディレクトリがプロジェクトディレクトリ想定で記載しています。
2. Dockerfile作成
RailsとVueそれぞれのDockerfileを作成します。
2-1. Rails
指定しているバージョン
- Ruby 2.7.1
- Rails 6.0.xapi/DockerfileFROM ruby:2.7.1 RUN apt-get update -qq && \ apt-get install -y build-essential \ libpq-dev \ nodejs \ && rm -rf /var/lib/apt/lists/* RUN mkdir /app ENV APP_ROOT /app WORKDIR $APP_ROOT ADD ./Gemfile $APP_ROOT/Gemfile ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install
rm -rf /var/lib/apt/lists/*
はaptのキャッシュを削除しています。これはDockerのイメージファイルサイズを軽量化するためです。このDockerfileではRailsを含むGemfileを必要とするので、以下のファイルを
api/
に作成します。api/Gemfilesource 'https://rubygems.org' gem 'rails', '~> 6.0.3'空のGemfile.lockも作成します。
$ touch api/Gemfile.lockこの時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile <- New! │ ├── Gemfile <- New! │ └── Gemfile.lock <- New! └── front2-2. Vue
指定しているバージョン
- node 12.18.3
- Vue 2系 <- これは後述のコンテナ内でVueプロジェクトを作成する際に指定しますfront/DockerfileFROM node:12.18.3-alpine ENV APP_HOME /app RUN mkdir -p $APP_HOME WORKDIR $APP_HOME RUN apk update && npm install -g @vue/cliこの時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile │ ├── Gemfile │ └── Gemfile.lock └── front └── Dockerfile <- New!3. docker-compose.yaml作成
以下のdocker-compose.yamlをプロジェクトディレクトリに作成します
docker-compose.yamlversion: '3' services: web: build: ./api command: bundle exec rails s -p 3000 -b '0.0.0.0' ports: - '3000:3000' depends_on: - db volumes: - ./api:/app - bundle:/usr/local/bundle tty: true stdin_open: true db: image: mysql:5.7 volumes: - mysql_data:/var/lib/mysql/ environment: MYSQL_ROOT_PASSWORD: password ports: - '3306:3306' front: build: ./front volumes: - ./front:/app ports: - '8080:8080' tty: true stdin_open: true command: npm run serve volumes: mysql_data: bundle:Railsの開発サーバ起動時に
-b
でホストを指定します。省略するとホストからコンテナにアクセスできません。0.0.0.0
を指定することで、コンテナが持つ全てのインターフェースでlistenできるようになるため、ホスト側からコンテナにアクセスできるようになります。MySQLとbundleのデータはボリュームにマウントし永続化しています。これによってコンテナを削除しても、データは消えません。
bundleはマウントしなくても、Gemを追加するたびにイメージのビルドすれば良いのですが時間がかかります。bundleをマウントすることでGemの追加がdocker-compose run api bundle install
で済むようにです。この時点でのディレクトリ構成
[project_name] ├── api │ ├── Dockerfile │ ├── Gemfile │ └── Gemfile.lock ├── docker-compose.yaml <- New! └── front └── Dockerfile4. プロジェクトの作成
4-1. Rails
Railsプロジェクトの作成
rails new
でRailsプロジェクトを作成します$ docker-compose run web rails new . --force --database=mysql --api --skip-bundle
rails new
の引数について
--force
:Gemfileを強制的に上書き更新する
--database
:使用するデータベースをMySQLにする
--api
:APIモードでプロジェクトを作成。APIモードではUIに関係するファイルが省略されます。
--skip-bundle
:bundle install
を省略します。次のdockerイメージのビルドでbundle install
をするためです。dockerイメージ更新
Gemfileが更新されたので、buildしてdocker imageを更新します。$ docker-compose build
DBの設定ファイルを修正
RailsのDB設定ファイル
api/config/database.yml
を修正します。api/config/database.ymldefault: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root - password: + password: password - host: localhost + host: db
password
はdocker-compose.yamlの環境変数MYSQL_ROOT_PASSWORD
で指定したものhost
はDBのサービス名 に対応しています。DBの作成
$ docker-compose run web rails db:create
これでRailsの環境構築は完了です!
4-2. Vue
vue-cliでVueプロジェクトを作成
コンテナ内に入り、vue-cliを使ってVueプロジェクトの作成を対話的にします。以下の設定項目はvue-cli v4.5.6のものです。設定内容は一例なのでお好みでどうぞ。
Vueコンテナでシェルを実行
$ docker-compose run front sh
以下フロントコンテナ内で対話的に設定していきます。
$ vue create . # 現在のディレクトリ(/app)に作成するかの確認 ? Generate project in current directory? (Y/n) Yes # プリセットを使用するかどうか ? Please pick a preset: (Use arrow keys) Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) ❯ Manually select features # TypeScriptをインストールするためこちらを選択 # プロジェクトにインストールするライブラリの選択 ? Check the features needed for your project: ◉ Choose Vue version # ◉ Babel ❯◉ TypeScript # TypeScriptをインストールする場合はこれを選択 ◯ Progressive Web App (PWA) Support ◯ Router ◯ Vuex ◯ CSS Pre-processors ◉ Linter / Formatter ◯ Unit Testing ◯ E2E Testing # Vueバージョンの選択 ? Choose a version of Vue.js that you want to start the project with (Use arrow keys) ❯ 2.x 3.x (Preview) # Class styleを使用するかどうか。私はObject styleを使うため No ? Use class-style component syntax? (Y/n) No # TypeScriptと一緒にbabelを使うか ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Yes # LintとFormatterの設定に何を使うか ? Pick a linter / formatter config: ESLint with error prevention only ESLint + Airbnb config ESLint + Standard config ❯ ESLint + Prettier TSLint (deprecated) # Lintの実行タイミング ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection) ❯◉ Lint on save # 保存時にLintを実行 ◯ Lint and fix on commit (requires Git) # Babel, ESLintなどの設定をどこに記述するか ? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys) ❯ In dedicated config files # 各設定ファイルにする In package.json # 今回設定した内容をプリセットで保存するか。基本的にはプロジェクトを以降作成することはないため No ? Save this as a preset for future projects? No # パッケージマネージャーに何を使うか ? Pick the package manager to use when installing dependencies: (Use arrow keys) ❯ Use Yarn Use NPMインストール完了後、
Ctrl+D
でコンテナを停止します。5. 動作確認
コンテナを立ち上げる
$ docker-compose up -d
-d
: バックグラウンドでプロセスを実行する5-1. Rails
localhost:3000
にアクセスし以下のページが表示されることを確認
5-2. Vue
localhost:8080
にアクセスし以下のページが表示されることを確認
おわりに
お疲れさまでした。
今回はDockerでRuby on Rails + Vue + MySQLの環境構築する方法について書きました。VueとRailsはそこまで学習コストが高くないため、初心者にもオススメ出来る構成です。ぜひお試しください!
- 投稿日:2020-09-25T00:08:47+09:00
【Rails】ActionMailerを使用してSendGridのAPI経由でメール送信する場合の差出人名
概要
メールを送信するときに差出人名を表示させたいことがあると思います。検索すると、Action Mailerでfromフィールドに差出人名を表示したいに書いている通り、fromに差出人名を文字列で入れれば反映されるのですが、SendGridのAPI経由で送る場合はこの方法では反映されないです。というわけでどうすれば良いのかというのをメモ。
対応
SendGridでは専用のメールアドレス設定用クラス(Email)が用意されていて、Send sender name with email SendGrid - Railsにある通り、アドレスと差出人名を設定する仕様になっています。
実装サンプル
ActionMailerでのSendGrid・API使用について、Rails: SendGrid(Web API)とAction Mailerでメールを送信するの記事がわかりやすくまとまっているのでこちらを元にします。
設定箇所はlib/mail/send_grid.rb
のfromの部分になります。下記のような形で2つ目の引数に、差出人名を設定します。(差出人名は固定の前提)send_grid.rbfrom = SendGrid::Email.new(email: mail.from.first, name: '山田太郎')