20200216のRailsに関する記事は30件です。

Rails ビュー

ビューはレスポンスとして見た目を返すものになります。
コントローラーの次にビューが実行される。
app/views/コントローラー名ディレクトリにアクション名.html.erbというファイル名で作成される。
a.png

コントローラにインスタンス変数を定義

#app/controllers/posts_controller.rbに定義する場合

class PostsController < ApplicationController
  def index
    @post = "これはコントローラーで定義したインスタンス変数を確認するための文字列です"
  end
end

#ビューファイル(app/views/posts/index.html.erb)にインスタンス変数を定義

<h1>トップページ</h1>
<%= @post %>

これで以下のようになる。
a.png

なぜできる?

a.png

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

Rails コントローラーついて

ルーティングを設定したらコントローラーを設定します。
コントローラーはルーティングで振り分けられたリクエストを実際に処理する役割を持っています。
a.png

コントローラーの作成

$ rails g controller コントローラー名

postsコントローラーを作成する場合

$ rails g controller posts

#app/controllers/posts_controller.rbが作成されているか確認
コントローラー内にアクションを記載する
class PostsController < ApplicationController

end

#作ったばっかりでは何も記載はない

indexアクションを定義する場合↓

class PostsController < ApplicationController

  def index  # indexアクションを定義した
  end

end 

これでコントローラーの大まかな作業は完了です。続きはビュー編でお伝えします。

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

Rails ルーティング設定について

ルーティングとはどの要求を受けたら、どのコントローラを呼び出すかを決めている場所。

ルーティングの記載

【例】config/routes.rbに記載
以下はHTTPメソッドとリクエスト、そして行き先のコントローラーとアクションが明示されている形です。

HTTPメソッド
クライアントから送られたリクエスト(GET[ページを表示する操作]、POST[データを登録する操作]、PUT[データを変更する操作]、DELETE[データを削除する操作])のこと。
URIパターン

URLのようなもの。例えば、http://localhost:3000/posts
にアクセスする際はpostsのみを記載。

コントローラー

ルーティングの次にやってくる処理のこと。

アクション

コントローラー内における、処理のカテゴリの事。
ルーティングは「どのようなリクエスト」=>「どのようなコントローラー・アクション」をイメージ。
Rails.application.routes.draw do
  [HTTPメソッド] '[URIパターン]', to: '[コントローラー名]#[アクション名]'
end

#例として以下のように記載
Rails.application.routes.draw do
  root to: 'posts#index'
  get 'posts', to: 'posts#index'
  get 'posts/new', to: 'posts#new'
  post 'posts', to: 'posts#create'
end

#get 'posts', to: 'posts#index'を例にとる。
#リクエスト: GETのHTTPメソッド、URLはhttp://localhost:3000/posts
#行き先: postsコントローラーという名前のコントローラー、indexアクションという名前のアクション
resources メソッドを使用したバージョン

resourcesを使用すると7つのアクション(index[一覧表示ページを表示]、new[新規投稿ページを表示]、create[データの投稿を行う]、show[個別詳細ページを表示]、edit[投稿編集ページを表示]、update[データの編集を行う]、destroy[データの削除])全てを実装したことになる。

onlyオプション

指定したアクションのルーティングのみを設定できる。

Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts, only: [:index, :new, :create]
end

rails routesコマンド

ルーティングの確認ができる。
a.png

Prefixとは

a.png

Prefixを指定した場合はパスの書き方が変わります。詳しくはビューファイル設定の際に説明します。

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

Rails アプリ作成に向けての準備

実際にアプリケーションを作成するにあたっての説明です。

rails new コマンド

Railsで新規アプリケションを作成する際に使用します。

$ rails new アプリケーション名 -オプション名
# オプションを付けてアプリケーションを作成

例:sample_appleを作成する場合

# Railsのバージョン5.2.3を用いて、「sample_apple」を「-d」オプションでMySQLを指定して作成。
$ rails _5.2.3_ new sample_apple -d mysql

# 「sample_apple」ディレクトリに移動
$ cd sample_apple

# 現在のディレクトリのパスを表示
$ pwd

データベースを作成

a.png

rails db:create
を実行して新しくデータベースを作成します。
$ rails db:create  # データベースの作成
Created database 'sample_apple_development'
Created database 'sample_apple_test'

これで開発環境とテスト環境用のデータベースが作成完了。
確認はdatabase.ymlで確認できます。

データベースはSequel Proで管理する前提で説明します。
以下の設定でデータベース接続。

a.png

rails s

開発者のみがローカルでサーバーを立ち上げるためのコマンドです。
control + cでサーバーを切ることができる。

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

rubocopをbundlerでインストールするときにrequire: falseにする理由

本記事の対象者

「rubocopを導入するときにrequire: falseにしてたけど、理由がよく分からない」

そんな方に向けてこの記事を書いています。

rubocopの導入方法

rubocopの公式ドキュメントでは、以下のコマンドを実行する

$ gem install rubocop`

もしくは、Gemfileに以下を記載して、bundlerで導入してくださいと書いてあります。

$ gem 'rubocop', require: false`

この記事では、後者について記載していきます。

bundlerの特徴

rubocopを導入するうえで、覚えておきたいbundlerの特徴は、
「bundlerでは、Gemfileに書いたgemをまとめて自動でrequireする仕組みになっている」ということです。

rubocopはどこで使うの?

rubocopは、ソースコードが規約に沿っているか確認するために、ターミナル等でコマンドを実行します。

結論

rubocopは、ターミナル等で使用するため、bundlerによってアプリ側に自動で読み込む必要がない。よって、require: falseにする。

参考

主に以下を参考にさせていただきました。ありがとうございます!

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

素人とEnumerizeとarray: true

はじめに

 先日、Railsでチェックボックスを使った複数選択を実装した際に詰まったところがあったので書いておきます。

やりたかったこと

  • 好きな果物
  • みかん
  • りんご
  • ぶどう
  • バナナ

 選択肢の要素はEnumerizeで管理し、ビュー側では上のように複数選択でき、String型でDBに保存し、CSVで出力する際に"りんご, バナナ"という形式にする。

詰まったところ

 CSVに出力しようとするとnilが返ってきた。

解決法

 以下のようにマイグレーションファイルでarray: trueを追加する。

def change
  add_column :users, :favorite_fruits, :string, array: true
end

原因

 複数選択の際にEnumerizeは配列を返す(#<Enumerize::Set {apple, banana}>)ので、array: trueの指定によって配列を許容しておかないとDBに保存できなかったのではないか(おそらく)。

おまけ

 今回使ったRailsで複数選択をする実装をついでに書いておきます。いろんな実装があると思うので個人の責任でお願いします。ちなみに、カラムに複数値を入れるのはアンチパターンです(『SQLアンチパターン』の最初に出てきたような気がする)。ご利用は計画的に!

model
class User < ActiveRecord::Base
  extend Enumerize

  enumerize :favorite_fruits,
    in: %i[
      orange
      apple
      grape
      banana
    ], multiple: true
  end
end
view
<%= form_for @user do |f| %>
  <% User.favorite_fruits.options.each do |name, value| %>
    <%= f.check_box :favorite_fruits, { id: "user_favorite_fruits_#{value}", multiple: true } %>
    <%= f.label :favorite_fruits, name, value: value %>
  <% end %>
<% end %>
controller
class User < ApplicationController
  def show
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      # 処理省略
    end
  end

  private

  def user_params
    params.require(:user).permit(favorite_fruits: [])
    # ストロングパラメーターの配列のための書き方
  end
end
locale
ja:
  user:
    favorite_fruits:
      orange: 'みかん'
      apple: 'りんご'
      grape: 'ぶどう'
      banana: 'バナナ'
csvに出力するとき
@user.favorite_topics.texts.join(', ')

おわり

 今回の記事におけるミスやもっと良い実装があれば気軽に教えてください!

参考

Enumerizeのドキュメント

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

[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで

某スクールのチーム開発にてpay.jpを活用したクレジットカード登録と商品購入機能実装を担当させていただくことになりました!
色々と苦戦しましたがなんとか実装できましたので、以下にまとめてみたいと思います。
今回は商品購入機能実装から商品購入後の設定までまとめております!
カスタムフォームを使用したクレジットカード登録については、以下の過去記事をご参照いただけますと幸いです。
https://qiita.com/Tatsu88/items/23fe4b83d0ff8d78709d

実装内容

購入イメージ
Payjpのアカウント情報を取得して、商品の購入を行ってます。
demo

購入後イメージ
購入後はその製品の写真にSoldとつけ、さらに購入ボタンを使用できないようにしています。
demo

全体の流れ

  1. [購入]コントローラの編集
  2. [購入後]viewの編集
  3. ルーティングの設定
  4. 注意点(公開鍵と秘密鍵について)

1. [購入]コントローラの編集

今回、実装するページ遷移としては以下のようになってます。
商品一覧ページ=>商品詳細ページ=>商品購入確認ページ=>商品購入(コントローラのみ)=>購入完了ページ
*商品一覧ページと商品詳細ページと購入完了ページは既にあるものとして割愛させていただきます。

商品購入確認ページの実装

それでは、商品購入確認ページから実装していきます。
こちらの内容に関しては、以下をポイントとしてコントローラを設定します。
1. 商品/ユーザー/クレジットカードの変数設定
2. Payjpの秘密鍵を取得
3. Payjpから顧客情報を取得し、表示

controller
  def buy
 #商品/ユーザー/クレジットカードの変数設定
    @user = current_user
    @creditcard = Creditcard.where(user_id: current_user.id).first
    @address = Address.where(user_id: current_user.id).first
    @product = Product.find(params[:id])
  #Payjpの秘密鍵を取得
    Payjp.api_key = ”秘密鍵”
  #Payjpから顧客情報を取得し、表示
    customer = Payjp::Customer.retrieve(@creditcard.customer_id)
    @creditcard_information = customer.cards.retrieve(@creditcard.card_id)
    @card_brand = @creditcard_information.brand 
    case @card_brand
    when "Visa"
      @card_src = "visa.svg"
    when "JCB"
      @card_src = "jcb.svg"
    when "MasterCard"
      @card_src = "master-card.svg"
    when "American Express"
      @card_src = "american_express.svg"
    when "Diners Club"
      @card_src = "dinersclub.svg"
    when "Discover"
      @card_src = "discover.svg"
    end
  end

Viewは以下のように実装しております。(参考まで)

view
=render 'single-container'

%main.buy-main 
  = form_tag(action: :purchase, method: :post) do
    .buy-item-container
      %h2.buy-item-head 購入内容の確認
      %section.buy-content.buy-item
        .buy-content-inner
          .buy-item-box
            .buy-item-image
              =image_tag(@product.images[0].product_image.url,class:"buy-image")
            .buy-item-detail
              %p.buy-item-name
                =@product.name
                %p.buy-item-price.bold
                  = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
                  %span.item-shipping-fee.f14.bold
                    (税込)送料込み
      %section.buy-content.buy-user-info
        .buy-content-inner
          %ul.buy-price-table
            %li.buy-price-row.buy-you-pay.bold
            %li.buy-price-cell 支払い金額
            %li.buy-price-cell
              = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
          %section.buy-content.buy-user-info
        .buy-content-inner
          %h3 支払方法
          .payment-content__creditcards__list
            %figure
              = image_tag "#{@card_src}",alt: @card_brand, id: "card_image"
            .payment-content__creditcards__list__number
              = "**** **** **** " + @creditcard_information.last4
            .payment-content__creditcards__list__number
              - exp_month = @creditcard_information.exp_month.to_s
              - exp_year = @creditcard_information.exp_year.to_s.slice(2,3)
              = exp_month + " / " + exp_year
          %section.buy-content.buy-user-info
        .buy-content-inner
          %h3 配送先
          %address.buy-user-info-text
            =@address.postal_code
            %br
            =@address.prefecture
            %br
            =@address.city
            =@address.address
            =@address.apartment
            %br
            =@user.last_name
            =@user.first_name
          %section.buy-content.buy-user-info
          = submit_tag("購入する", class:"purchase")
.exhibit-page__footer
  .exhibit-page__footer__content
    .exhibit-page__footer__content__main
      %ul.exhibit-page__footer__lists
        %li.exhibit-page__footer__list
          プライバシーポリシー
        %li.exhibit-page__footer__list
          メルカリ利用規約
        %li.exhibit-page__footer__list
          特定商取引に関する表記
        %p.exhibit-page__footer__copyright
          © 2019 Mercari

商品購入(コントローラのみ)

上記で実装した商品購入確認ページ情報の購入するを押した時に動くメソッドをコントローラに記載します。
ここでのポイントは以下の通りです。
1. クレジットカードと製品の変数を設定
2. Payjpの秘密鍵を取得
3. payjp経由で支払いを実行
4. 製品のbuyer_idを付与(このbuyer_id、現状は不要ですが、以降の記述で使用します)

支払いについては、下記リファレンスをご参照ください。
https://pay.jp/docs/api/#%E6%94%AF%E6%89%95%E3%81%84%E3%82%92%E4%BD%9C%E6%88%90

controller
  def purchase
 #クレジットカードと製品の変数を設定
    @creditcard = Creditcard.where(user_id: current_user.id).first
    @product = Product.find(params[:id])
 #Payjpの秘密鍵を取得
    Payjp.api_key= '秘密鍵'
 #payjp経由で支払いを実行
    charge = Payjp::Charge.create(
      amount: @product.price,
      customer: Payjp::Customer.retrieve(@creditcard.customer_id),
      currency: 'jpy'
    )
 #製品のbuyer_idを付与
    @product_buyer= Product.find(params[:id])
    @product_buyer.update( buyer_id: current_user.id)
    redirect_to purchased_product_path
  end

以上で商品の購入ができました。
Payjpでログインし、売り上げにしっかりと反映されているか確認しましょう。

続いて購入後のviewの編集を行います。

2. [購入後]viewの編集

購入された製品がいつまで経っても一覧に残っていたり、また購入できたりすると問題です。
そのような事態を防ぐために以下の記述を行いましょう。

商品一覧ページの編集

まず、商品一覧ページのサムネイルに”SOLD”の画像を追加しましょう。
ここでのポイントは以下の通りです。
1. 製品に関して、"buyer_id"の有無を確認
2. "buyer_id"がある場合には、追加の記述を行う

view*一部抜粋
  %li.products__list
    %figure.product
      %figucaption.product__text
        = product.name
      .product__thumbnail
        .product__thumbnail--label
          = #{product.price.to_s(:delimited)}"
        .product__thumbnail--image
          =image_tag(product.images[0].product_image.url)
 #製品に関して、"buyer_id"の有無を確認
            -if product.buyer_id.present? 
 #"buyer_id"がある場合には、追加の記述を行う。
            .items-box_photo__sold
              .items-box_photo__sold__inner SOLD

追加されるクラスのCSSは以下のようになってます。
ポイントとしてSOLDの赤三角形を左上に表示するのですが、その部分はborderを使って実装してます。
width: 0;とheight: 0;にして、border-widthは上と右側のみを幅を設定し、
border-colorは上部のみに設定しています。

css*一部抜粋
.items-box_photo__sold{
  width: 0;
  height: 0;
  border-width: 120px 120px 0 0;
  border-color: #ea352d transparent transparent transparent;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  border-style: solid;
  &__inner{
    top: -90px;
    font-size: 30px;
    position: absolute;
    left: 0;
    z-index: 2;
    color: #fff;
    transform: rotate(-45deg);
    letter-spacing: 2px;
    font-weight: 600;
  }
}

商品購入確認ページの編集

続いて商品購入確認ページも編集します。
既に購入された製品は購入できないようにしましょう。
*写真に関しては、上記と同様のため割愛します。

ここでのポイントは以下の通りです。
1. 製品に関して、"buyer_id"の有無を確認
2. "buyer_id"がある場合には、”売り切れました”と表示する。(ボタンもdisableにします)

view*一部抜粋
        .product-buy__btn__box       
        - if user_signed_in? && current_user.id ==@product.user_id
          = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn"
          = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn"
 #製品に関して、"buyer_id"の有無を確認
        - elsif @product.buyer_id.present? 
          = link_to "売り切れました",buy_product_path,class:"disabled-button bold"
        - else
          = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"

追加されるクラスのCSSは以下のようになってます。

css*一部抜粋
.disabled-button{
  text-align: center;
  margin: 16px 0 0;
  background: #888888;
  color: #fff;
  display: block;
  width: 100%;
  font-size: 24px;
  line-height: 60px;
  cursor: not-allowed;
}

以上で購入後に行うべき変更が実装できました。

3. ルーティングの設定

上記にてコントローラとviewの設定が完了しました!
最後にルーティングを設定しましょう。
注意点としては、memberを使用することにあります。
こちらをcollectionにしてしまうと、productのidを取得できなくなってしまいますので、
idを取得するためにmemberで設定しましょう。(私はここで迷走しました笑)

route.rb*一部抜粋
resources :products do
    member do
      post 'purchase'
      get 'purchased'
      get 'buy'
    end
end

4. 注意点(公開鍵と秘密鍵について)

上記にて機能の実装ができました。
そこで一点注意がございます。
公開鍵と秘密鍵をそのまま記述してしまうのは危険なため、secrets.ymlなどに記述しましょう。

公開鍵と秘密鍵を.bash_profileに格納

まず、公開鍵と秘密鍵を隠しファイルに格納します。

ターミナル
#公開鍵と秘密鍵を記述します。
$ vim ~/.bash_profile

#まず「i」を押して入力モードにしましょう
export PAYJP_ACCESS_KEY='sk_test_*************'
export PAYJP_PUBLIC_KEY='pk_test_*************'
#入力後、"escキー"=> ":" => "w" => "q"の順でコマンドを打ちましょう。

#公開鍵と秘密鍵を記述後、保存を行います。
$ source ~/.bash_profile

Vimのコマンド詳細は以下を確認ください
https://qiita.com/hide/items/5bfe5b322872c61a6896

以上で隠しファイルへの記述は完了です。
続いてsecrets.ymlに記述しましょう。

secrets.ymlに記載

今回、rubyのver的にsecrets.ymlに記載しております。
verが新しい方はcredential.ymlに記載ください。

secrets.yml
#developmentとproduction、共に記載します。
  payjp_access_key: <%= ENV["PAYJP_ACCESS_KEY"] %>
  payjp_public_key: <%= ENV["PAYJP_PUBLIC_KEY"] %>

各コントローラへの記述をベタ打ちから変更

上記にてやっと準備が整いました。
コントローラへの記述をベタ打ちから変更しましょう。

contoroller
#どちらでも機能します。どちらかを記述ください
Payjp.api_key =ENV["PAYJP_ACCESS_KEY"]

Payjp.api_key = Rails.application.secrets.payjp_access_key

参照

PAYjp 支払いを行う
https://pay.jp/docs/charge

Payjpに登録したクレジットカードで商品購入を実装する(Rails)
https://qiita.com/takachan_coding/items/d21c0d2621368c9b0d9b

RailsでPayjpを使った購入機能を実装する
https://qiita.com/suzy1031/items/7964829086eb929471a6

商品購入後の商品にSOLDを表記(某フリマアプリのSOLD機能)
https://qiita.com/kortaban/items/ddc613b330c19fcedbeb

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

[Rails]『gemを使わない管理者機能』と『ActionMailerのメール送信機能で問い合わせ機能』を実装

実装すること

①ユーザーは問い合わせフォームから問い合わせを送ることができる
②管理者はユーザーからのお問い合わせ一覧から問い合わせの確認ができる
③管理者が問い合わせ詳細から返信すると、ユーザーの登録しているメールアドレスに届く

ActionMailerとは

ActionMailerを使うと、railsアプリケーションのメイラークラスやビューでメールを送信することができます。
(例)
・メールマガジンの一斉送信
・ウェブサイトに会員登録した時のthank youメール
・お問い合わせフォームの記入内容が管理者にメールでも送信される

参考:Railsガイド https://railsguides.jp/action_mailer_basics.html

ER図

User:Contact = 1:N の関係になります。

問い合わせER図.png

モデルの作成

今回はUserモデル、Adminモデル、Contactモデルを作成します。
Userモデル、Adminモデルはdeviseを使用してモデルを作成していきます。
deviseのviewはusersフォルダとadminsフォルダそれぞれに作成されるようにします。

deviseの導入

Gemfile.
gem 'devise'
$ bundle install
$ rails g devise:install
$ rails g devise User name:string
$ rails g devise Admin name:string
$ rails g devise:views users
$ rails g devise:views admins

アソシエーションの確認

Userモデル

app/models/user.rb
class User < ApplicationRecord
 devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable

 has_many :contact, dependent: :destroy
end

Contactモデル

reply以外のtitleとbodyにバリデーションを掛けます。

app/models/contact.rb
class Contact < ApplicationRecord

    belongs_to :user
    #バリデーション
    validates :title, presence: true
    validates :body, presence: true
end

コントローラーの作成

$ rails g controller users
$ rails g controller admins
$ rails g controller contacts new

ルーティングの設定

・管理者側のURLは、path_namesで推測されないように変更します。
namespaceで、エンドユーザー側か管理者側かが分かりやすいように、urlにusersかadminsかを記載します。

config/routes.rb
Rails.application.routes.draw do
 devise_for :admins, 
     path: 'auth', 
     path_names: { 
     sign_in: 'xxxxxxxxxx', 
     sign_out: 'xxxxxxxxxx', 
     password: 'xxxxxxxxxx', 
     confirmation: 'xxxxxxxxxx', 
     unlock: 'xxxxxxxxxx', 
     registration: 'xxxxxxxxxx', 
     sign_up: 'xxxxxxxxxx' }, 
     controllers: {
         sessions: 'admin/sessions',
         passwords: 'admin/passwords',
         registrations: 'admin/registrations',
  }
  devise_for :users, 
     controllers: {
         sessions: 'users/sessions',
         passwords: 'users/passwords',
         registrations: 'users/registrations',
         omniauth_callbacks: 'users/omniauth_callbacks'
  }

  namespace :admins do

    resources :contacts

  end

  namespace :users do

    resources :users
    resources :contacts

  end
end

メイラーの作成

$ rails g mailer ContactMailer

サーバーの設定

今回はGmailを使用するメール送信機能を記述します。
raise_delivery_errorsは、メールの送信に失敗した時にエラーを出すかどうかです。
default_url_optionsで、アプリケーションのホスト情報をメイラー内で使いたい場合は:hostパラメータを明示的に指定します。
delivery_methodは、デフォルトでsmtpです。
smtp_settingsで、詳細設定をします。
1.port => SMTPサーバーのポート番号
2.address => SMTPサーバーのホスト名
3.domain => HELOドメインを指定する必要がある場合はここで行なう
4.user_name => メール送信に使用するgmailのアカウント
5.password => メール送信に使用するgmailのパスワード
6.authentication => 認証方法
7.enable_starttls_auto => メールの送信にTLS認証を使用するか

config/environments/development.rb
Rails.application.configure do
    config.action_mailer.raise_delivery_errors = true

    config.action_mailer.default_url_options = { :host => 'localhost:3000' }
    config.action_mailer.delivery_method = :smtp

    config.action_mailer.smtp_settings = {
        port:                 587,
        address:              'smtp.gmail.com',
        domain:               'gmail.com',
        user_name:            '<YOUR EMAIL ADDRESS>',
        password:             '<YOUR EMAIL PASSWORD>',
        authentication:       'login',
        enable_starttls_auto: true
    }
end

メイラーを編集

application_mailerには、全メイラー共通の設定を、
sample_mailerには、メイラー個別の設定をします。

application_mailer.rb

defaultメソッドで、共通の処理・設定を記述します。

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end

contact_mailer.rb

管理者の返信が、ユーザーのemailに届くようにします。
個別の設定にはmailメソッドを使用します。
send_when_replyedメソッドを呼び出す時に渡されるユーザーの情報から、
emailアドレスだけを取り出してメールの送信先としてします。
mailメソッドが呼び出されると、メール本文が記載されているビューが読み込まれます。
インスタンス変数でメイラービューに値を渡してあげたいので、インスタンス変数を用意しています。

app/mailers/contact_mailer.rb
class ContactMailer < ApplicationMailer
    default from: 'from@example.com'

    def send_when_admin_reply(user, contact) #メソッドに対して引数を設定
    @user = user #ユーザー情報
    @answer = contact.reply #返信内容
    mail to: user.email, subject: '【サイト名】 お問い合わせありがとうございます'
  end
end

メールの本文を作成する

1つはHTMLフォーマット、もう一つはテキストメールです。
顧客によってはHTMLフォーマットのメールを受け取ることができない / 受け取りたくない人もいるので、テキストメールも作成しておくのが最善です。

contact_mailer/send_when_admin_reply.html.erb

app/views/contact_mailer/send_when_admin_reply.html
<h2><%= @user.name %></h2>
 <p>この度は、お問い合わせありがとうございました。<br>
 以下でご質問の回答となっておりますでしょうか。</p>

  <p><%= @answer %></p>

  <p>今後とも弊社をよろしくお願いいたします。</p>

contact_mailer/send_when_admin_reply.text.erb

app/views/contact_mailer/send_when_admin_replyd.text
  <%= @user.name %> 様

  この度は、お問い合わせありがとうございました。
 以下でご質問の回答となっておりますでしょうか。

  <%= @answer %>

  今後とも弊社をよろしくお願いいたします。

問い合わせフォームの作成(ユーザー側)

users/contacts_controller.rb

app/controllers/users/contacts_controller.rb
class Users::ContactsController < ApplicationController

    def new
        @contact = Contact.new
    end

    def create
        @contact = Contact.new(contact_params)
        @contact.user_id = current_user.id
        if @contact.save
            redirect_to root_path
        else
            @contacts = Contact.all
            @users = User.all
            render :new
        end
        flash[:success] = 'お問い合わせを送信しました。'
    end

    private
    def contact_params
        params.require(:contact).permit(:title, :body, :reply)
    end
end

users/contacts/new.html.erb

app/views/users/contacts/new.html
  <% if @contact.errors.any? %>
        <%= @contact.errors.count %>件のエラー。
        <% @contact.errors.full_messages.each do |message| %>
          <%= message %><% end %>
  <% end %>
  <h2 class="contact-title">お問い合わせフォーム</h2>
      <%= form_for @contact, url: users_contacts_path do |f| %>
          <%= f.label :タイトル %>
              <%= f.text_field :title, class:"form-control" %>
          <%= f.label :内容 %>
              <%= f.text_field :body, class:"form-control" %>
          <%= f.submit "送信" %>
      <% end %>

返信処理の作成(管理者側)

・コントローラーのアクションによって、application_mailer.rb / contact_mailer.rbを起動します。
流れは、
①管理者が問い合わせ一覧(index)から
②問い合わせ詳細(edit)へ飛び、
③返信を送信(update)し、
④その内容をメールでユーザーに送信します。

・ユーザーが問い合わせをした時点で、Contactsテーブルの中に、
レコードが作成されます。その中に「返信(reply)」というカラムが用意されています。
updateアクションを使って、すでにあるレコードの中に管理者の返信のデータだけを更新することで、ContactMailerのsend_when_admin_replyアクションが発火します。

deliver_nowについて

いくつかの参考書や解説サイトでは、deliver_nowメソッドではなくdeliverメソッドを使うように記載されています。
じつは、deliver_nowを使ってメール送信すると、メール送信が完了するまでRailsが反応しなくなることがあります。そのため、Rails4.2から「Active Job」という非同期処理が導入されました。「Active Job」を使うと、メール送信を裏で行わせながら処理を行うことができますので、メール送信完了を待つ必要がなくなるのです。
そのため、「すぐに送信する」ということが分かるように、deliverではなく、deliver_nowという名称になっているのです。
参考:https://web-camp.io/magazine/archives/19143

admins/contacts_controller.rb

app/controllers/admins/contacts_controller.rb
class Admins::ContactsController < ApplicationController

    def index
        @contacts = Contact.page(params[:page]).order(created_at: :desc).per(16)
        @users = User.all
    end

    def edit
        @contact = Contact.find(params[:id])
    end

    def update
        contact = Contact.find(params[:id]) #contact_mailer.rbの引数を指定
        contact.update(contact_params)
        user = contact.user
        ContactMailer.send_when_admin_reply(user, contact).deliver_now #確認メールを送信
        redirect_to admins_items_path
    end

    def destroy
        contact = Contact.find(params[:id])
        contact.destroy
        @contacts = Contact.page(params[:page]).order(created_at: :desc).per(16)
        @users = User.all
        render :index
    end

    private
    def contact_params
        params.require(:contact).permit(:title, :body, :reply)
    end
end

問い合わせ一覧(admins/contacts/index.html.erb)

app/views/admins/contacts/index.html
<h2>問い合わせ一覧</h2>
    <table>
        <thead>
        <tr>
            <th>ユーザー名</th>
        <th>問い合わせタイトル</th>
        <th>受信日</th>
        <th></th>
        <th></th>
        </tr>
    </thead>
    <tbody>
        <% @users.each do |user| %>
            <% user.contact.each do |contact| %>
              <tr>
                  <td><%= contact.user.name %><%= user.name %></td>
                  <td><%= contact.title %></td>
                  <td><%= contact.created_at.strftime("%Y年%m月%d ") %></td>
                  <td><%= link_to "返信", edit_admins_contact_path(contact) %></td>
                  <td><%= link_to "削除", admins_contact_path(contact), method: :delete %></td>
               </tr>
            <% end %>
       <% end %>
        </tbody>
</table>

問い合わせ内容(admins/contacts/edit.html.erb)

app/views/admins/contacts/edit.html
<h2>お問い合わせ内容</h2>
    <%= form_for @contact, url: admins_contact_path do |f| %>
        <h3>顧客名</h3>
        <%= @contact.user.name %>さん
        <h3>問い合わせ日</h3>
        <%= @contact.created_at.strftime("%Y年%m月%d ") %>
        <h3>タイトル</h3>
        <%= @contact.title %>
        <h3>問い合わせ内容</h3>
        <%= @contact.body %>
        <h3>返信</h3>
        <%= f.text_area :reply, class:"form-control", placeholder: "返信内容" %>
        <%= f.submit "送信" %>
    <% end %>

Google側での設定

・2段階認証をオンにする
https://support.google.com/accounts/answer/185839?co=GENIE.Platform%3DDesktop&hl=ja
・アプリケーション用のパスワードを発行する
https://support.google.com/accounts/answer/185833?hl=ja
・2段階認証プロセスで使用するパスワードとコード
https://support.google.com/accounts/answer/1070457?hl=ja

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。

twitter:https://twitter.com/yto_oct
note:https://note.com/yto_oty

参考

Railsガイド
https://railsguides.jp/action_mailer_basics.html
Action Mailer でメール送信機能をつくる
https://qiita.com/annaaida/items/81d8a3f1b7ae3b52dc2b
Gmail 経由での SMTP メール送信が出来ない時の解決法 (ユーザー名とパスワードは合っているのに。。)
https://qiita.com/Yinaura/items/6886682a607951a71bac

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

docker-compose run web rake db:createできないときにやったこと

エラー文

Starting myapp_db_1 ... done
warning Integrity check: System parameters don't match                                                                                                                                                     
error Integrity check failed                                                                                                                                                                               
error Found 1 errors.                                                                                                                                                                                      


========================================
  Your Yarn packages are out of date!
  Please run `yarn install --check-files` to update.
========================================

#ここの文章を参照した
To disable this check, please change `check_yarn_integrity`  
to `false` in your webpacker config file (config/webpacker.yml).


yarn check v1.21.1
info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.

解決策

「config/webpacker.yml」ファイルのcheck_yarn_integrity: trueの箇所をfalseに変更したら解決した。

check_yarn_integrityって何?

JavaScriptのパッケージをチェックしたり、保全したりする役割があるらしい。。。

参考記事

webpacker の check_yarn_integrity オプションについて調べてみた
yarnが原因でdocker-compose runが実行できないときの対処法
上記記事を参考にしたらできたので執筆者には感謝です。

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

[Rails]通知機能の実装

実装すること

自分の投稿にいいねされた時・コメントをもらった時、フォローされた時に通知が来るようにします。

完成イメージ

通知を未確認の状態では、ヘッダーのNOTICEにマークが付きます。
通知を確認すると、マークが消えます。
通知イメージ(マーク).png
ユーザーは自分への通知を見ることができます。
通知はまとめて全削除することができます。
通知一覧.png

ER図

Notificationテーブル
item_id : いいねされた投稿のid
visiter_id : 通知を送ったユーザーのid
visited_id : 通知を送られたユーザーのid
comment_id : 投稿へのコメントのid
action : 通知の種類(フォロー、いいね、コメント)
checked : 通知を送られたユーザーが通知を確認したかどうか default: false

通知ER図.png

モデルの作成

Userモデル、Itemモデルは作成した前提で進めていきます。
作成手順は下記リンク先で説明しております。
[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448

$ rails g model Notification visiter_id:integer visited_id:integer item_id:integer comment_id:integer action:string checked:boolean

マイグレーションファイルの追記

checked(通知を送られたユーザーが通知を確認したかどうか)のdefault: false,null: falseを追記します。

app/db/migrate/20200119094201_create_notification.rb
class CreateNotifications < ActiveRecord::Migration[5.2]
  def change
    create_table :notifications do |t|
      t.integer :visiter_id
      t.integer :visited_id
      t.integer :item_id
      t.integer :comment_id
      t.string :action
      t.boolean :checked, default: false, null: false ←追記

      t.timestamps
    end
  end
end
$ rails db:migrate

アソシエーションの確認

Userモデル

・自分が作った通知(active_notifications)と自分宛の通知(passive_notifications)の関連付けメソッドを実装します。
active_notificationsでは、class_name: "Notification"でNotificationモデルの、foreign_key: "visiter_id" で、visiter_idを参考に、active_notificationsモデルへアクセスするようにしています。
passive_notificationsも同じです。

app/models/user.rb
class User < ApplicationRecord

   has_many :active_notifications, class_name: "Notification", foreign_key: "visiter_id", dependent: :destroy
   has_many :passive_notifications, class_name: "Notification", foreign_key: "visited_id", dependent: :destroy

end

Itemモデル

app/models/item.rb
class Item < ApplicationRecord

   has_many :notifications, dependent: :destroy

end

Notificationモデル

optional: trueはitem_idにnilを許容するものです。railsではbelongs_toのつけられたカラムには自動的にallow_nil: falseが付与されます。フォロー通知ではitem_idは関与しないためnilとなるので、このオプションをつけないとフォロー通知が有効になりません。

app/models/notification.rb
class Notification < ApplicationRecord

    #スコープ(新着順)
    default_scope->{order(created_at: :desc)}

    belongs_to :item, optional: true
    belongs_to :comment, optional: true
    belongs_to :visiter, class_name: 'User', foreign_key: 'visiter_id', optional: true
    belongs_to :visited, class_name: 'User', foreign_key: 'visited_id', optional: true

end

コントローラーの作成

$ rails g controller notifications

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do

    resources :notifications, only: :index

end

通知メソッドの作成

Itemモデル(いいねとコメントのメソッド)

distinctメソッドは、重複レコードを1つにまとめるためのメソッドです。

app/models/item.rb
    def create_notification_by(current_user)
        notification = current_user.active_notifications.new(
          item_id: id,
          visited_id: user_id,
          action: "like"
        )
        notification.save if notification.valid?
    end

    def create_notification_comment!(current_user, comment_id)
        # 自分以外にコメントしている人をすべて取得し、全員に通知を送る
        temp_ids = Comment.select(:user_id).where(item_id: id).where.not(user_id: current_user.id).distinct
        temp_ids.each do |temp_id|
            save_notification_comment!(current_user, comment_id, temp_id['user_id'])
        end
        # まだ誰もコメントしていない場合は、投稿者に通知を送る
        save_notification_comment!(current_user, comment_id, user_id) if temp_ids.blank?
  end

    def save_notification_comment!(current_user, comment_id, visited_id)
        # コメントは複数回することが考えられるため、1つの投稿に複数回通知する
        notification = current_user.active_notifications.new(
          item_id: id,
          comment_id: comment_id,
          visited_id: visited_id,
          action: 'comment'
        )
        # 自分の投稿に対するコメントの場合は、通知済みとする
        if notification.visiter_id == notification.visited_id
          notification.checked = true
        end
        notification.save if notification.valid?
     end

Userモデル(フォローのメソッド)

app/models/user.rb
 #フォロー時の通知
 def create_notification_follow!(current_user)
    temp = Notification.where(["visiter_id = ? and visited_id = ? and action = ? ",current_user.id, id, 'follow'])
    if temp.blank?
      notification = current_user.active_notifications.new(
        visited_id: id,
        action: 'follow'
      )
      notification.save if notification.valid?
    end
  end

いいねの通知(likes_controller)

いいねの作成と同時に通知も作成されます。

app/controllers/likes_controller.rb
  def create
      like = current_user.likes.new(item_id: @item.id)
      like.save
      @item = Item.find(params[:item_id])
      #通知の作成
      @item.create_notification_by(current_user)
      respond_to do |format|
        format.html {redirect_to request.referrer}
        format.js
      end
  end

コメントの通知(comments_controller)

app/controllers/comments_controller.rb
def create
    @item = Item.find(params[:item_id])
    #投稿に紐づいたコメントを作成
    @comment = @item.comments.build(comment_params)
    @comment.user_id = current_user.id
    @comment_item = @comment.item
    if @comment.save
      #通知の作成
      @comment_item.create_notification_comment!(current_user, @comment.id)
      render :index
    end
end

フォローの通知(relationships_contoroller)

app/controllers/comments_controller.rb
def create
      @user = User.find(params[:following_id])
      current_user.follow(@user)
      #通知の作成
      @user.create_notification_follow!(current_user)
end

通知一覧ページの作成

notification_controller

・indexを開いたらchecked:falseが全てchecked:trueになるようにしています。
・ログインユーザーの通知全削除では、destroy_allメソッドを使用しています。
参考:https://pikawaka.com/rails/destroy_all

app/controllers/notification_controller.rb
class Users::NotificationsController < ApplicationController

    def index
      #current_userの投稿に紐づいた通知一覧
        @notifications = current_user.passive_notifications
      #@notificationの中でまだ確認していない(indexに一度も遷移していない)通知のみ
        @notifications.where(checked: false).each do |notification|
            notification.update_attributes(checked: true)
        end
    end

    def destroy_all
      #通知を全削除
        @notifications = current_user.passive_notifications.destroy_all
        redirect_to users_notifications_path
    end

end

通知一覧(notifications/index.html.erb)

通知内容は、パーシャルにします。
通知一覧.png

app/views/notifications/index.html
<h3 class="text-center">通知</h3>
    <%= link_to destroy_all_users_notifications_path, method: :delete do %>
        <i class="fas fa-trash" style="color: black;"></i>
        <h7 style="color: black;">全削除</h7>
    <% end %>
    <hr>
<% if @notifications.exists? %>
    <div class="users-index">
        <%= render @notifications %>
    </div>
<% else %>
    <p>通知はありません</p>
<% end %>

通知内容

notification_helper

通知内容によって、表示内容を分岐させます。

app/helpers/notifications_helper.rb
module Users::NotificationsHelper

   def notification_form(notification)
      @visiter = notification.visiter
      @comment = nil
      your_item = link_to 'あなたの投稿', users_item_path(notification), style:"font-weight: bold;"
      @visiter_comment = notification.comment_id
      #notification.actionがfollowかlikeかcommentか
      case notification.action
        when "follow" then
          tag.a(notification.visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"があなたをフォローしました"
        when "like" then
          tag.a(notification.visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:users_item_path(notification.item_id), style:"font-weight: bold;")+"にいいねしました"
        when "comment" then
            @comment = Comment.find_by(id: @visiter_comment)&.content
            tag.a(@visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:users_item_path(notification.item_id), style:"font-weight: bold;")+"にコメントしました"
      end
    end

end

notifications/_notification.html.erb

app/views/notifications/_notification.html
<% visiter = notification.visiter %>
<% item = notification.item %>

<div class="user-view clearfix ">
  <%= link_to users_user_path(notification.visiter) do %>
    <%= attachment_image_tag visiter, :profile_image, :fill,100,100, format: "jpeg", fallback: "no_image.jpg", size: "50x50", class:"profile-img-circle" %>
  <% end %>
  <%= notification_form(notification) %><span class="moderate-font"><%= " (#{time_ago_in_words(notification.created_at)} 前)" %></span>
  <br>
  <% if !@comment.nil? %>
    <p class="moderate-font text-center" style="color: #C0C0C0;"><%= @comment %></p>
  <% end %>
</div>

未確認の通知があるときはマークで知らせる

notifications_helper

未確認の通知(checked:falseの通知)を示すunchecked_notificationsメソッドをnotifications_helperに書きます。

app/helpers/notifications_helper.rb
def unchecked_notifications
    @notifications = current_user.passive_notifications.where(checked: false)
end

layouts/application.html.erb

未確認の通知があるときは、黄色いマークで知らせます。

通知イメージ(マーク).png

app/views/layouts/application.html
<% if unchecked_notifications.any? %>
    <i class="fa fa-circle" style="color: gold;"></i>
<% end %>
<li>
    <%= link_to "NOTICE", users_notifications_path, class: "btn-default" %>
</li>

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
いいねとコメントとフォローは下記リンク先で実装しています。

[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装
https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
[Rails]Ajaxを用いて非同期でコメント機能の実装
https://qiita.com/yuto_1014/items/c7d6213139a48833e21a
[Rails]Ajaxを用いて非同期でフォロー機能の実装
https://qiita.com/yuto_1014/items/8d508b84fd0c2316ba01

twitter:https://twitter.com/yto_oct
note:https://note.com/yto_oty

参考

【学習アウトプット4】通知機能の作り方
https://qiita.com/tktk0430/items/bdb8fbcf4ce3258b2d41

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

Railsチュートリアルの開発環境を Docker にしてみなイカ?

【概要】

Railsチュートリアルで作成する Sample App の開発環境を Docker でコンテナ化しました。

細かい用語の勉強はおいといて、とりあえず動くものを作ってみましょう。



【本文】

□ 事前準備

※ 準備済みの場合は省略可能

■ Docker 準備

インストール方法については、検索したら大量に出てくることから、本記事では詳細を記述しないため、ご了承ください。以下のURLが参考になるかと思います。
- Mac OS X に Docker Toolbox のインストール — Docker-docs-ja 1.10.0 ドキュメント

■ Sample App 準備

 ご存知の通り、Railsチュートリアルでは、Ruby on Rails を使用してSample App という名称のアプリケーションを作成します。しかし、一から作成するのは手間でしたので、本記事では、YassLab(株)がGitHubに公開しているサンプルコードを使用しました。
 なお、サンプルコードを使用する手順は、以下の通りです。

○ 1: GitHubから自分のPCにダウンロード

bash
 $ git clone https://github.com/yasslab/sample_apps.git

○ 2: 使用するプロジェクトのディレクトリ準備

bash
 $ mkdir sample_app
 $ cp -rp sample_apps/5_1_2/ch14/* sample_app/
 $ cd sample_app

□ 1: Dockerfile

  • プロジェクトのルートディレクトリからの操作を想定します。

○ 1: Dockerfile生成

bash
 $ mkdir docker
 $ touch docker/Dockerfile

○ 2: Dockerfileの編集

Dockerfile
FROM ruby:2.4.9-alpine3.11

ENV LANG C.UTF-8 \
    TZ Asia/Tokyo

ENV BUILD_PACKAGES="build-base" \
    DB_PACKAGES="sqlite-dev postgresql-dev" \
    RAILS_PACKAGES="tzdata nodejs imagemagick" \
    FAVORITE_PACKAGES="less"

RUN apk update && \
    apk upgrade && \
    apk --update --no-cache add \
        ${BUILD_PACKAGES} \
        ${DB_PACKAGES} \
        ${RAILS_PACKAGES} \
        ${FAVORITE_PACKAGES}

WORKDIR /app

COPY Gemfile \
     Gemfile.lock \
     /app/

RUN bundle install --jobs=4

# 下記のコードは、頻繁に Gemfile を変更する場合、コメントアウトを推奨します。
# RUN apk del ${BUILD_PACKAGES}

# https://github.com/bundler/bundler/issues/6154
ENV BUNDLE_GEMFILE='/app/Gemfile'

□ 2: docker-compose.yml

○ 1: Dockerfile生成

bash
 $ touch docker-compose.yml

○ 2: docker-compose.ymlの編集

docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    ports:
      - 3000:3000
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    volumes:
      - ./:/app:cached
    stdin_open: true
    tty: true     

□ 3: コンテナ生成

○ 1: docker-compose.yml を元にコンテナに必要なイメージを構築

bash
 $ docker-compose build

○ 2: 構築したイメージを元にコンテナを構築して起動

bash
 $ docker-compose up -d

□ 4: Sample App の準備

  • ここまでの操作により、Docker によるコンテナ化は終了といって差し支えありません。
  • ここからは、通常の Rails アプリケーションに必要な操作と同じです(Heroku の操作感に似てるかと)。

○ 1: コンテナ環境に入る

  • Alpine Linux のデフォルトのログインシェルは、ashになります。
  • bashで操作したい方は、拡張機能に適宜追加してください。
bash
 $ docker-compose exec app ash

○ 2: データベース関係の準備

  • ご存知の通りデータベースがない状態では、 http://localhost:3000 にアクセスしてもアプリケーションは動きません。
  • コンテナ内で rails コマンドを試してみましょう。
ash(コンテナ内)
 $ rails db:create
 $ rails db:migrate
 $ rails db:seed

○ 3: 作業完了

  • これで、開発環境のコンテナ化が完了しました。
  • http://localhost:3000 にアクセスして、試しに操作してみてください。
  • 終了時は、以下のコマンドとなります。
bash
 # 一時停止
 $ docker-compose stop
 # 再起動
 $ docker-compose start
 # コンテナ削除
 $ docker-compose down

□ ※ 注意事項等

■ 注意事項

※ 1: 新たにGemを追加する場合

  • bundle install が以下のように失敗する可能性があります。
    • Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
  • 原因として、コンテナ環境の OS である Alpine Linux において、導入したい Gem に必要となる OS の拡張機能(=package)がインストールされていないことが多くの理由になるかと思われます。
  • 解決策として、エラー文中に記載されているログを$ cat 〜色々path〜/mkmf.logで内容を確認して、必要な拡張機能を$ apk add 〜拡張機能〜で逐一インストールしていけば、解決されるはずです。
  • 本法で解決した場合は、$ docker-compose build --no-cacheの際に追加でインストールした拡張機能がリセットされるため、 Dockerfile にも必ず追加しておいてください。
  • 大元の原因は、本記事で使用した Dockerfile は、限られた拡張機能しかインストールしていないことです。もし、不便に感じる場合は、別のイメージを使用して、自分だけのコンテナ環境の作成に挑戦してみると良いでしょう。Docker 及び Linux の学習に繋がります。
  • また、他のエラーとして、 Gem はインストールされるが正常に動作しないこともあります。気が向いたら別記事で簡単に説明します。

※ 思いつき次第追加

■ あとがき

プログラマへの転職前、駆け出しエンジニアで Docker を勉強してる人を散見して、正直焦ってました。入門用の記事や動画を見ても、訳の分からない Dockerfile にコマンドの数々...しかし、実際に転職してからDockerfile の中身を理解してなくても、コンテナ作って作業ができれば十分でした。

この経験から、初期段階ではコマンドが扱えてコンテナを立ち上げられる程度の理解で十分と考えたため、コピペでファイルを作成して、コマンド打つだけの構成にして、簡単に Docker の世界を体験できるようにしてみました。

もちろん、Dockerfile から考えて作成するのも良い勉強になりますが、入社初期にインフラ周りを触らせることも少ないと思うので、コーディングに直接関係する学習を優先した方がよいでしょう(DBとかSQLとかActiveRecordとか...)。

Docker への入門にあたり参考になりましたら幸いです。


  1. Railsチュートリアル公式では、Rubyのバージョンが2.4.0でしたが、当該バージョンのDockerfileがサポートされていないため、サポート中で同じマイナーバージョンである2.4.9を選択しています。2.4.0を使用したい場合は、Dockerfile中の FROM部分をFROM ruby:2.4.0-alpineに置換してください。 

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

(メモ)【Rails勉強会 第8回】アソシエーンション Part2(2020/2/16)

※こちらの記事は,「人生逆転サロン共同開発参加者限定の勉強会」で使用したコードのメモ書きです。

人生逆転サロンの共同開発について知りたい方はこちらをご覧下さい。
http://yanbaru-spike.com/l/c/yE5eqgMh/NuSs3Jlb

0. 内容

【開催日】 2/16(日) 19:30〜20:30

前回,「1対多」のアソシエーションについての基礎を話しましたので,その実践として「ログイン機能付きのメッセージ投稿アプリ」を作成していきましょう!

1. ログイン機能の実装

1.1 アプリの作成

  • アプリの作成
ターミナル
rails new message_app

rails sでサーバーを起動し,http://localhost:3000にアクセスしたときに,Yay! You’re on Rails!が表示されることを確認して下さい。

1.2 ログイン機能の実装・日本語化

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.5'

gem 'rails', '~> 6.0.2', '>= 6.0.2.1'
gem 'sqlite3', '~> 1.4'
gem 'puma', '~> 4.1'
gem 'sass-rails', '>= 6'
gem 'webpacker', '~> 4.0'
gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.7'
gem 'bootsnap', '>= 1.4.2', require: false

# ***** 以下を追加 *****
# ログイン機能
gem 'devise'

# 日本語化(Rails 6の場合)
gem 'rails-i18n', '~> 6.0'
gem 'devise-i18n'
# ***** 以上を追加 *****

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  # ***** 以下を追加 *****
  # デバッグ用
  gem 'pry-byebug'
  # ***** 以上を追加 *****
end

group :test do
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  gem 'webdrivers'
end
ターミナル
# 必要があれば, $ cd message_app でアプリのルートディレクトリに移動後
bundle install
rails g devise:install
rails g devise user
rails db:migrate
config/application.rb
module MessageApp
  class Application < Rails::Application
    config.load_defaults 6.0
    # ***** 以下を追加 *****
    # 以下を追加すれば日本語に
    config.i18n.default_locale = :ja
    # ***** 以上を追加 *****
  end
end
  • サーバー「再」起動後に,http://localhost:3000/users/sign_upにアクセス

  • ログイン関連ページのビューファイル(見た目)を変更したい場合は,次のコマンドを使用してからファイルを修正

ターミナル
rails g devise:i18n:views
# 注意: 日本語のサイトを作るのに次を使用しないこと!
rails g devise:views
  • 日本語訳を変更したい場合は,次のコマンドを使用してファイルを修正
ターミナル
# 日本語訳を変更したい場合は,次でファイルを作成して編集
rails g devise:i18n:locale ja

【補足】 ログインページにフラッシュを実装したり,ユーザー編集ページなど修正した方がよい箇所があるが,今回はスルーで。

2. メッセージのCRUD機能

2.1 メッセージの一覧表示

ターミナル
rails g controller messages index new create edit update destroy
rails g model Message content:string user:references
  • バリデーションを入れておく
db/migrate/日時.rb
class CreateMessages < ActiveRecord::Migration[6.0]
  def change
    create_table :messages do |t|
      # ***** null: false を追加 *****
      t.string :content, null: false
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
ターミナル
rails db:migrate
app/models/user.rb
class User < ApplicationRecord
  # ***** 以下を追加 *****
  has_many :messages, dependent: :destroy
  # ***** 以上を追加 *****
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
app/models/message.rb
class Message < ApplicationRecord
  belongs_to :user
  # ***** 以下を追加 *****
  validates :content, presence: true, length: { maximum: 140 }
  # ***** 以上を追加 *****
end
  • 初期データ

  • usersテーブル

id email
1 test@example.com
2 hoge@example.com
  • messagesテーブル
id user_id content
1 1 おはよう
2 2 こんにちは
3 1 こんばんは
4 1 おやすみ
db/seeds.rb
user1 = User.create!(email: "test@example.com", password: "password")
user2 = User.create!(email: "hoge@example.com", password: "password")

user1.messages.create!(content: "おはよう")
user2.messages.create!(content: "こんにちは")
user1.messages.create!(content: "こんばんは")
user1.messages.create!(content: "おやすみ")
ターミナル
rails db:seed
  • ルーティング
config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  resources :messages
  root to: "messages#index"
end
  • コントローラ
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  # ***** 以下を追加 *****
  # ログイン必須とする
  before_action :authenticate_user!
  # ***** 以上を追加 *****

  def index
    # ***** 以下を追加(Message.allはよくない) *****
    @messages = Message.includes(:user)
    # ***** 以上を追加 *****
  end
end
  • ビュー
app/views/messages/index.html.erb
<% @messages.each do |message| %>
  <div>
    <p>【メールアドレス】 <%= message.user.email %></p>
    <p>【内容】 <%= message.content %></p>
  </div>
  <hr>
<% end %>

http://localhost:3000にアクセス

2.2 ヘッダー

app/views/layouts/application.html.erb
  <!-- 省略 -->
  <body>
    <!-- ***** 以下を追加 ***** -->
    <%= render 'shared/header' %>
    <!-- ***** 以上を追加 ***** -->
    <%= yield %>
  </body>
  • app/viewssharedディレクトリを作成
app/views/shared/_header.html.erb
<header>
  <% if user_signed_in? %>
    <%= link_to "投稿一覧", root_path %>
    <%= link_to "新規投稿", new_message_path %>
    <%= link_to 'アカウント編集', edit_user_registration_path %>
    <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <%= "【ログイン中のアドレス】#{current_user.email}" %>
  <% else %>
    <%= link_to "新規登録", new_user_registration_path %>
    <%= link_to "ログイン", new_user_session_path %>
  <% end %>
</header>
<hr>

2.3 メッセージの新規投稿機能

  • 投稿時のエラー処理は
app/controllers/messages_controller.rb
  # ***** 以下を編集 *****
  def new
    @message = Message.new
  end

  def create
    current_user.messages.create!(message_params)
    redirect_to action: :index
  end
  # ***** 以上を編集 *****

  # 略
  # ***** 一番下に以下を追加 *****
  private

  def message_params
    params.require(:message).permit(:content)
  end
  # ***** 以上を追加 *****
end
app/views/messages/new.html.erb
<%= form_with model: @message, local: true do |form| %>
  <%= form.text_field :content, required: true %>
  <%= form.submit "送信" %>
<% end %>

【補足】 本当はフラッシュを実装したり,投稿エラー時の処理が必要だが,今回はスルー

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

[Rails]ActionCableを使用してリアルタイムチャットの実装

実装すること

ActionCableを使用して特定のユーザーとリアルタイムチャットをできるようにしていきます。

完成イメージ

チャットイメージ図.png

ActionCableとは

RailsにおいてWebsocketによる双方向通信を実現してくれているものです。
(Webにおいて双方向通信を低コストで行うための仕組み)
参考:Railsガイドhttps://railsguides.jp/action_cable_overview.html

ER図

「EntryテーブルとDirectMessageテーブル」が「UserテーブルとRoomテーブル」の中間テーブルになっています。

チャットER図.png

ページ設計

①ユーザーの詳細ページ(users/show)から新規でダイレクトメッセージを送ることができる。
②チャットルーム(rooms/show)に入ると、
(1)メッセージフォームまでスクロールされる(新着メッセージは下に蓄積されていくため)
(2)チャットルームでは、ログインユーザーが右に相手が左に表示される
③チャットルーム一覧(rooms/index)はヘッダーのリンクから飛べる

Gemの追記

jQueryを使えるようにします。

Gemfile.
gem 'jquery-rails'
app/assets/javascripts/application.js
//
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery ←追記
//= require jquery_ujs ←追記
//= require_tree .
$ bundle install

モデルの作成

Userモデルはデバイスを元に作成したものとします。
作成手順は下記リンク先で説明しております。
[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448

それではRoomモデル、Entryモデル、DirectMessageモデルを作成していきます。

$ rails g model Room name:string
$ rails g model Entry user_id:integer room_id:integer
$ rails g model DirectMessage content:string user_id:integer room_id:integer

アソシエーションの確認

多対多の関連がある時はthroughオプションをつけてあげると中間テーブルを経由して関連先のモデルを取得できるようになります。

Userモデル

app/models/user.rb
   has_many :entries
   has_many :direct_messages
   has_many :rooms, through: :entries

Roomモデル

dependent: :destroyで、roomの特定のレコードが消えれば、そのレコードの紐づいたentryレコードとdirect_messageレコードも消えるようにしています。

app/models/room.rb
   has_many :entries, dependent: :destroy
   has_many :direct_messages, dependent: :destroy
   has_many :users, through: :entries

Entryモデル

app/models/room.rb
  belongs_to :user
  belongs_to :room

DirectMessageモデル

データが作成されたら非同期でブロードキャスト処理を実行するようにします。
after_createではなく、after_create_commitを使っている点に注意してください。
トランザクションをコミットしたあとでブロードキャストしないと、他のクライアントからデータが見えない恐れがあります。

app/models/direct_message.rb
  belongs_to :user
  belongs_to :room
  #ブロードキャスト
  after_create_commit { DirectMessageBroadcastJob.perform_later self }

コントローラーの作成

$ rails g controller users
$ rails g controller rooms

ルーティングの作成

config/routes.rb
 devise_for :users
 resources :users
 resources :rooms

ユーザー詳細ページ(コントローラー・ビューの編集)

users_controller

app/controllers/users_controller.rb
    def show
        @user = User.find(params[:id])
        #チャット
        if user_signed_in?
            #Entry内のuser_idがcurrent_userと同じEntry
            @currentUserEntry = Entry.where(user_id: current_user.id)
            #Entry内のuser_idがMYPAGEのparams.idと同じEntry
            @userEntry = Entry.where(user_id: @user.id)
                #@user.idとcurrent_user.idが同じでなければ
                unless @user.id == current_user.id
                  @currentUserEntry.each do |cu|
                    @userEntry.each do |u|
                      #もしcurrent_user側のルームidと@user側のルームidが同じであれば存在するルームに飛ぶ
                      if cu.room_id == u.room_id then
                        @isRoom = true
                        @roomId = cu.room_id
                      end
                    end
                  end
                  #ルームが存在していなければルームとエントリーを作成する
                  unless @isRoom
                    @room = Room.new
                    @entry = Entry.new
                  end
                end
        end
    end

ユーザー詳細ページ(users/show.html.erb)

<% if @isRoom == true %>で、roomが存在していれば既存のroomへ飛び、まだroomが存在していなければroomを作成するようにしています。
<%= fields_for @entry do |e| %>で、roomと同時にentryも作成するようにしています。

app/views/users/show.html
<% if user_signed_in? %>
    <% unless @user.id == current_user.id %>
        <!-- 既にroomが存在していれば既存のroomへ -->
        <% if @isRoom == true %>
            <!-- メールアイコンでroomに飛べるようにする -->
            <%= link_to room_path(@roomId) do %>
                <button id="dm_submit"><i class="fas fa-envelope"></i></button>
            <% end %>
        <% else %>
            <!-- roomが存在していなければroomを作成する  -->
            <%= form_for @room, url: rooms_path do |f| %>
                <%= fields_for @entry do |e| %>
                   <%= e.hidden_field :user_id, :value=> @user.id %>
                <% end %>
                <button type="submit" id="dm_submit"><i class="fas fa-envelope"></i></button>
            <% end %>
        <% end %>
    <% end %>
<% end %>

ルームの作成・閲覧ページ(コントローラー・ビューの編集)

rooms_controller

『index』では、ログインユーザーのroom一覧の相手の名前を表示しています。
ログインユーザーが所属しているroomの中でログインユーザー以外のユーザー名を取って来ています。
『show』は、roomの詳細を表示しています。
roomが存在していれば、そのroomに紐づいたdirect_messagesとentriesを取ってきます。
『create』で、roomを作成します。
roomの作成と同時に、そのroomに紐づいたentryユーザーを2名(ログインユーザーとユーザー詳細ページのユーザー)を作成します。
『destroy』で、roomを削除します。

app/controllers/rooms_controller.rb
  def index
    @user = current_user
    @currentEntries = current_user.entries
    #@currentEntriesのルームを配列にする
    myRoomIds = []
    @currentEntries.each do |entry|
      myRoomIds << entry.room.id
    end
    #@currentEntriesのルーム且つcurrent_userでないEntryを新着順で取ってくる
    @anotherEntries = Entry.where(room_id: myRoomIds).where.not(user_id: @user.id).order(created_at: :desc)
  end

  def show
    @room = Room.find(params[:id])
    #ルームが作成されているかどうか
    if Entry.where(:user_id => current_user.id, :room_id => @room.id).present?
      @direct_messages = @room.direct_messages
      @entries = @room.entries
    else
      redirect_back(fallback_location: root_path)
    end
  end

  def create
    @room = Room.create(:name => "DM")
    #entryにログインユーザーを作成
    @entry1 = Entry.create(:room_id => @room.id, :user_id => current_user.id)
    #entryにparamsユーザーを作成
    @entry2 = Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(:room_id => @room.id))
    redirect_to room_path(@room.id)
  end

  def destroy
      room = Room.find(params[:id])
      room.destroy
      redirect_to users_rooms_path
  end

ルーム一覧(rooms/index.html.erb)

チャットメッセージ一覧.png

app/views/rooms/index.html
<h2>メッセージ一覧</h2>
<% @anotherEntries.each do |e| %>
    <table class="table table-striped">
        <tr>
            <td>
         <!-- 名前からroomの詳細に飛べるようにリンク化 -->
                <%= link_to room_path(e.room.id) do %>
                    <%= attachment_image_tag e.user, :profile_image, format: 'jpeg', class: "rounded-circle", fallback: "no_image.jpg", size: "30x30" %>
                    <%= e.user.name %>
                <% end %>
            </td>
            <td>
               <!-- 最新メッセージ内容の最初の7文字を表示する -->
                <% dm = DirectMessage.find_by(id: e.room.direct_message_ids.last).content %>
                <%= truncate(dm, length: 10) %>
            </td>
            <td>
                <h6 style="color: #C0C0C0;"><%= e.updated_at.strftime("%Y/%m/%d %H:%M") %></h6>
            </td>
            <td>
               <!-- ゴミ箱アイコンクリックでroomを消せるようにする -->
                <%= link_to room_path(e.room.id), method: :delete do %>
                    <i class="fas fa-trash" style="color: black;"></i>
                <% end %>
            </td>
        </tr>
    </table>
<% end %>

ルーム詳細(rooms/show.html.erb)

チャットイメージ図.png

app/views/rooms/show.html
<% @entries.each do |e| %>
   <!-- eachで回したentryユーザーがログインユーザーであれば、@classに"current_user"の文字列を代入する。ログインユーザーであれば、空白を代入する -->
    <% current_user == e.user ? @class = "current_user" : @class = ""  %>
       <!-- eachで回したentryユーザーがログインユーザーでなければ、entryユーザー名を表示する -->
        <% if e.user != current_user %>
           <h5 class="text-left <%= @class %>" id="<%= @class %>" style="font-size: 30px;" data-id="<%= e.user.id %>"><%= link_to "@#{e.user.name}", user_path(e.user_id) %></h5>
        <% else %>
           <h5 class="text-left <%= @class %>" id="<%= @class %>" data-id="<%= e.user.id %>"></h5>
        <% end %>
<% end %>
<!-- メッセージ内容は、パーシャルにします -->
<div id="direct_messages" data-room_id="<%= @room.id %>">
    <%= render @direct_messages %>
</div>
<!-- メッセージフォーム -->
<form>
    <label style="color: white;" id="target">新しいメッセージを作成</label><br>
    <input type="text" id="chat-input" data-behavior="room_speaker" class="form-control">
</form>

<script>
    //トークルーム遷移時に入力フォーム記載場所にスクロールする(最新トークは下にあるため)
    var element = document.getElementById('target'); // 移動させたい位置の要素を取得
    var rect = element.getBoundingClientRect();
    var position = rect.top;
    setTimeout( function() {
        scrollTo(0, position);}
        , 1000);
</script>

メッセージ内容(direct_messages/_direct_message.html.erb)

app/views/direct_messages/_direct_message.html
<p class="<%= direct_message.id %>" style= "color: white; margin-top: 0;">
    <%= attachment_image_tag direct_message.user, :profile_image, fallback: "no_image.jpg", class:"profile-img-circle", size: "40x40" %>
    <%= direct_message.user.name %>
</p>
<p class="<%= direct_message.id %>">
    <span class="balloon1-top">
        <h7 class="dm_content"><%= direct_message.content %></h7><br>
        <h7 style="color: #C0C0C0;"><%= direct_message.created_at.strftime("%Y/%m/%d %H:%M") %></h7>
    </span>
</p><br>

<!-- メッセージの表示位置を指定する -->
<script>
    //メッセージのユーザーidを取ってくる
    var direct_message = <%= direct_message.user.id %>;
    //id="current_user"の内容を取得
    var dm_user = document.getElementById('current_user');
    //dm_userのdata-idを取得する
    var current_user = dm_user.getAttribute('data-id')
    //dm_userがログインユーザーであれば右に表示する
    if(direct_message == current_user){
      $('p.' + <%= direct_message.id %>).css('text-align', 'right');
    }else{
      $('p.' + <%= direct_message.id %>).css('text-align', 'left');
    }
</script>

roomチャンネルの作成

各ユーザーは、複数のケーブルチャネルにサブスクライブできます。各チャネルには機能の論理的な単位がカプセル化され、そこで行われることは、コントローラが通常のMVPセットアップで行うことと似ています。
参考:Railsガイドhttps://railsguides.jp/action_cable_overview.html

今回はspeakメソッドを持っているroomチャネルを作成します。

$ rails g channel room speak

これを実行することで下記のファイルが作成されます。
room_channel.rbに、サーバー側の記述を記載します。
room.coffeeに、クライアント側の記述を記載します。

app/channels/room_channel.rb

app/assets/javascripts/channels/room.coffee

room_channel

speakメソッドで、メッセージ内容(content)、user_id、room_idを作成します。

app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  #接続されたとき
  def subscribed
    # stream_from "some_channel"
    stream_from "room_channel_#{params['room']}"
  end
#切断されたとき
  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    DirectMessage.create! content: data['direct_message'], user_id: current_user.id, room_id: params['room']
  end
end

room.coffee

app/assets/javascripts/channels/room.coffee
document.addEventListener 'turbolinks:load', ->
    if App.room
      App.cable.subscriptions.remove App.room
    App.room = App.cable.subscriptions.create { channel: "RoomChannel", room: $('#direct_messages').data('room_id') },
      #通信が確立された時
      connected: ->
      #通信が切断された時
      disconnected: ->
      #値を受け取った時
      received: (data) ->
        #投稿を追加
        $('#direct_messages').append data['direct_message']
      #サーバーサイドのspeakアクションにdirect_messageパラメータを渡す
      speak: (direct_message) ->
        @perform 'speak', direct_message: direct_message
    $('#chat-input').on 'keypress', (event) ->
      #return キーのキーコードが13
      if event.keyCode is 13
        #speakメソッド,event.target.valueを引数に.
        App.room.speak event.target.value
        event.target.value = ''
        event.preventDefault()

jobの作成・編集

非同期でブロードキャストするためのDirectMessageBroadcastジョブを作成します。

$ rails g job DirectMessageBroadcast 

direct_message_broadcast_job.rb

performメソッドでブロードキャストを実行します。
このとき、direct_messageに単純な文字列ではなく、direct_messages/direct_messagパーシャルのHTMLを返しています。
ApplicationController.renderer.renderメソッドを使うと、コントローラ以外の場所でビューをレンダリングできます。

app/jobs/direct_message_broadcast_job.rb
class DirectMessageBroadcastJob < ApplicationJob
  queue_as :default

    def perform(direct_message)
      ActionCable.server.broadcast "room_channel_#{direct_message.room_id}", direct_message: render_direct_message(direct_message)
    end

    private

    def render_direct_message(direct_message)
      ApplicationController.renderer.render partial: 'direct_messages/direct_message', locals: { direct_message: direct_message }
    end
end

ログインユーザーの情報を取得する(connection.rb)

ApplicationCable::Connectionクラスを使って、認証情報を定義します。
identified_byはコネクションを識別するキーとなるものです。connectメソッドはコネクションの接続時に呼ばれるメソッドです。ここではコネクションの識別キーとして、ログイン時に設定したCookieからuser_idを取り出しています。

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
        if verified_user = User.find_by(id: cookies.signed['user.id'])
          verified_user
        else
          reject_unauthorized_connection
        end
    end
  end
end

最後に

最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
twitter:https://twitter.com/yto_oct
note:https://note.com/yto_oty

参考

ActionCableを用いてリアルタイムチャットの実装
https://freecamp.life/rails-realtimechat/
Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)
https://qiita.com/bisque33/items/1360477c2260b361ec03
[Rails5]Action Cableのサンプルを読み解いてみる
https://qiita.com/bisque33/items/1360477c2260b361ec03
リアルタイムチャットは誰でもつくれる~Action CableでDM機能を作ろう~
https://qiita.com/OgawaNorihiro/items/6d9f85d8e89d1def4f15
【Ruby on Rails】DM機能でDM相手の一覧ページを作成!
https://novice-programmer.com/dm_index/
Railsガイド Action Cable の概要
https://railsguides.jp/action_cable_overview.html

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

【Rails】ユーザーに紐づかない投稿によるエラーの解消法

エラーが発生

下記のようなエラーが発生。
NoMethodError in Boards#index undefined method decorate
d8da0dfcf578fef98beaee1e1a10acf6.png
デコレータはすでに設定済みなのになぜ?となる。

原因

そもそもデコレータの原因ではなく、ユーザがnilだったのが原因だった。ユーザが存在しないので、名前を取りに行こうとしてもできないためこのようなエラーが発生した。

解決法

rails cでBoard.all.destroy_allを行い、ユーザが紐づかない投稿が消えた。その後新規作成を行うと、エラーが発生せずに、表示された。

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

【Rails】ユーザに紐づかない投稿が存在するため起こるエラーの解消法

エラーが発生

下記のようなエラーが発生。
NoMethodError in Boards#index undefined method decorate
デコレータはすでに設定済みなのになぜ?となる。
Screenshot from Gyazo

原因

そもそもデコレータの原因ではなく、ユーザがnilだったのが原因だった。ユーザが存在しないので、名前を取りに行こうとしてもできないためこのようなエラーが発生した。

解決法

rails cでBoard.all.destroy_allを行い、ユーザが紐づかない投稿が消えた。その後新規作成を行うと、エラーが発生せずに、表示された。

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

SIerの僕、プログラミング始めてみた(10月スタート・初投稿)

簡単な自己紹介

SIerに2年間在籍しております、かなたです。
誰も興味がない趣味とか仕事とか、本記事のきかっけを初めに。

  • 好き①:服(Margielaとか)
  • 好き②:映画(おすすめは Into the Wild)
  • 業務①:DataSpiderを用いたデータ連携基盤を構築
  • 業務②:C#デスクトップ系業務アプリ

10月からWEBアプリケーションの勉強を独学で始めまして、よくよくエラー等ひっかかります。
昨日の自分自身に対して、教えるならどのように伝えるかを、ここに表現できればと思っています。
(あとモチベーションアップに繋がれば…という思いです)

作っているもの

SIer(以外もそうだと思うんですが)は基本的に、実績ベースで予定・工数を積み上げ、スケジューリングします。
ですが、実績を記録しているPJ/チームを僕は見たことがありません。
そのため、予実データ化し改善までつながるアプリケーションを開発しよう!と始めてみました。

成果

さてさて、10月からおよそ4ヶ月の成果はざっくり以下の通りです。

プログラミング基本

  • HTML/CSS/JavaScript/Ruby/Railsの基本(Progate)
  • Railsチュートリアル2周(GitHub~Herokuデプロイ)
  • Vueサンプル複写(基礎から学ぶ Vue.js)

  • リーダブルコード
  • 初めてのRuby
  • Webを支える技術
  • Web API: The Good Parts
  • テスト駆動開発
  • リファクタリング
  • エンジニアファースト
  • エンジニアの知的生産術
  • コンピュータはなぜ動くのか
  • デザイン系の本を数冊

アプリケーションの実装状況

  • フロントエンド ・・・ Vue.js + Vuex + VueRouter + Vuetify
  • サーバサイド ・・・ RubyonRails
  • 実装:メールアドレスを利用したユーザの登録〜ログインおよびユーザ情報の更新
  • 詳細:ユーザログイン時には、WebStorageを利用。ログイン永続化を実装。

ざっくりこんな状況です。頑張らないと・・・

さいごに

また、目標・スケジュールをこちらに記載できればと思っています(もしかして、そういった場でない?)
そして、次回からは、冒頭で述べたとおり、自分自身の備忘録として、
学んだことを表現させていただきます。

よろしくお願いいたします。

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

メソッド内にループがある場合の戻り値(返り値)について

皆さんこんにちは! プログラマーを目指して学習している、久保雄貴と申します。

今回は「メソッド内にループがある場合の戻り値(返り値)について」自分が疑問に思ったこと、考えたこと、知ったことについて書いてみようと思います。

あるときrubyの勉強中にエラーが起きました。メソッドの戻り値を使って演算をしようとした時です。
「undefined method '+' for nil:nilclass」 のような表示がされました。
空のオブジェクトと足し算はできません。ということのようです。
頭の中に「?」がたくさん出てきて、自分が戻り値についてよく理解できていないことがわかりました。
そこでいろいろ考え、実験して、調べてみたわけです。

皆さんのお役に立てる内容であれば嬉しいです。

なお、間違っている点や「こういう風に書いたらいいよ」などアドバイスがありましたら、コメントをください。

では始めましょう!

1. 戻り値とは

戻り値とは、オブジェクトやメソッドが処理された後の最終的な値のことです。返り値とも言います。
Rubyのオブジェクト自体、またはメソッドを利用した「式」には、全て戻り値があります。

Rubyにおける「式」とは文字列や数値の他に、変数やメソッド呼び出し、演算子式などが含まれます。
すなわち、"あいうえお", 5296, (4 + 24)などはすべて式になります。
そしてそれらすべてに戻り値があるということです。(上の例でいうと、"あいうえお", 5296, 28)

2. メソッドの戻り値はどうなる?

では、メソッドの戻り値はどうなるのでしょうか。
Ruby Referenceで調べてみました。

http://ruby-for-beginners.rubymonstas.org/writing_methods/return_values.html

以下のように書かれています。

In Ruby, a method always return exactly one single thing (an object).
Also note that in Ruby we do not have to use the statement return, as in other languages. In fact, most Ruby code does not use the keyword return at all.

This is extremely convenient, but it is also something we need to learn:

If we don’t do anything else, then a method will return the value that was returned from the last evaluated statement. Most often, this is the last line in the method body.

端的に言えば、
「メソッドは一つの値を返が、他の言語と違って、returnと記述する必要はない。
もし記述しなかったら、最後の式の値が返される。」
という感じでしょうか。

例えば、

def test(number)
  number + 1      ←メソッドの中の最後の式
end

puts test(5)

これを実行すると、6と表示されますね。
"number + 1" の答えである "6" がtestメソッドの戻り値となっているわけです。

3. 実験: メソッドの戻り値の判定

冒頭で書きましたが、メソッドの戻り値を使って演算をしようとした時に、「戻り値は「nil」ですよ。」とエラーが出ました。実は、その時はメソッド内にwhile文(ループ)がありました。
条件を満たすまで、命令を実行し、条件を満たしたら、ループから抜け出して、メソッドの呼び出し元にもどる。
その戻り値で演算をしようとしてエラーが出たわけです。

そこでメソッドの戻り値が「nil」かそうでないかを判断してみたらどうかと思いつきました。

メソッドの戻り値が「nil」なのかそうでないのかを判断するコードを考えましたので、みていきたいと思います。
まずはwhile文なしのパターンです。

def test(count)
    puts "0より大きい数字を入力してください。"
    count = gets.to_i
end

count = test(count)

if count.nil?
  puts "test(count)の戻り値はnilです。"
else
  puts "test(count)の戻り値は#{count}です。"
end

「.nil?」はcount(レシーバ)の中身が「nil」の場合trueを返し、「nilでない」場合falseを返すメソッドです
例えば、ターミナルで実行して、24という数字を入れたとすると、

代替テキスト

と表示されます。

上のコードでは予想通り、returnの記述がなくても戻り値は「nil」ではありませんでした。


では次はどうでしょう。
testメソッドの中にwhile文を入れて、10より大きい数字が入るまで、入力させ続けます。
そして、その戻り値が「nil」かどうかを判断します。

def test(count)
  while count <=10
    puts "10より大きい数字を入力してください。"
    count = gets.to_i
  end

end

count = 0
count = test(count)

if count.nil?
puts "test(count)の戻り値はnilです。"
else
puts "test(count)の戻り値は#{count}です。"
end

ターミナルで実行してみましょう。
今回は、「4→24」の順で入れてみます。(while文が機能しているか判断するため。)

代替テキスト

なんと今回は戻り値が「nil」になりました!




次は、while文の後に「return count」を入れてみます。

def test(count)
  while count <=10
    puts "10より大きい数字を入力してください。"
    count = gets.to_i
  end
  return count   ←追加しました
end

count = 0
count = test(count)

if count.nil?
puts "test(count)の戻り値はnilです。"
else
puts "test(count)の戻り値は#{count}です。"
end

実行してみましょう。

代替テキスト

当然のことながら、戻り値は「24」ですね。

4. 結果

さて、もう一度まとめてみましょう。
メソッドの戻り値が「nil」かどうかを判断しました。

順番 条件 結果
while文なし 戻り値: 24
2 while文あり かつ returnメソッドなし 戻り値: nil
3 while文あり かつ returnメソッドあり 戻り値: 24

5. 考察

さて、上記の結果になりました。
ここからは、あくまで自分で考えた内容です。(信憑性はとても低いです)

Rubyは、メソッド内において式から得られる「値」をどこかに仮置きしているのではないか。
例えば、1の実験では

def test(count)
    puts "0より大きい数字を入力してください。"
    count = gets.to_i      ←ここで入力された値をどこかに仮置きしている。
end

count = test(count)

if count.nil?
  puts "test(count)の戻り値はnilです。"
else
  puts "test(count)の戻り値は#{count}です。"
end

1) 3行目で「値」をどこかに仮置きする。
2) もしメソッドの中に次の式があれば、仮置きしている「値」を破棄し、その次の「値」をまた仮置きする。(今回はなし)
3) メソッドが終わったら、一番最後に仮置きした「値」をメソッドの戻り値として返す。

と行った感じです。

ただ、メソッドの中にループ文がある場合は、ループから抜け出す時になぜかその仮置きしておいた「値」を破棄してしまうのではないか。
なぜかはわかりません。。。(考察になっていない?)

6. 結論

実は、このブログを書く前にいろいろと参考になるものはないかと探していたところ、答えになるものを見つけました。
Rubyのリファレンスマニュアルです。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html

この中のwhileに関する説明の一番下にこう書かれています。

代替テキスト

while は nil を返します。

はっきり書かれてますね(笑)

なぜそういうふうになっているのかは書いてありませんが、そうなっているもんは仕方ない。
今回はそう割り切りましょう。

ということで、
結論は、「Rubyのルールでそうなっているから」です。



7. 感想

なんか予想外な感じになってしまいました。
でも、まあ、すっきりできたかな?




さて、気を取り直して、
今後学習を進めていくにあたって、事情がない限り、returnメソッドを書いておこうと思いました。
実は、"return count" と書かずに直接 "count" と書いても同じ結果になります。
でもそれだとその記述が何をしているのかわかりません。

ループの有無に関わらず、他のプログラマーのためにも、コードを見やすくしたり、そのメソッドが何をしているのかがわかりやすいように書いた方が良さそうです。



いかがだったでしょうか。
今回二回目の投稿ですが、学んだことは、
1) 疑問に思ったことは、自分で仮説を立てて、実験して、結果から自分なりの答えを出すことがとても大事ということ。
2) その過程は実は楽しいこと。
3) その過程の中でいろいろと調べるので、途中で答えが見つかることもあるということ。
4) ブログという形でアウトプットすることは思った以上に緊張するし怖い部分があるが予想以上に学びがあること
です。

いいねをもらえたり、ストックしていただけるのもとても励みになります。
ありがとうございます。m(_ _)m

今回は以上です。ありがとうございました。




おまけ

リファレンスマニュアルに

また、引数を伴った break により while 式の戻り値をその値にすることもできます。

と書かれていたので調べてみました。

https://hydrocul.github.io/wiki/programming_languages_diff/control_flow/break.html#ruby

breakの後ろに数字あるいは変数を書くといいみたいです。

def test(count)
  while count <=10
    puts "10より大きい数字を入力してください。"
    count = gets.to_i
    break count    ←ここです
  end

end

count = 0
count = test(count)

if count.nil?
puts "test(count)の戻り値はnilです。"
else
puts "test(count)の戻り値は#{count}です。"
end

やってみましょう。

代替テキスト

「nil」になりませんでした!



もうお気づきかもしれませんが、これには問題が。。。

代替テキスト

そうです。ループされないんです。
ループの意味なし。
なんじゃこりゃ。。。。

ということで 本当におしまいです。ありがとうございました。



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

Spreeで使われているfactorybot

spreeで使われているfactorybot。
勉強のために切り抜きました。参考までに。

product_factory.rb
FactoryBot.define do
  factory :base_product, class: Spree::Product do
    sequence(:name)   { |n| "Product ##{n} - #{Kernel.rand(9999)}" }
    description       { generate(:random_description) }
    price             { 19.99 }
    cost_price        { 17.00 }
    sku               { generate(:sku) }
    available_on      { 1.year.ago }
    deleted_at        { nil }
    shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) }

    # ensure stock item will be created for this products master
    before(:create) { create(:stock_location) unless Spree::StockLocation.any? }

    factory :custom_product do
      name  { 'Custom Product' }
      price { 17.99 }

      tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) }
    end

    factory :product do
      tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) }

      factory :product_in_stock do
        after :create do |product|
          product.master.stock_items.first.adjust_count_on_hand(10)
        end
      end

      factory :product_with_option_types do
        after(:create) { |product| create(:product_option_type, product: product) }
      end
    end
  end
end

taxon_factory.rb
FactoryBot.define do
  factory :taxon, class: Spree::Taxon do
    sequence(:name) { |n| "taxon_#{n}" }
    association(:taxonomy, strategy: :create)
    parent_id { taxonomy.root.id }
  end
end
taxonomy_factory.rb
FactoryBot.define do
  factory :taxonomy, class: Spree::Taxonomy do
    sequence(:name) { |n| "taxonomy_#{n}" }
  end
end

詳細は本家githubのファイルで。
https://github.com/spree/spree/tree/master/core/lib/spree/testing_support/factories

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

CarrierWaveでデフォルト画像の設定

CarrierWaveでユーザーアイコンの実装をしていたのですが、デフォルト画像の表示で少し躓いてしまったのでメモしておきます。

前提

  • CarrierWaveは導入済み
  • 登録した画像表示はできる

image_uploader.rbの設定

CarrierWaveを入れたときに

$ rails g uploader image

をしたらimage_uploader.rbというファイルが生成されます。

このファイルを以下のようにいじってください。

image_uploader.rb
#アップロードした画像の表示
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

#デフォルト画像の設定
  # Provide a default URL as a default if there hasn't been a file uploaded:
  def default_url(*args)
  #   For Rails 3.1+ asset pipeline compatibility:
    ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  end

これで登録時にファイル選択をしなかった場合、デフォルト画像をimageとして渡す準備ができました。

assets/images配下に画像を設置

assets/images配下にデフォルト画像を設置してください。
画像名はこの場合「default.png」にしときましょう。

Viewの設定

Viewに、if文でユーザーのimageがある場合とない場合で表示が変わるように設定してあげましょう。

以下は例です。

_show.html.erb
<% if post.user.image? %>
  <img src='<%= post.user.image %>' class="icon" alt="ユーザーアイコン">
<% else %>
  <image src="/assets/default.png" class="icon" alt="ユーザーアイコン" %>
<% end %>

まとめ

CarrierWaveさんは非常に便利なのですが、デフォルト画像設置に関しては画像パスとして渡っているので、dbを調べてもnullとしか表示されず焦ってしまうというパターンがよくあるようです。

よく使う機能だとは思いますので、誰かの助けになれば幸いです。

ではでは。

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

【リリース】Qiitaの新着記事をニュース感覚で聞き流せるwebアプリ【聞い太?】

Qiitaの新着記事をニュース感覚で聞き流せるwebアプリ【聞い太?】

Qiitaの新着記事をニュース感覚で聞き流せるwebアプリを作りました。

【聞い太?】
https://technews-app.herokuapp.com/

記事選択して▶️ボタンを押すと、選択した記事を読み上げます。(safariは一部制限あり)

また、記事タイトルやタグに関連する技術書を表示しています。

満員電車の中やながら作業も、インプット時間として有効活用出来ます★

新着順なので本家にはあまり出てこない記事も発見できるかも?

日々のスキルUPに是非?

使った技術

Ruby on Rails(6.0.2.1)
Ruby(2.6.4)
JavaScript / jquery
Heroku
S3
QiitaAPI
Web Speech API
Amazon Polly
Selenium Webdriverなど。

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

Sidekiqのダッシュボードでエラーが出ていたので対応した

最近Sidekiqのダッシュボード(Sidekiq::Web)がエラーで表示されない現象が発生したので、調査結果と対応をまとめておきます。同じ現象に困っている人の参考になれば:pray:

バージョン

ライブラリ バージョン
rails 5.2.2.1
sidekiq 5.2.5
redis 4.1.0
rack 2.0.8

エラー内容

マウントしたパスにアクセスすると、以下のRuntime Errorが出ます。どうやらRackのセッション周りでエラーになっているようです。

エラーが出たRailsアプリケーションではセッション管理にRedisを使っているのですが、試しに別の形式に変えるとエラーが消えました。

RuntimeError (Most recent call first)
Hide 33 non-project frames
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/session/abstract/id.rb line 31 in to_s
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/redis-session-store-0.11.0/lib/redis-session-store.rb line 87 in prefixed
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/redis-session-store-0.11.0/lib/redis-session-store.rb line 123 in set_session
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/session/abstract/id.rb line 381 in commit_session
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/session/abstract/id.rb line 261 in context
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/session/abstract/id.rb line 253 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/cookies.rb line 670 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/callbacks.rb line 28 in block in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/activesupport-5.2.2.1/lib/active_support/callbacks.rb line 98 in run_callbacks
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/callbacks.rb line 26 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rollbar-2.19.2/lib/rollbar/middleware/rails/rollbar.rb line 24 in block in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rollbar-2.19.2/lib/rollbar.rb line 146 in scoped
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rollbar-2.19.2/lib/rollbar/middleware/rails/rollbar.rb line 22 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/debug_exceptions.rb line 61 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rollbar-2.19.2/lib/rollbar/middleware/rails/show_exceptions.rb line 22 in call_with_rollbar
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/show_exceptions.rb line 33 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/railties-5.2.2.1/lib/rails/rack/logger.rb line 38 in call_app
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/railties-5.2.2.1/lib/rails/rack/logger.rb line 28 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/remote_ip.rb line 81 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/request_store-1.4.1/lib/request_store/middleware.rb line 19 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/request_id.rb line 27 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/method_override.rb line 22 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/runtime.rb line 22 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/activesupport-5.2.2.1/lib/active_support/cache/strategy/local_cache_middleware.rb line 29 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/actionpack-5.2.2.1/lib/action_dispatch/middleware/executor.rb line 14 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/rack-2.0.8/lib/rack/sendfile.rb line 111 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/railties-5.2.2.1/lib/rails/engine.rb line 524 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/stackprof-0.2.12/lib/stackprof/middleware.rb line 22 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/puma-3.12.2/lib/puma/configuration.rb line 227 in call
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/puma-3.12.2/lib/puma/server.rb line 674 in handle_request
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/puma-3.12.2/lib/puma/server.rb line 476 in process_client
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/puma-3.12.2/lib/puma/server.rb line 334 in block in run
File /var/www/app/vendor/bundle/ruby/2.6.0/gems/puma-3.12.2/lib/puma/thread_pool.rb line 135 in block in spawn_thread

Sidekiqのセッションを無効にしてみる

セッション周りっぽいので、とりあえずSidekiqのセッションを無効にしてみます。

diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 218cdc9960..a4a40e4195 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true

 require 'sidekiq/web'
-Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
+Sidekiq::Web.set :sessions, false # 上の一行をこちらに書き換えてセッションを無効化

 Rails.application.routes.draw do
   ActiveAdmin.routes(self)

無事表示されました。引き続き調べます。

rack/rack#1432

調べていくとRackに関係のありそうなIssueが上がっているのを見つけました。
https://github.com/rack/rack/issues/1432

どうやら2.0.8のセキュリティフィックスで仕様が変わり不具合が起こるようになったとのこと。以下のコミットで解消されており、修正は2.0.9に含まれているようです。
https://github.com/rack/rack/pull/1462

ということで、Rackのバージョンを上げれば良さそうなので上げてみます。

$ bundle update rack

バージョンが2.0.8から2.0.9に上がりエラーが解消されました。やったね:clap:

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

validates メソッドでユニーク制約を行うと ActiveRecord::RecordNotUnique ではなく、 ActiveRecord::RecordInvalid が raise される

概要

DBのユニーク制約だけを行なっていた状態から、ユニーク制約の validates メソッドを書いたときにエラーハンドリングで少しはまったので共有です。

解説

DBのユニーク制約のみがかかっているとき

user.rb
# == Schema Information
#
# Table name: users
#
#  id         :bigint(8)        not null, primary key
#  name      :string(255)      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
# Indexes
#
#  index_users_on_name    (name) UNIQUE
#

class User < ApplicationRecord
end

ActiveRecord::RecordNotUnique が raise されます

sample.rb
User.create!(name: 'Duplication')
User.create!(name: 'Duplication')

=> ActiveRecord::RecordNotUnique

ユニーク制約の validates メソッドがあるとき

user.rb
# == Schema Information
#
# Table name: users
#
#  id         :bigint(8)        not null, primary key
#  name      :string(255)      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
# Indexes
#
#  index_users_on_name    (name) UNIQUE
#

class User < ApplicationRecord
  validates :name, uniqueness: true # 追加
end

ActiveRecord::RecordInvalid が raise されます

sample.rb
User.create!(name: 'Duplication')
User.create!(name: 'Duplication')

=> ActiveRecord::RecordInvalid

注意点

例えば下記のようにユニーク制約に引っかかったときの例外処理を書いているコードがあるときは validates メソッドでユニーク制約をかけると ActiveRecord::RecordNotUnique ではキャッチできなくなるので注意です。

sample.rb
begin
  User.create!(name: 'Duplication')
rescue ActiveRecord::RecordNotUnique
  head :conflict
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #19 Docker編

こんな人におすすめ

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

前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
番外:#18.5 環境変数, Gmail送信設定編
次回:準備中

今回の流れ

  1. 完成のイメージを理解する
  2. Dockerを導入する理由を知る
  3. RailsアプリをGitHubからクローンする
  4. ローカルの開発環境を整える
  5. Dockerをインストールする
  6. Dockerの仕組みを理解する
  7. Dockerのファイルを作成する
  8. Dockerを起動する
  9. トラブルシューティング

この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回は、Railsアプリの開発環境にDockerを組み込みます。
ローカル(ホスト)にはMacを使います。

完成のイメージを理解する

はじめにDockerの必要性を知ります。
ローカル(Mac)にRailsアプリがない、動く環境にない方は先に環境を整えます。

次にDockerを構成します。
具体的には、プロセスをapp(RailsやPumaなど)db(MySQL)nginx(Nginx)の3つに分け、Docker Composeで定義します。
ここでDockerを構成しながら、Dockerの理解を深めます。

最後にDockerを起動します。
起動がスムーズに行くことは稀で、何らかのエラーが発生します。
トラブルシューティングを活用しながら、起動を成功させます。

以上です。

Dockerを導入する理由を知る

Dockerは、軽くてポータビリティのある開発環境ツールです。
Dockerは、モダンな企業のほとんどが使うツールです。
Dockerを、ポートフォリオに組み込むことで、転職先の企業とのマッチング率を高めるという狙いがあります。

さてDockerの動作については、こちらの記事が分かりやすいので、一読します。
いまさらだけどDockerに入門したので分かりやすくまとめてみた

RailsアプリをGitHubからクローンする

RailsアプリをローカルであるMacにクローンします。
※ すでにローカルでRailsアプリを開発している方は飛ばしてください。

local(自分のMacターミナル)
$ vi ~/.gitconfig
.gitconfig
[user]
  name = Gitに登録している名前
  email = Gitに登録しているメールアドレス

[url "github:"]
  InsteadOf = https://github.com/
  InsteadOf = git@github.com:
local
$ cd ~/.ssh
$ ssh-keygen -t rsa
Enter file in which to save the key ():git_rsa
Enter passphrase (empty for no passphrase): 
# 何もせずエンター
Enter same passphrase again: 
# 何もせずエンター
$ vi config
.ssh/config
# 以下を追加
Host github
  Hostname github.com
  User git
  IdentityFile ~/.ssh/git_rsa
local
$ cat git_rsa.pub
# 中身をコピー

GitHubにログイン
右上アイコン『Settings』 → 『SSH and GPG keys』 → 『New SSH key』

Title:任意
Key:コピーした公開鍵をペースト
local
$ cd ~
$ git clone -b ブランチ名 git@
github.com:GitHubのユーザー名/アプリ名.git

以上でRailsアプリのクローンは完了です。

参考になりました↓
リモートから特定のブランチを指定してcloneする

ローカルの開発環境を整える

ローカル(Mac)の開発環境を整えます。
ここでの手順は以下の通りです。
※ すでにローカルでRailsアプリを開発している方は飛ばしてください。

  • Homebrewをインストールする
  • Rubyをインストールする
  • Bundlerをインストールする
  • MySQLをインストールする

Homebrewをインストールする

Homebrew(公式)をインストールします。
HomebrewはMacでよく利用されるパッケージ管理です。

local(Mac)
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Rubyをインストールする

Rubyをインストールします。
RubyはMacに標準で入っていますが、バージョン管理のためにrbenvを使います。

local(Mac)
$ brew install rbenv ruby-build
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source .bash_profile
$ rbenv install 使いたいバージョン
$ rbenv global 使いたいバージョン
$ rbenv rehash

Bundlerをインストールする

Bundlerをインストールします。

$ gem install bundler

MySQLをインストールする

MySQLをインストールします。

$ brew install mysql

参考になりました↓
Ruby初学者のRuby On Rails 環境構築【Mac】

Dockerをインストールする

Docker Desktop for Macからアカウントを作成し、インストールします。
手順は以下の通りです。

  1. 『Get Started』からDcokerHubアカウントを作成する
  2. 『Get Docker』からDockerをインストールする

参考になりました↓
DockerをMacにインストールする

Dockerの仕組みを理解する

それではDockerを使って、環境を構築します。
仕組みとしては、app(RailsとPuma)db(MySQL)nginx(Nginx)のイメージをDockerfileなどで用意し、Docker Composeでプロセスを起動させます。

早速、以下のようにホスト側のディレクトリを構成します。
Dockerの各ファイル、用語は後述します。

lantern_app
├── config
│   ├── database.yml(既存)
│   └── puma.rb(既存)
├── containers
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
├── Dockerfile
├── environments
│   └── db.env
├── Gemfile(既存)
├── Gemfile.lock(既存)
└── 他

用語を説明します。

Image
色々な環境を提供してくれる入れ物のことです。
イメージからRubyやNginxなどを取得します。

Dockerfile
イメージを作るためのファイルです。
イメージを元に機能を拡張するなどの用途があります。

Volume
データ永続のための保存場所です。
Dockerのデータはコンテナの終了とともに消えるため、必要に応じて設定します。

Container
イメージを元に作られたプロセスのことです。
イメージを起動すると、環境がプロセスとしてコンテナに隔離されます。

Docker Compose
複数のコンテナを定義・実行するツールです。

Dockerのファイルを作成する

それではDockerのディレクトリとファイルを作成します。

local(Mac)
$ touch Dockerfile docker-compose.yml
$ mkdir containers containers/nginx environments
$ cd containers/nginx
$ touch Dockerfile nginx.conf
$ cd ../..
$ touch environments/db.env

各ファイルを編集します。
各ファイルの詳細は、後述します。

Dockerfile
# 元にするイメージ
FROM ruby:2.6.3
# コンテナを機能させるまでの準備のコマンドを実行する
RUN apt update -qq && \
    apt install -y build-essential nodejs
RUN mkdir /lantern_docker
# 環境変数を設定する
ENV APP_ROOT /lantern_docker
# コマンドを実行するディレクトリを設定する
WORKDIR $APP_ROOT
# ホストのファイルをコンテナにコピーする
ADD Gemfile $APP_ROOT/Gemfile
ADD Gemfile.lock $APP_ROOT/Gemfile.lock
# 既出
RUN gem install bundler
RUN bundle install
ADD . $APP_ROOT
RUN mkdir -p tmp/sockets
# コンテナ起動時のポートを設定する
EXPOSE 3000

RailsアプリはRubyイメージを元にして作成します。
RUN apt ...でRailsが動く環境をインストールし、ADD . $APP_ROOTでGemfileなどをコンテナにコピーし、RailsやPumaなどを動作させます。

docker-compose.yml
# Docker Composeのバージョン
version: '3'

# Docker Composeではコンテナをサービスとして扱う
services:
  # サービス名をappとして定義
  app:
    # 参照するDockerfileを指定(docker-compose.ymlから基準)
    build: .
    # 環境変数が設定されているファイルを指定
    env_file:
      - ./environments/db.env
    # ボリュームをマウント(後述)
    volumes:
      - .:/lantern_docker
    # サービス名dbが作成されたらappを作成
    depends_on:
      - db
    # コンテナを起動させ続ける際に使用
    tty: true
    # コマンドを実行
    command: bundle exec puma -C config/puma.rb

  db:
    # イメージを指定(後述)
    image: mysql
    env_file:
      - ./environments/db.env
    # ボリュームをマウント(後述)
    volumes:
      - db-data:/var/lib/mysql

  nginx:
    build: containers/nginx
    # ホスト:コンテナのポートを指定
    ports:
      - "80:80"
    depends_on:
      - app

# ボリュームとして扱うボリューム名
volumes:
  db-data:

Docker Composeの各サービスは、DockerfileまたはDockerHubのイメージを参照します。
ここでは、appとnginxがDockerfile、dbがイメージを参照しています。

appのボリュームは、ホスト側のホームディレクトリ全てをボリューム化しています。
dbのボリュームは、コンテナ側のディレクトリ/var/lib/mysqlをdb-dataというボリューム名でボリュームをマウントしています。

この辺りはややこしく、参考記事を読むことをおすすめします。

参考になりました↓
Docker、ボリューム(Volume)について真面目に調べた
Dockerの-vや--volumeオプションはわかりづらいから、--mountを使おう
バインドマウント | Docker docs(日本語)
Pumaの起動におけるpumaコマンドとpumactlコマンドの違い

containers/nginx/Dockerfile
FROM nginx
RUN rm -f /etc/nginx/conf.d/*
ADD nginx.conf /etc/nginx/conf.d/lantern_docker.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

Nginxはフォアグラウンドでの動作を期待しているので、'deamon off;'とします。
フォアグラウンドとバックグラウンドの違いは、PCで表すとこんな感じです。

  • フォアグラウンド:一番上のウインドウ
  • バックグラウンド:それ以外のウインドウ

参考になりました↓
フォアグラウンド(foreground)とは | 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

containers/nginx/nginx.conf
# Pumaとやり取りするための通信路
upstream lantern_docker {
  server unix:///lantern_docker/tmp/sockets/puma.sock;
}
# サーバーに関する情報
server {
  # ポート
  listen 80;
  # 処理するサーバー名
  server_name localhost;
  # ログに関するファイルの場所
  access_log /var/log/nginx/access_log;
  error_log /var/log/nginx/error_log;
  # ドキュメントルートの設定
  root /lantern_docker/public;
  try_files $uri/index.html $uri @app;

  # 内部リダイレクトの処理
  location @app {
    # バックエンドサーバーに送信するヘッダーを定義し直す
    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;
    # プロキシのパス(Pumaに渡す)
    proxy_pass http://lantern_docker;
    proxy_redirect off;
  }
}

アプリ名を自分のものに変更します。
Nginxの設定は#18で説明済みなので、省略します。

environments/db.env
MYSQL_USER=ユーザー名
MYSQL_ROOT_PASSWORD=パスワード
MYSQL_HOST=エンドポイント

環境変数の設定です。
エンドポイントが分からない場合、#17.5をご覧ください。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  timeout: 5000
  reconnect: false
  pool: 5
  socket: /var/lib/mysql/mysql.sock
  username: <%= Rails.application.credentials.mysql[:user_name] %>
  password: <%= Rails.application.credentials.mysql[:password] %>
  host: <%= Rails.application.credentials.mysql[:host] %>

development:
  <<: *default
  database: lantern_development

test:
  <<: *default
  database: lantern_test

production:
  <<: *default
  database: lantern_production

MySQLの設定です。
credentailsが未設定の場合は、#18.5をご覧ください。

config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  ActiveRecord::Base.establish_connection
end

plugin :tmp_restart

app_dir = File.expand_path("../..", __FILE__)
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
pidfile "#{app_dir}/tmp/pids/puma.pid"
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true

Pumaの設定です。
ここも#18で説明済みなので、省略します。

以上で、Dockerのファイル編集は完了です。

Dockerを起動する

Docker Composeを使ってDockerコンテナを起動します。

local(Mac)
# Dockerfileからイメージをビルド
$ docker-compose build
# Docker Composeのコンテナを起動(-dでバックグラウンド起動)
$ docker-compose up -d
# Docker Composeのコンテナのプロセスを確認
$ docker-compose ps

以上でDockerの組み込みは完了です。
後は各コンテナに入り、開発を進めてください。

shell
# appのbashに入る
$ docker-compose exec app bash
# 例
$ rails db:migrate

参考になりました↓
Docker + Rails + Puma + Nginx + MySQL
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
既存のRailsアプリにDockerを導入する手順
Nginx + Rails (Puma) on Docker のいくつかの実用パターン
Docker + Rails + Puma + Nginx + Postgres
Docker ComposeでNginx Rails MySQL環境を構築してみる

トラブルシューティング

Dockerで起動する際、様々なエラーが発生します。
一例ですが、私が見舞ったトラブルの解決策を書き残します。

よく使うスクリプトを確認する

先によく使うスクリプトを紹介します。
まずはDockerやDocker Composeのコマンドを紹介します。

shell
# ログの確認
$ docker-compose logs
# サービスの確認
$ docker-compose ps -a
# コンテナの確認
$ docker ps -a
# コンテナとネットワークの停止と削除
$ docker-compose down --rmi all
# コンテナの削除
$ docker rm ID名
# イメージの削除
$ docker rmi ID名
# コンテナのbash内に入る
$ docker-compose exec サービス名 bash

続いてbashを紹介します。

bash
# ディレクトリの有無を確認する(出力0=有、出力1=無)
$ test -f パス;echo $?
# プロセスが使用しているポートを確認する
$ lsof -i
# ポートを指定してプロセスを確認する
$ lsof -i:ポート番号

以上です。
以降から、具体的なトラブルシューティングに入ります。

参考になりました↓
Docker実践〜dockerのコンテナ環境をきれいに消す
docker-compose up したコンテナを起動させ続ける方法
Linux/UNIXでファイル・ディレクトリの存在確認をする
Linuxでプロセスが何のポート使っているかを調べる

/usr/local/lib/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': Could not find 'bundler' (2.1.4) (Gem::GemNotFoundException)

Bundlerが見当たらないのでエラーが発生します。
GemからBundlerをインストールします。

Dockerfile
# 以下を追加
RUN gem install bundler

参考になりました↓
Docker + Rails のdocker-compose build でGemNotFoundExceptionの時の対処

mysql client is missing.

MacにMySQLがないためにエラーが発生します。
HomebrewでMySQLをインストールします。

local(Mac)
$ brew install mysql

参考になりました↓
mysql2 が原因でbundle installにてエラーを吐く(Mac OS X)

uses an image, skipping

こちらはエラーではありません。
すでにビルドが済んだイメージをリモートで取得するので、upすれば問題ありません。

local(Mac)
$ docker-compose up -d

参考になりました↓
docker-compose.yml起動時にuses an image, skippingされる現象
docker-compose `up` とか `build` とか `start` とかの違いを理解できていなかったのでまとめてみた。

error: database is uninitialized and password option is not specified

データベースのエラーです。
データベースが初期化されていない、パスワードがないため発生します。
環境変数のあたりを確認します。

environments/db.env
MYSQL_USER=ユーザー名
MYSQL_ROOT_PASSWORD=パスワード
MYSQL_HOST=エンドポイント
docker-compose.yml
# 中略
  db:
    image: mysql
    env_file:
      - ./environments/db.env
# 中略

参考になりました↓
docker, docker-composeでmysqlが起動しない
docker-compose.ymlで.envファイルに定義した環境変数を使う
How to link MySQL RDS in docker-compose.yml file?

NoMethodError: Cannot load database configuration:

データベースのエラーです。
データベースに関する設定に問題があります。

解決策1:ttyをtrueにする

ttyをtrueにすると解決するかもしれません。

docker-compose.yml
app:
# 中略
    tty: true
# 中略

参考になりました↓
docker-compose up したコンテナを起動させ続ける方法
docker-composで起動したコンテナがすぐに停止する

解決策2:database.ymlのERbを削除する

コメントアウトしていてもERbは評価されます(恐らく)。
database.yml内のERbは削除します。

config/database.yml
# 以下で書かれた必要ない箇所を削除する
<%= %>

参考になりました↓
Rails を起動したら Cannot load `Rails.application.database_configuration`: (NoMethodError) が出てハマった

502 Bad Gateway

何らかのサーバーに関するエラーです。
私の場合は、以下のように変更すると解決しました。

nginx.conf
upstream lantern_docker {
  # ソケットからサービス名に変更
  # server unix:///lantern_docker/tmp/sockets/puma.sock;
  server app:3000;
}
# 中略

参考になりました↓
Docker Compose と nginx でリバースプロキシを作ろうとしたお話(出題編)


前回:#18 EC2環境構築, Nginx+Puma+Capistrano編
番外:#18.5 環境変数, Gmail送信設定編
次回:準備中

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

db:create で ActiveRecord::NoDatabaseError の原因が factory_bot_rails だった話

ググっても引っかからなかったので取り急ぎメモ

結論

Gemfile
- gem 'factory_bot_rails'
+ gem 'factory_bot_rails', require: false

背景

bin/rails db:createbundle exec rake db:create 等で ActiveRecord::NoDatabaseError: Unknown database 'xxxxx' というエラーが発生した。いや、その DB を作れと言ってるんだが...?

スタックトレースを追うと factory_bot に関する怪しげな部分が見つかった。以下は GitHub Actions 上での例。

/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/trait.rb:12:in `instance_eval'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/trait.rb:12:in `initialize'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/definition_proxy.rb:173:in `new'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/definition_proxy.rb:173:in `trait'
/home/runner/work/xxxx/xxxx/spec/factories/articles.rb:70:in `block (2 levels) in <main>'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:18:in `instance_eval'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:18:in `factory'
/home/runner/work/xxxx/xxxx/spec/factories/articles.rb:2:in `block in <main>'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:49:in `instance_eval'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:49:in `run'
/home/runner/work/xxxx/xxxx/vendor/bundle/ruby/2.5.0/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:7:in `define'

応急的に require するまで factory_bot_rails をロードしないようにして解決した。

Gemfile
- gem 'factory_bot_rails'
+ gem 'factory_bot_rails', require: false

あとは FactoryBot を使いたい場所で require すれば良い。

spec/support/factory_bot.rb
+ require 'factory_bot_rails'
+
  RSpec.configure do |config|
    config.include FactoryBot::Syntax::Methods
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FontAwesomeの導入

FontAwesomeとは?

ウェブフォントの一種。
文字を扱うのと同じようにアイコンを表示させることができる。

導入方法

FontAwesome用のGemがをインストールする。
全ての環境で使用したいので、最下部に以下のコードを記載する。

Gemfile
gem 'font-awesome-sass'

bundle installとサーバ再起動を忘れずに。

assetsフォルダに書き込み

application.scss
@import "font-awesome-sprockets";
@import "font-awesome";

補足

CSSの仕様で、paddingなどの設定を行った場合、その要素全体のサイズが
大きくなる場合がある。以下の記述を追加すると、paddingやborderの設定を行っても、
要素の大きさがwidthやheightで指定したサイズが維持される。

messeges.scss
* {
  box-sizing: border-box;
}

「*」は、全ての要素に適用させるという意味

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

rails 基礎講義資料2章 Scaffold を用いた高速なアプリケーション構築 及び MVCの理解

1章:環境構築
2章:Scaffold を用いた高速なアプリケーション構築 及び MVCの理解
3章:Scaffold を用いない開発方法 及び 応用

1.Scaffold

この章では、Railsの強力な機能をいくつか紹介するためのアプリケーションを作成します。
大量の機能を自動的に生成するscaffoldジェネレータというスクリプトを使って蔵書管理アプリケーションを生成し、
それを元にRailsの概要を一緒にみていきましょう。

*3章以降では、scaffold は使わずに、自力で作る方法を学びます。

scaffold コマンド

まずは、名前と学籍番号を登録できるだけの機能を作成します。
下記のコマンドは一字一句間違いなくタイプするか、コピペで行ってください。

失敗した場合は、下記のページに解決策が記載してあります。(すこし複雑です)
scaffold作成時にカラム名(属性名)を打ち間違えた場合の修正手順

terminal
$bin/rails generate scaffold User name:string number:integer

Railsのscaffoldは、rails generateスクリプトにscaffoldコマンドを渡すことで生成されます。scaffoldコマンドの引数には、リソース名を単数形にしたもの (この場合はUser) を使い、必要に応じてデータモデルの属性をオプションとしてパラメータに追加します

リソースとは、コントローラが扱う対象に名前をつけたものです。
例:モデル、画像、セッション...etc

自動で多数のファイルが生成されたとおもいます。
その中で重要なのは次のファイルです。

terminal
    create    app/models/user.rb
    create    app/views/users/(略)
    create    app/controllers/users_controller.rb
    route     resources :users

ここで生成された Models Views Controllers の頭文字をとって
MVC と呼びます。MVCについては後ほど、説明します。

データベースの利用

このアプリケーションではデータを保存するために、データベースを扱います。
下記コマンドを実行することで、データベースに users という名前のテーブルが作成されます。

terminal
$bin/rails db:migrate

下記のような結果が表示されていればテーブルの生成ができています。
(migrate については今後の章で解説します)

terminal
==  CreateUsers: migrating =============================
-- create_table(:users)
   -> 0.0007s
==  CreateUsers: migrated (0.007s) =============================

コマンド実行後に、結果の文章を読む癖もつけていきましょう
初学者が陥る問題に、terminalにエラー文が表示されているにも関わらず気付かない問題があります。
エラー文を見つけた場合は、エラー文をそのまま検索することで解決策を見つけることも難しくありません。
コマンド実行の結果、どういったことが行われているのかを見ることも理解を深めることに役立ちます。

いま実行した2行で下記の機能が自動的に生成されています。
・ユーザーの作成
・ユーザーの一覧表示
・ユーザーの編集
・ユーザーの削除

試しにサーバーを起動してみましょう。

terminal
$bin/rails server

ブラウザで開いてみましょう。
下記URLにアクセスすると。それに対応した機能が呼び出されます。
一覧表示
 http://localhost:3000/users
新規作成
 http://localhost:3000/users/new
ユーザー表示
 http://localhost:3000/users/1
ユーザー編集
 http://localhost:3000/users/1/edit

たった2行で想像以上の機能が実装されてしまい驚きと困惑があると思います。
多くの初学者が、何が起きたのかわからず Rails を魔法のように感じてしまいます。

自動で作られたファイルについて、簡略的な説明を行っていきます。
3章以降で、いちからつくるので事前に概要を知っておくことは、
今後の理解を深める上で重要です。

ルーティング

まずはどの機能が、どのURLと紐付いているかですが、
下記のコマンドで確認できます。

terminal
$bin/rails routes

下記のような表示がなされていると思います。

GET    /users(.:format)           users#index
POST   /users(.:format)           users#create
GET    /users/new(.:format)       users#new
GET    /users/:id/edit(.:format)  users#edit
GET    /users/:id(.:format)       users#show
PUT    /users/:id(.:format)       users#update
DELETE /users/:id(.:format)       users#destroy

よくみると、show update destroy がすべて /users/:id という同じURLになっています。
同じURLでも機能が異なるのは、利用しているHTTPメソッドが異なるためです。

show : GET データの取得
update : PUTデータの送信(主に更新の際)
destroy : DELETE:データの削除
 HTTPメソッドについて、もっと知りたい方はこちらをご覧ください
 https://tsuyopon.xyz/2019/01/31/understand-4-http-methods/

この紐付け(ルーティング)は、scaffold によって自動的に行われたものですが、
自力で行う場合には config/routes.rb の中に結びつけを書いていきます。
いまは 紐付けは routes.rb で行われている。 とだけ覚えてもらえば大丈夫です。
3章以降で詳しく説明していきます。

紐付けと言われても、すこし伝わりづらいとおもいます。
ルーティングの見方を、例をあげて説明します。
/users に対するルーティングをみると
GET /users(.:format)      users#index
と書かれています。これは usersコントローラーの index アクションを呼び出す
と理解してください。

コントローラー (app/controllers/users_controller.rb) を見てみると下記のコードが書かれています。

users_controller.rb
  def index
    @users = User.all
  end

 Userモデルからもらったすべてのデータを、変数@users に代入せよ
 という意味です。

 開かれる htmlファイルはどこにあるのでしょう?
 
 実は Rails では、特に指定をしない場合、
 アクション名.html.erb が自動的に呼び出されます。
 重要なので覚えておいてください。
 app/views/users/index.html.erb が呼びされます。

ここまでの流れをまとめると、下記のようになります。

 ①URLにアクセスがあった場合
 ②ルーティングに従ってコントローラーが呼び出され
 ③モデルを経由してデータを受け取り、コントローラーにデータを返し
 ④コントローラーのアクションに従って、データがビューに渡され
 ⑤ビューは受け取ったデータを利用して、HTMLを生成する

上記は、Rails を理解する上で重要な流れなので、覚えておいてください。

MVC(Model View Controller)については後ほど詳しく説明しますが、
Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う
と、大まかにご理解ください。

理屈だけより実際に触れてみると理解が深まるとおもうので、
一連のながれを体感するために、View と Controller を触っていきましょう。

2.View

ページの編集

ブラウザから一覧ページを開いてみましょう
http://localhost:3000/users

まずは、メンバー一覧表示ページを編集してみましょう。
編集するファイルは、app/views/users/index.html.erb です。
3行目 h1タグ内の、 Users の部分を下記のように置き換えてみましょう。

index.html.erb
<h1>ユーザーの一覧</h1>

編集が完了したら、上書き保存し、ブラウザを更新してください。
編集が反映されていれば成功です。

erb

ERBとは、Embedded RuBy の略であり、Rubyを埋め込めるHTMLファイルのようなものと
いまは思っておいて頂いて結構です。

ERBは、HTMLテンプレートエンジンと呼ばれ、他にも haml slim などあるので
興味がある方は、調べてみてください。

Ruby の埋め込みも体験してみましょう。
erb 内に下記のコードを記述することで、その中にrubyを埋め込むことができます。

<% %> この中にRubyコードを記述できる。
<%= %> この中に書かれた Ruby は文字列として出力される。

erb:変数の利用

index.html.erb の1行目に下記の2行を追加してください。

index.html.erb
<% name = "Player" %>
<%= "ようこそ #{name} さん" %>

Ruby と同様に、変数の代入と展開が行われ
ようこそ Playerさん と表示されます。

Timeクラスの利用

下記のように追加すると、今日の日付が表示されます。

index.html.erb
<% t = Time.now %>
<%= "いまの時刻は #{t} です" %></br>
<%= "今日は #{t.month}#{t.day} 日です" %></br>
<%= "一週間前は #{t.weeks_ago(1)} 日です" %></br>
<%= "一週間前は #{t.weeks_ago(1).month}#{t.weeks_ago(1).day} 日です" %>

Timeクラスについて詳しく

【課題1】 erbの変更

5分ほど使って index.html.erb を自由に変更してみてください。
変更例)日本語化
1, name,number をそれぞれ、名前,学籍番号 に変更
2, New user を、新規ユーザー作成 に変更
3, Show,Edit,Destoroy を 表示、編集、削除 に変更
4, Ruby のコードをerbの中に書いてみる...etb

つづいてコントローラーをみていきましょう。

3.Controller

Controller から View への、データの受け渡しを確認

Controllerは、クライアント Model View の仲介を担っています。
Controller名は、複数形にする(命名規則)

再度、コントローラー (app/controllers/users_controller.rb) の下記の行をみてください。

users_controller.rb
  def index
    @users = User.all
  end

 これは、Userモデルからもらったすべてのデータを、変数@users に代入せよ
 という意味でした。

今回はデータベースを使わずに、データを作成してみましょう。
(このあと Model の項目で、データベースを利用するように置き換えます)

@book という連想配列を作成し、その中に情報を書き込んでいきます。

users_controller.rb
  def index
    @users = User.all
    @book = Hash.new
    @book[:country] = "Japan"
    @book[:title] = "吾輩は猫である"
    @book[:author] = "夏目漱石"
    @book[:year] = 1905
    @book[:book_id] = 1
    @book[:comment] = "おもしろかった"
  end

@book[:title] Hash(連想配列)のSymbolです。お忘れの方は
Array(配列)とHash(連想配列)入門
を御覧ください。

本の情報を @book 連想配列に追加したので、 View で受け取れるか確認してみましょう。
(def index に記載しているので、 index.html.erb で受け取れます)

index.html.erb
<%= @book %></br>
<%= "本のタイトルは #{@book[:title]} です" %></br>
<%= "著者は #{@book[:author]} です" %></br>

http://localhost:3000/users
ブラウザを更新すると下記のように表示されます。

 {:country=>"Japan", :title=>"吾輩は猫である", :author=>"夏目漱石", :year=>1905,
  :book_id=>1, :comment=>"おもしろかった"}
 本のタイトルは 吾輩は猫である です
 著者は 夏目漱石 です

無事、情報を受け取れていますね。

erbの応用

ここに和書かどうかを調べる機能を追加してみましょう。
erb はRubyが使えるので、条件分岐も利用できます

下記のコードを追加してみましょう。

index.html.erb
<% if @book[:country] == "Japan" %>
<p>この本は、和書です</p>
<% end %>

 この本は、和書です
と表示されます。

すこし蔵書管理アプリっぽくなりました。

蔵書データを増やしていきたいところですが、問題があります。
Controller に書くには蔵書データはあまりに多すぎるという問題です。
(そしてユーザーは追記できない)

これを解決するのが、データベースとModelです。

@book に関するコードは、すべて消してしまいましょう。

4.Model

Books リソースの作成

Modelは基本的に、1つのテーブルに対して1つのModelを作成します。
Model名は、複数形にする(命名規則)

人に関するリソースは作成したので、本に関するリソースも作成していきましょう。
(usersテーブルとは、別にbooksテーブルを作成するので、Book Modelを作成します)

再度、Scaffold を実行します。

terminal
$bin/rails generate scaffold Book country:text title:text author:text year:integer user_id:integer comment:text

Users リソースの時と同様に、自動で多くのファイルが作成されます。
Model View Controller route もそれぞれ作成されています。

terminal
    create    app/models/book.rb
    create    app/views/books/(略)
    create    app/controllers/books_controller.rb
    route     resources :books

前回同様、下記コマンドを実行することで、データベースに books という名前のテーブルが作成されます。

terminal
$bin/rails db:migrate

ルーティングも確認してみましょう。

terminal
$bin/rails routes

下記のように、紐付けされていることが確認できます。

GET    /books(.:format)           books#index
POST   /books(.:format)           books#create
GET    /books/new(.:format)       books#new
GET    /books/:id/edit(.:format)  books#edit
GET    /books/:id(.:format)       books#show
PUT    /books/:id(.:format)       books#update
DELETE /books/:id(.:format)       books#destroy

ブラウザで確認してみましょう。本の情報を入力できるようになっています。
http://localhost:3000/users/new

このままではユーザーは自由に投稿できる状態です。
year と user_id は数値を入力してもらいたいので入力制限をかけてみましょう。

Rails では制限(validation)も簡単に設定することができます。

Validation

app/models/book.rb を開き下記のように編集してください。

book.rb
class Book < ApplicationRecord
  validates :year, :user_id, numericality: true
end

この1行だけで制限を設けることができます。
Book Model に追記しているので、 books テーブルの yearカラム と user_idカラムに対して制限をかけます。
実際に試してみると、下記のようなエラー文が表示されます。
validate.png

【課題2】自作バリデーション

Railsバリデーションまとめ
を参考にして、制限を設けてみましょう。
例)
1, Title を空白禁止にする
2, 著者の名前を125文字以内にする ...etc

バリデーションがうまくかけたかを試すために、
実際にブラウザから本のデータを2件登録してみてください。

内容は自由で良いですが、 user の項目だけは 1 にしてください

rails console

うまく情報が登録されたかを確認するために、データベースから情報を取り出してみましょう。
SQL文が苦手な方もご安心ください。
Rails console を使えば、簡単にデータを取り出してみることができます。

新規ターミナルを起動し、下記のコマンドを実行してください。

terminal
  $bin/rails c

すると >> といった表示がされるとおもいます。これでrails console が起動しました。
( exit 入力し、Enterを実行すると終了します )

試しに、最初に登録したユーザー情報を、(Model を経由し)データベースから取り出してみましょう

railsConsole
  >> user = User.find(1)

user はただの変数です。(名前は自由ですが、わかりやすく use にしました)
User.find(1) は User Model を利用して、 1番目の人を取り出せ
という意味ですので、 U は大文字です。(Model は頭文字が大文字で単数形でしたね)

さきほど、ブラウザから入力したデータが登録されていると思います。

【課題3】 rails c を使った、データの取り出し

本のデータを取り出して表示してみましょう。
その際に、find(1) 意外の取り出し方も試してみましょう。
データ取得メソッドまとめ
例) .all を使用してみる。

MVC総合 User が所持しているデータを表示してみる。

さてここまでで、冒頭に説明した下記の役割を体験してみました。
Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う

なにもわからなかった状態から、
ルーティング・DB・コンソール・MVC
と多くのことを吸収し、成長を感じられると思います。

まだ大まかな理解だと思うので、更に理解を深めるべく
応用課題に取り組んでいきましょう!

id: 1 のUserが所持している本のデータを取り出してみましょう。

ブラウザでの表示は View を編集する必要があり大変なので
まずは console の中で、取り出しを確認するところだけみていきましょう。
(小さく着実に作っていくことは大事です)

rails console を起動した端末にて下記のコマンドを実行します。

railsConsole
   >> user = User.find(1)
   >> user.books

と入力することで、 id:1 のuserが持っている本のデータ
(booksテーブルの中で、 user_id を 1 にした本のデータ)
がすべて表示されます!

.
..
...
されないとおもいます。
実行結果を見てみると、エラー文が出ていますね

railsconsole
>> user.books
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (undefined method `books' for #<User:0x00007fa63942dce0>)

NoMethodError undefined method `books'
これは、 books がなにかわからない。定義されていない。という意味のエラーです。
(今後良くみかけることになるとおもいます。タイピングミスしてもこのエラーが表示されます)

今回の原因は、 user テーブルと books テーブルが紐付いていないことによるものです。

booksテーブルで勝手に user_id というカラムを追加しただけで、
userテーブルにはなにも書いていないので当然ですね。
Userモデルから、bookテーブルのデータを取得できないのでエラーが出ています
こういったテーブル同士の紐付けも、Railsでは1行で出来ます。

has_many belongs_to

いま、 users と books の2つのテーブルがあります。
どの蔵書データが、どのユーザーに紐付いているのかといった関連付けを行いたいとき
どうすれば良いのかを学んでいきます。

1,一人のuserは、たくさんの本を持っています
こういう場合、 user.rb (UserのModel)に、
has_many :books (たくさんなので、複数形です)
と1行追加するだけで、上記のような紐付けが完了します。

2,1冊の本は、一人のUserに紐付いています
こういう場合、 book.rb (BookのModel)に、
belongs_to :user (ひとつなので、単数形です)
と追加するだけで、上記のような紐付けが完了します。

user.rb
class Book < ApplicationRecord
  has_many :books
end
book.rb
class Book < ApplicationRecord
  belongs_to :user
  validates :year, :user_id, numericality: true
end

has_many belongs_to コード追加が終われば
rails console を立ち上げ直してください
(exitとタイプしEnterで終了できます)

再度 rails c を起動し、さきほどと同じコマンドを実行するとどう変わっているでしょうか。

railsConsole
   >> user = User.find(1)
   >> user.books

↓出力結果例

railsConsole
Loading development environment
>> user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "daichi", number: 10

>> user.books
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Book id: 1, country: "Japan", title: "吾輩は猫である", author: "夏目漱石", year: 1905, user_id: 1, comment: "おもしろかった"

紐付けられたことでエラーが解消され、上記のような結果が表示されます。

テーブルに関連性を持たせたいときは has_many belongs_to を使用すること
覚えておいてください。

Modelにて、データベース内のテーブルの関連づけは出来き、
user.books にてユーザーが持っている本のデータを取り出せることが確認できました。

さて、Modelで取り出したデータを、View に送るにはどうすればよかったでしょうか?
そうですね。Controller の役割でしたね。

Controller

app/controllers/users_controller.rb
をひらきます(@bookを消していない人は消しておいてください)

前回は、index.html.erb に情報を渡したかったので index アクションに
@book のデータを(データベースを使わずに直接)書きました。

今回は、show.html.erb に情報を渡したいので、 show アクションに
モデルを用いて、データを取り出すコードを書きます

app/controllers/users_controller.rb
  def show
    @user = User.find(1)
    @books = @user.books
  end

(小文字大文字, s の有無に気をつけてください)
最後に、Controller から送られてきたデータの表示の部分をやっていきます。

View

表示させたい場所は、1番目のユーザーのページなのでこちらです
http://localhost:3000/users/1

【MVCの流れの復習】
ちなみに、ルーティングには下記のように書いてあります
 GET /users/:id       users#show
users/(idの数字) にGETリクエストがあったら users controller の show アクションを実行でしたね。
show アクションが、実行されたら自動的に、 show.html.erb が表示されるのでした。

show.html.erb の一番上に下記コードを追記してください。

app/views/users/show.html.erb
  <% @books.each do |book| %>
    <p><%= "本のタイトルは #{book[:title]} です" %></p>
    <p><%= "著者は #{book[:author]} です" %></p></br>
  <% end %>

これで、 Controller から受け取った @books (本の情報が格納された連想配列)
の中身が表示されていると思います。
(配列の中身を順番に取り出していくときは each でした。 Ruby をお忘れの方は復習を)

【最終課題】

show.html.erb を自由に編集してください

例)
・本の情報をすべて表示

まとめ

以上で、scaffold を用いた、高速蔵書アプリケーションの制作講座は終わりです。
お疲れ様でした。

3章では、scaffold が自動生成していた部分を、1から自分で制作していきます。
今回の内容を再度読み返すか、もう一度素早く作成して復習をしておいてください。

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

Before Rails Tutorial2章 Scaffold を用いた高速なアプリケーション構築 及び MVCの理解

本投稿は講義資料であり、Rubyの基礎は理解しているが、rails tutorialで躓く読者を対象としています。
1章:環境構築
2章:Scaffold を用いた高速なアプリケーション構築 及び MVCの理解
3章:Scaffold を用いない開発方法 及び 応用

1.Scaffold

この章では、Railsの強力な機能をいくつか紹介するためのアプリケーションを作成します。
大量の機能を自動的に生成するscaffoldジェネレータというスクリプトを使って蔵書管理アプリケーションを生成し、それを元にRailsの概要を一緒にみていきましょう。

*3章以降では、scaffold は使わずに、自力で作る方法を学びます。

scaffold コマンド

まずは、名前と学籍番号を登録できるだけの機能を作成します。
下記のコマンドは一字一句間違いなくタイプするか、コピペで行ってください。

失敗した場合は、下記のページに解決策が記載してあります。(すこし複雑です)
scaffold作成時にカラム名(属性名)を打ち間違えた場合の修正手順

terminal
$bin/rails generate scaffold User name:string number:integer

Railsのscaffoldは、rails generateスクリプトにscaffoldコマンドを渡すことで生成されます。scaffoldコマンドの引数には、リソース名を単数形にしたもの (この場合はUser) を使い、必要に応じてデータモデルの属性をオプションとしてパラメータに追加します

リソースとは、コントローラが扱う対象に名前をつけたものです。
例:モデル、画像、セッション...etc

自動で多数のファイルが生成されたとおもいます。
その中で重要なのは次のファイルです。

terminal
    create    app/models/user.rb
    create    app/views/users/(略)
    create    app/controllers/users_controller.rb
    route     resources :users

ここで生成された Models Views Controllers の頭文字をとって
MVC と呼びます。MVCについては後ほど、説明します。

データベースの利用

このアプリケーションではデータを保存するために、データベースを扱います。
下記コマンドを実行することで、データベースに users という名前のテーブルが作成されます。

terminal
$bin/rails db:migrate

下記のような結果が表示されていればテーブルの生成ができています。
(migrate については今後の章で解説します)

terminal
==  CreateUsers: migrating =============================
-- create_table(:users)
   -> 0.0007s
==  CreateUsers: migrated (0.007s) =============================

コマンド実行後に、結果の文章を読む癖もつけていきましょう
初学者が陥る問題に、terminalにエラー文が表示されているにも関わらず気付かない問題があります。
エラー文を見つけた場合は、エラー文をそのまま検索することで解決策を見つけることも難しくありません。
コマンド実行の結果、どういったことが行われているのかを見ることも理解を深めることに役立ちます。

いま実行した2行で下記の機能が自動的に生成されています。
・ユーザーの作成
・ユーザーの一覧表示
・ユーザーの編集
・ユーザーの削除

試しにサーバーを起動してみましょう。

terminal
$bin/rails server

ブラウザで開いてみましょう。
下記URLにアクセスすると。それに対応した機能が呼び出されます。
一覧表示
 http://localhost:3000/users
新規作成
 http://localhost:3000/users/new
ユーザー表示
 http://localhost:3000/users/1
ユーザー編集
 http://localhost:3000/users/1/edit

たった2行で想像以上の機能が実装されてしまい驚きと困惑があると思います。
多くの初学者が、何が起きたのかわからず Rails を魔法のように感じてしまいます。

自動で作られたファイルについて、簡略的な説明を行っていきます。
3章以降で、いちからつくるので事前に概要を知っておくことは、
今後の理解を深める上で重要です。

ルーティング

まずはどの機能が、どのURLと紐付いているかですが、
下記のコマンドで確認できます。

terminal
$bin/rails routes

下記のような表示がなされていると思います。

GET    /users(.:format)           users#index
POST   /users(.:format)           users#create
GET    /users/new(.:format)       users#new
GET    /users/:id/edit(.:format)  users#edit
GET    /users/:id(.:format)       users#show
PUT    /users/:id(.:format)       users#update
DELETE /users/:id(.:format)       users#destroy

よくみると、show update destroy がすべて /users/:id という同じURLになっています。
同じURLでも機能が異なるのは、利用しているHTTPメソッドが異なるためです。

show : GET データの取得
update : PUTデータの送信(主に更新の際)
destroy : DELETE:データの削除
 HTTPメソッドについて、もっと知りたい方はこちらをご覧ください
 https://tsuyopon.xyz/2019/01/31/understand-4-http-methods/

この紐付け(ルーティング)は、scaffold によって自動的に行われたものですが、
自力で行う場合には config/routes.rb の中に結びつけを書いていきます。
いまは 紐付けは routes.rb で行われている。 とだけ覚えてもらえば大丈夫です。
3章以降で詳しく説明していきます。

紐付けと言われても、すこし伝わりづらいとおもいます。
ルーティングの見方を、例をあげて説明します。
/users に対するルーティングをみると
GET /users(.:format)      users#index
と書かれています。これは usersコントローラーの index アクションを呼び出す
と理解してください。

コントローラー (app/controllers/users_controller.rb) を見てみると下記のコードが書かれています。

users_controller.rb
  def index
    @users = User.all
  end

 Userモデルからもらったすべてのデータを、変数@users に代入せよ
 という意味です。

 開かれる htmlファイルはどこにあるのでしょう?
 
 実は Rails では、特に指定をしない場合、
 アクション名.html.erb が自動的に呼び出されます。
 重要なので覚えておいてください。
 app/views/users/index.html.erb が呼びされます。

ここまでの流れをまとめると、下記のようになります。

 ①URLにアクセスがあった場合
 ②ルーティングに従ってコントローラーが呼び出され
 ③モデルを経由してデータを受け取り、コントローラーにデータを返し
 ④コントローラーのアクションに従って、データがビューに渡され
 ⑤ビューは受け取ったデータを利用して、HTMLを生成する

上記は、Rails を理解する上で重要な流れなので、覚えておいてください。

MVC(Model View Controller)については後ほど詳しく説明しますが、
Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う
と、大まかにご理解ください。

理屈だけより実際に触れてみると理解が深まるとおもうので、
一連のながれを体感するために、View と Controller を触っていきましょう。

2.View

ページの編集

ブラウザから一覧ページを開いてみましょう
http://localhost:3000/users

まずは、メンバー一覧表示ページを編集してみましょう。
編集するファイルは、app/views/users/index.html.erb です。
3行目 h1タグ内の、 Users の部分を下記のように置き換えてみましょう。

index.html.erb
<h1>ユーザーの一覧</h1>

編集が完了したら、上書き保存し、ブラウザを更新してください。
編集が反映されていれば成功です。

erb

ERBとは、Embedded RuBy の略であり、Rubyを埋め込めるHTMLファイルのようなものと
いまは思っておいて頂いて結構です。

ERBは、HTMLテンプレートエンジンと呼ばれ、他にも haml slim などあるので
興味がある方は、調べてみてください。

Ruby の埋め込みも体験してみましょう。
erb 内に下記のコードを記述することで、その中にrubyを埋め込むことができます。

<% %> この中にRubyコードを記述できる。
<%= %> この中に書かれた Ruby は文字列として出力される。

erb:変数の利用

index.html.erb の1行目に下記の2行を追加してください。

index.html.erb
<% name = "Player" %>
<%= "ようこそ #{name} さん" %>

Ruby と同様に、変数の代入と展開が行われ
ようこそ Playerさん と表示されます。

Timeクラスの利用

下記のように追加すると、今日の日付が表示されます。

index.html.erb
<% t = Time.now %>
<%= "いまの時刻は #{t} です" %></br>
<%= "今日は #{t.month}#{t.day} 日です" %></br>
<%= "一週間前は #{t.weeks_ago(1)} 日です" %></br>
<%= "一週間前は #{t.weeks_ago(1).month}#{t.weeks_ago(1).day} 日です" %>

Timeクラスについて詳しく

【課題1】 erbの変更

5分ほど使って index.html.erb を自由に変更してみてください。
変更例)日本語化
1, name,number をそれぞれ、名前,学籍番号 に変更
2, New user を、新規ユーザー作成 に変更
3, Show,Edit,Destoroy を 表示、編集、削除 に変更
4, Ruby のコードをerbの中に書いてみる...etb

つづいてコントローラーをみていきましょう。

3.Controller

Controller から View への、データの受け渡しを確認

Controllerは、クライアント Model View の仲介を担っています。
Controller名は、複数形にする(命名規則)

再度、コントローラー (app/controllers/users_controller.rb) の下記の行をみてください。

users_controller.rb
  def index
    @users = User.all
  end

 これは、Userモデルからもらったすべてのデータを、変数@users に代入せよ
 という意味でした。

今回はデータベースを使わずに、データを作成してみましょう。
(このあと Model の項目で、データベースを利用するように置き換えます)

@book という連想配列を作成し、その中に情報を書き込んでいきます。

users_controller.rb
  def index
    @users = User.all
    @book = Hash.new
    @book[:country] = "Japan"
    @book[:title] = "吾輩は猫である"
    @book[:author] = "夏目漱石"
    @book[:year] = 1905
    @book[:book_id] = 1
    @book[:comment] = "おもしろかった"
  end

@book[:title] Hash(連想配列)のSymbolです。お忘れの方は
Array(配列)とHash(連想配列)入門
を御覧ください。

本の情報を @book 連想配列に追加したので、 View で受け取れるか確認してみましょう。
(def index に記載しているので、 index.html.erb で受け取れます)

index.html.erb
<%= @book %></br>
<%= "本のタイトルは #{@book[:title]} です" %></br>
<%= "著者は #{@book[:author]} です" %></br>

http://localhost:3000/users
ブラウザを更新すると下記のように表示されます。

 {:country=>"Japan", :title=>"吾輩は猫である", :author=>"夏目漱石", :year=>1905,
  :book_id=>1, :comment=>"おもしろかった"}
 本のタイトルは 吾輩は猫である です
 著者は 夏目漱石 です

無事、情報を受け取れていますね。

erbの応用

ここに和書かどうかを調べる機能を追加してみましょう。
erb はRubyが使えるので、条件分岐も利用できます

下記のコードを追加してみましょう。

index.html.erb
<% if @book[:country] == "Japan" %>
<p>この本は、和書です</p>
<% end %>

 この本は、和書です
と表示されます。

すこし蔵書管理アプリっぽくなりました。

蔵書データを増やしていきたいところですが、問題があります。
Controller に書くには蔵書データはあまりに多すぎるという問題です。
(そしてユーザーは追記できない)

これを解決するのが、データベースとModelです。

@book に関するコードは、すべて消してしまいましょう。

4.Model

Books リソースの作成

Modelは基本的に、1つのテーブルに対して1つのModelを作成します。
Model名は、複数形にする(命名規則)

人に関するリソースは作成したので、本に関するリソースも作成していきましょう。
(usersテーブルとは、別にbooksテーブルを作成するので、Book Modelを作成します)

再度、Scaffold を実行します。

terminal
$bin/rails generate scaffold Book country:text title:text author:text year:integer user_id:integer comment:text

Users リソースの時と同様に、自動で多くのファイルが作成されます。
Model View Controller route もそれぞれ作成されています。

terminal
    create    app/models/book.rb
    create    app/views/books/(略)
    create    app/controllers/books_controller.rb
    route     resources :books

前回同様、下記コマンドを実行することで、データベースに books という名前のテーブルが作成されます。

terminal
$bin/rails db:migrate

ルーティングも確認してみましょう。

terminal
$bin/rails routes

下記のように、紐付けされていることが確認できます。

GET    /books(.:format)           books#index
POST   /books(.:format)           books#create
GET    /books/new(.:format)       books#new
GET    /books/:id/edit(.:format)  books#edit
GET    /books/:id(.:format)       books#show
PUT    /books/:id(.:format)       books#update
DELETE /books/:id(.:format)       books#destroy

ブラウザで確認してみましょう。本の情報を入力できるようになっています。
http://localhost:3000/users/new

このままではユーザーは自由に投稿できる状態です。
year と user_id は数値を入力してもらいたいので入力制限をかけてみましょう。

Rails では制限(validation)も簡単に設定することができます。

Validation

app/models/book.rb を開き下記のように編集してください。

book.rb
class Book < ApplicationRecord
  validates :year, :user_id, numericality: true
end

この1行だけで制限を設けることができます。
Book Model に追記しているので、 books テーブルの yearカラム と user_idカラムに対して制限をかけます。
実際に試してみると、下記のようなエラー文が表示されます。
validate.png

【課題2】自作バリデーション

Railsバリデーションまとめ
を参考にして、制限を設けてみましょう。
例)
1, Title を空白禁止にする
2, 著者の名前を125文字以内にする ...etc

バリデーションがうまくかけたかを試すために、
実際にブラウザから本のデータを2件登録してみてください。

内容は自由で良いですが、 user の項目だけは 1 にしてください

rails console

うまく情報が登録されたかを確認するために、データベースから情報を取り出してみましょう。
SQL文が苦手な方もご安心ください。
Rails console を使えば、簡単にデータを取り出してみることができます。

新規ターミナルを起動し、下記のコマンドを実行してください。

terminal
  $bin/rails c

すると >> といった表示がされるとおもいます。これでrails console が起動しました。
( exit 入力し、Enterを実行すると終了します )

試しに、最初に登録したユーザー情報を、(Model を経由し)データベースから取り出してみましょう

railsConsole
  >> user = User.find(1)

user はただの変数です。(名前は自由ですが、わかりやすく use にしました)
User.find(1) は User Model を利用して、 1番目の人を取り出せ
という意味ですので、 U は大文字です。(Model は頭文字が大文字で単数形でしたね)

さきほど、ブラウザから入力したデータが登録されていると思います。

【課題3】 rails c を使った、データの取り出し

本のデータを取り出して表示してみましょう。
その際に、find(1) 意外の取り出し方も試してみましょう。
データ取得メソッドまとめ
例) .all を使用してみる。

MVC総合 User が所持しているデータを表示してみる。

さてここまでで、冒頭に説明した下記の役割を体験してみました。
Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う

なにもわからなかった状態から、
ルーティング・DB・コンソール・MVC
と多くのことを吸収し、成長を感じられると思います。

まだ大まかな理解だと思うので、更に理解を深めるべく
応用課題に取り組んでいきましょう!

id: 1 のUserが所持している本のデータを取り出してみましょう。

ブラウザでの表示は View を編集する必要があり大変なので
まずは console の中で、取り出しを確認するところだけみていきましょう。
(小さく着実に作っていくことは大事です)

rails console を起動した端末にて下記のコマンドを実行します。

railsConsole
   >> user = User.find(1)
   >> user.books

と入力することで、 id:1 のuserが持っている本のデータ
(booksテーブルの中で、 user_id を 1 にした本のデータ)
がすべて表示されます!

.
..
...
されないとおもいます。
実行結果を見てみると、エラー文が出ていますね

railsconsole
>> user.books
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (undefined method `books' for #<User:0x00007fa63942dce0>)

NoMethodError undefined method `books'
これは、 books がなにかわからない。定義されていない。という意味のエラーです。
(今後良くみかけることになるとおもいます。タイピングミスしてもこのエラーが表示されます)

今回の原因は、 user テーブルと books テーブルが紐付いていないことによるものです。

booksテーブルで勝手に user_id というカラムを追加しただけで、
userテーブルにはなにも書いていないので当然ですね。
Userモデルから、bookテーブルのデータを取得できないのでエラーが出ています
こういったテーブル同士の紐付けも、Railsでは1行で出来ます。

has_many belongs_to

いま、 users と books の2つのテーブルがあります。
どの蔵書データが、どのユーザーに紐付いているのかといった関連付けを行いたいとき
どうすれば良いのかを学んでいきます。

1,一人のuserは、たくさんの本を持っています
こういう場合、 user.rb (UserのModel)に、
has_many :books (たくさんなので、複数形です)
と1行追加するだけで、上記のような紐付けが完了します。

2,1冊の本は、一人のUserに紐付いています
こういう場合、 book.rb (BookのModel)に、
belongs_to :user (ひとつなので、単数形です)
と追加するだけで、上記のような紐付けが完了します。

user.rb
class Book < ApplicationRecord
  has_many :books
end
book.rb
class Book < ApplicationRecord
  belongs_to :user
  validates :year, :user_id, numericality: true
end

has_many belongs_to コード追加が終われば
rails console を立ち上げ直してください
(exitとタイプしEnterで終了できます)

再度 rails c を起動し、さきほどと同じコマンドを実行するとどう変わっているでしょうか。

railsConsole
   >> user = User.find(1)
   >> user.books

↓出力結果例

railsConsole
Loading development environment
>> user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "daichi", number: 10

>> user.books
  Book Load (0.1ms)  SELECT  "books".* FROM "books" WHERE "books"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Book id: 1, country: "Japan", title: "吾輩は猫である", author: "夏目漱石", year: 1905, user_id: 1, comment: "おもしろかった"

紐付けられたことでエラーが解消され、上記のような結果が表示されます。

テーブルに関連性を持たせたいときは has_many belongs_to を使用すること
覚えておいてください。

Modelにて、データベース内のテーブルの関連づけは出来き、
user.books にてユーザーが持っている本のデータを取り出せることが確認できました。

さて、Modelで取り出したデータを、View に送るにはどうすればよかったでしょうか?
そうですね。Controller の役割でしたね。

Controller

app/controllers/users_controller.rb
をひらきます(@bookを消していない人は消しておいてください)

前回は、index.html.erb に情報を渡したかったので index アクションに
@book のデータを(データベースを使わずに直接)書きました。

今回は、show.html.erb に情報を渡したいので、 show アクションに
モデルを用いて、データを取り出すコードを書きます

app/controllers/users_controller.rb
  def show
    @user = User.find(1)
    @books = @user.books
  end

(小文字大文字, s の有無に気をつけてください)
最後に、Controller から送られてきたデータの表示の部分をやっていきます。

View

表示させたい場所は、1番目のユーザーのページなのでこちらです
http://localhost:3000/users/1

【MVCの流れの復習】
ちなみに、ルーティングには下記のように書いてあります
 GET /users/:id       users#show
users/(idの数字) にGETリクエストがあったら users controller の show アクションを実行でしたね。
show アクションが、実行されたら自動的に、 show.html.erb が表示されるのでした。

show.html.erb の一番上に下記コードを追記してください。

app/views/users/show.html.erb
  <% @books.each do |book| %>
    <p><%= "本のタイトルは #{book[:title]} です" %></p>
    <p><%= "著者は #{book[:author]} です" %></p></br>
  <% end %>

これで、 Controller から受け取った @books (本の情報が格納された連想配列)
の中身が表示されていると思います。
(配列の中身を順番に取り出していくときは each でした。 Ruby をお忘れの方は復習を)

【最終課題】

show.html.erb を自由に編集してください

例)
・本の情報をすべて表示

まとめ

以上で、scaffold を用いた、高速蔵書アプリケーションの制作講座は終わりです。
お疲れ様でした。

3章では、scaffold が自動生成していた部分を、1から自分で制作していきます。
今回の内容を再度読み返すか、もう一度素早く作成して復習をしておいてください。

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

rails 基礎講義資料3章 Scaffold を用いない開発

1章:環境構築
2章:Scaffold を用いた高速なアプリケーション構築 及び MVCの理解
3章:Scaffold を用いない開発方法 及び 応用

前章では、Scaffoldを用いて高速でアプリケーションを構築しました。
自動化された部分も多く、現段階では、大まかな理解であるとおもいます。

今回は、route Model View Controller それぞれ自分で作っていきます。
とはいっても、継承まで含めてすべて書くには理解することが膨大で大変ですので、
前回も使用した generate は使用していきます。

まずは、Railsの新しいアプリケーションを作成します。
1章:環境構築の内容を参考に作成してみましょう。

terminal
$ cd ~/rails_app
$ mkdir bukukore
$ bundle init
   (Gemfileにgem "rails","~> 5.2.3"を追加し保存してください)
$ bundle install --path vendor/bundlesa
$ bin/rails s

http://localhost:3000/ にアクセスして表示されていればokです。

$ rails s -p 3100 などとポートを変更することでサーバーを複数起動可能です。
この場合は http://localhost:3100/ にアクセスします。

MVC 復習

前章では、アプリ制作を通して、MVCの流れを理解してきました。

Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う

前回、制作したながれを図を見ながら復習していきます。
それぞれの番号が、コードではどうなっていたか思い出しながら見てください。

MVC-02.png

1, ブラウザから /users へのリクエストを Rails sever に送信する
2, router によって、/users は Users controller の index アクションを呼び出す
3, index アクションから、User Model が呼び出される
4, User Model は DataBase からデータを取り出す
5, 取り出したデータを、Controller に返す
6, 受け取ったデータを @users に保存し、 View (indexアクションなので index.html.erb) に渡す。
7, ERBを実行し、@users のデータを含んだ HTMLを生成し、Controller へ返す
8, Controller は、受け取ったHTMLをブラウザにわたす

今回も Model View Controller を構築していくので、適宜図を見返してください。

Controller と View の作成

作成は基本的に generate コマンドを用います。
(gは generateコマンドの短縮形です)
g の後は、controller に続けて、コントローラーの名前(複数形)を書きます。
rails g controller Books index show new
そのうしろ(引数といいます)に書いたものは、同名のviewとアクションが自動生成されます。

tarminal
$ rails g controller Books index show new
     create    app/controllers/books_controller.rb
     route     get 'books/index'
               get 'books/show'
               get 'books/new'
     create    app/views/books
     create    app/views/books/index.html.erb
     create    app/views/books/show.html.erb
     create    app/views/books/new.html.erb

ルーティングが生成されているので、確認してみましょう。

terminal
$ rails routes
  Verb  URI             Pattern
  GET   /books/index    books#index
  GET   /books/show     books#show
  GET   /books/new      books#new

これは具体的には、 http://localhost:3000/books/index
というURLに対するリクエストがあれば、 books_controller の index アクション を呼び出すというものでした。
今回アクションの中身が空なので、Model は呼び出されず、単に対応するView (index.html.erb) が実行されるだけです。

ルーティングがうまくいってみるか確認するために、試しにアクセスしてみましょう。
http://localhost:3000/books/index

Books#index
Find me in app/views/books/index.html.erb

と表示されているとおもいます。

Viewの変更

erbの変更は前回行ったとおりですね。とてもシンプルなものでした。
Books#index の部分を、全蔵書一覧などに変更しておきましょう。

app/views/books/index.html.erb
<h1>全蔵書一覧</h1>

 erb の中身がシンプルすぎて不思議な方もいらっしゃるかもしれません。
 【気になって次にすすめない人向けの解説】
  app/views/layouts/application.html.erb を開いてください。
 コード内の、<%= yiels %> の部分に、 index.html.erb が移植されて HTML を生成しています。 
 いまはこの程度の理解で問題ありません。 https://railstutorial.jp/ でより深く理解できます。

Controllerの変更

コントローラーの中にアクションが本当に作られているかも確認してみましょう。
app/controllers/books_controller.rb を開いてみてください。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
  end

  def show
  end

  def new
  end
end

データベース内のデータを取り出すには、Controllerのアクションから
Model を呼び出せば良いのでした。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
  end

  def show
   @book = Book.find(1)
  end

  def new
  end
end

このままではエラーがでます。(Modelも、本の情報も作っていないので)
generate コマンドを使用して、Model を作成していきます。

Model の作成

booksテーブルには、前章と同じカラムを持たせていきます。

terminal
$ bin/rails g model Book country:text title:text author:text year:integer user_id:integer comment:text
      create    db/migrate/20200202020202_create_books.rb
      create    app/models/book.rb

g の後は、model に続けて、Model名(単数形)を書きます。
データベースにテーブルを作成するには migrate を実行するのでしたね。

terminla
$ bin/rails db:migrate
== 20200202020202 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0008s
== 20200202020202 CreateBooks: migrated (0.0009s) =============================

補足. migrate はマイグレーションファイルを実行しています。
db/migrate/(作成日時)_create_books.rb というファイルです。
ひらいてみると、下記のようなコードがあると思います。

class CreateBooks < ActiveRecord
 def change
   create_table :books do |t|
    t.text :country
    t.text :title
    t.text :author
    t.integer :year
    t.integer :user_id
    t.text :comment
    t.timestamps
   end
 end
end

上記は text型であるcountryというカラム(他6つ)を持つ books テーブルを作成
せよという意味です。 change 意外に up down などもあります。詳細は下記サイトなどを参照ください
https://www.sejuku.net/blog/14229

これでbooksテーブルが作成されました。

テーブルは作成したけども、本の情報はまだなにもいれてませんね。
初期データをいれるには db/seeds.rb ファイルを使用します。
( seed = 種 という意味です )
大量のデータを入力するのは手間なのでseed.rbを作成していきます。
(そもそもブラウザからデータをいれる機能もまだ作っていません)

下記の seed ファイルを実行すると、bookテーブルに本の情報が3件保存されます

db/seeds.rb
@book = Book.new
@book.country = "jp"
@book.title = "吾輩は猫である"
@book.author = "夏目漱石"
@book.year = 1905
@book.user_id = 1
@book.comment = "おもしろかった"
@book.save

@book = Book.new
@book.country = "jp"
@book.title = "人間失格"
@book.author = "太宰治"
@book.year = 1948
@book.user_id = 2
@book.comment = "すばらしかった"
@book.save

@book = Book.new
@book.country = "gb"
@book.title = "Alice's Adventures in Wonderland"
@book.author = "Lewis Carroll"
@book.year = 1865
@book.user_id = 1
@book.comment = "Amazing"
@book.save

seed ファイルは、下記のコマンドで実行できます。

terminal
$ bundle exec rake db:seed

データが保存されたかの確認は。rails c でコンソールを起動すると確認できます。

railsConsole
@book = Book.find(1)

ブラウザ(show)で表示してみましょう。

app/view/books/show.html.erb
<p>本のタイトル: <%= @book.title %></p>
<p>著者: <%= @book.author %></p>

本のタイトル: 吾輩は猫である

著者: 夏目漱石

と表示されていればOKです。

課題1,

@book はどこで定義したか、再確認してください。

URLからパラメーターの取得

本のデータを取り出して表示するところまでうまくいきました!
すごい進歩です!

ただ、まだ不満点があります。
それは、 id:1 の本のデータしか表示できていない点です。
理由は、Contoroller の show アクションをみると、
@book = Book.find(1)
となっているためです(id:1 の本の情報しか見つけてきていない)

ここをidパラメーターが1以外も入力できるように変更するにはどうすればよいでしょうか。
@book = Book.find(params[:id])
とすることで、1以外のパラメーターを見つけてこれます。

app/controllers/books_controller.rb
 def show
    @book = Book.find(params[:id])
 end

パラメーター(数値)は、どこで指定すればよいでしょうか。
URL にパラメーターを含めて渡すテクニックがあるので覚えておきましょう。
例)ID:2の本の情報を取得したい場合 http://localhost:3000/books/show/2
  このままではルーティングエラーが表示されます。
  books/show へのリクエストに関しては記述していますが、
  books/show/(数字) へのリクエストについてはなにも書いていない為です。

config/routes.rb を開いてください。
URLに含まれる数字をパラメーターとして取得するには、下記のように記述します。

config/routes.rb
#   get 'books/show'(# でコメントアウトしてます)
get 'books/show/:id', to:'books#show'

こうすることで /books/show/2 の 2の部分が、:id パラメーターとして利用可能になります。
( 利用するときは、params[:id] とかきます)

to: 'books#show' 
books Cコントローラーの show アクションを呼び出す指示になります。

これで、任意のidの本情報を取得できるようになりました。

http://localhost:3000/books/show/1
http://localhost:3000/books/show/2

Model 検索

いま本の情報の取り出しに id を利用していますが、
id 以外のカラムを用いて取り出す方法も覚えておくと便利です。

コンソールを起動してください。 ( rails c )
下記を参考に色々と試してみてください。

railsConsole
>> Book.all
   #bookテーブルのすべてのデータを取得します。
>> Book.find_by(title: "人間失格")
   #条件に合ったデータを、1件だけ取得します。
>> Book.where(country: "jp")
   #条件に合ったデータを、すべて取得します。
>> Book.where(country: "jp").where(year: 1905)
   #AND検索。 和書で 1905年出版の本を取得しています。
>> Book.where("(country = ?) OR (year = ?)","jp",1905)
   #OR検索。 和書 または 1905年出版の本を取得しています。
>> Book.order(create_at: :asc)
   #並び順を指定します。 作成日時の昇順で取得しています。
>> Book.where(user_id: 1).order(id: :desc)
   #user_id 1さんの蔵書を、idの降順で取得しています。 

where の利用

検索条件を学んだのでこれ使って、蔵書管理アプリケーションらしく
個人の蔵書を一覧表示する機能を構築してみましょう。

app/controllers/books_controller.rb
def show
#   @book = Book.find(params[:id]) コメントアウトしました。
    @book = Book.where(user_id: params[:id])
end

user_id が パラメーター番号の人の蔵書データをすべて取得して@bookに格納しています。
View も編集していきましょう。 @book のなかに複数のデータがある場合(連想配列)
each を用いて、順に表示させていくのでしたね。(2章の復習です)

app/views/books/show.html.erb
<% @book.each do |book| %>
  <p>本のタイトル: <%= book.title %></p>
  <p>著者: <%= book.author %></p>
<% end %>

http://localhost:3000/books/show/1
にアクセスしてみると、 user_id:1 になっている本のデータがすべて表示されました。

 本のタイトル: 吾輩は猫である
 著者: 夏目漱石

 本のタイトル: Alice's Adventures in Wonderland
 著者: Lewis Carroll

【補足】「あれ? has_many とかつけなくてよいんだっけ?」
と思った方は、前章の内容を覚えてくれていますね。
ただ少し勘違いがあります。 has_many,belongs_to は異なるテーブルの紐付けに使います。
今回は、 Book Modelを経由し、Bookテーブルだけにアクセスしているので不要です。
User Model から、Bookテーブルのデータを取り出すならば必要になってきます。

データ登録フォームの作成

さて、データベースから情報を取り出したり加工したりする方法は覚えました。
しかしこのままではユーザーは、本を登録することができません。

本を登録するための機能を作成する方法を学んでいきましょう。

登録形式は、フォーム。
URL は http://localhost:3000/books/new
とします。

form_for

データベースに新しいデータを保存したいので、 Model を呼び出しましょう。
場所はどこに書きましょう。
new.html.erb に登録フォームを置くので、 new アクションの中ですね。
新しいデータを登録するときは、 Model名.new とします。下記のコードを追記してください。

app/controllers/books_controller.rb
  def new
    @book = Book.new
  end

new.html.erb に 入力フォームを作成していきましょう。
rails では、フォームを簡単につくれる form_forヘルパーというものがあります。

app/views/books/new.html.erb
<%= form_for(@book) do |f| %>
  <%= f.label :country %>
  <%= f.text_field :country %>

  <%= f.label :title %>
  <%= f.text_field :title %>

  <%= f.label :author %>
  <%= f.text_field :author %>

  <%= f.label :year %>
  <%= f.number_field :year %>

  <%= f.label :user_id %>
  <%= f.number_field :user_id %>

  <%= f.label :comment %>
  <%= f.text_area :comment %>

  <%= f.submit "送信" %>
<% end %>

form_for に関する詳しい使い方は、こちらのサイトで学習してください。
form_forの使い方を徹底解説!

ここでは重要な部分だけ、簡単に解説します。
上の form_forヘルパーにより、下記のような HTML が生成されます。(少しシンプル化してます)

new.html
<form class="new_book" id="new_book" action="/books" method="post">
   <label for="book_country">Country</label>
   <input type="text" name="book[country]" id="book_country" />

   <label for="book_title">Title</label> 
   <input type="text" name="book[title]" id="book_title" /> 

   <label for="book_author">Author</label> 
   <input type="text" name="book[author]" id="book_author" /> 

   <label for="book_year">Year</label> 
   <input type="number" name="book[year]" id="book_year" /> 

   <label for="book_user_id">User</label> 
   <input type="number" name="book[user_id]" id="book_user_id" /> 

   <label for="book_comment">Comment</label> 
   <textarea name="book[comment]" id="book_comment">  </textarea> 

   <input type="submit" name="commit" value="送信" /> 
 </form> 

ここで重要な属性は、
< form class="new_book" id="new_book" action="/books" method="post" >
の中の、 action="/books"method="post" の2つです。

この2つは送信ボタンが押されたら、
/books に対して、 POST リクエストを送信する
という指示をしています。

なので、 /books 対して、 POST リクエストがあった場合の処理を書いていきましょう。
送信したあとは、books Controller の create アクションに飛ばしたいです。
(のちほどデータベースに保存する処理を create アクションに書いていきます)

post 'books', to: 'books#create'
コードはこのようになるので、 routes.rb に追記しましょう。
(ルーティング関係は、 routes.rb でしたね)

config/routes.rb
Rails.application.routes.draw do
  get 'books/index'
  get 'books/show/:id', to: 'books#show'
  get 'books/new'
  post 'books', to: 'books#create'
end

index と new は to: が書かれていませんね。
前章でも説明したとおり、Railsは、アクションが呼び出されると、
自動的に アクション名.html.erbが呼び出されるので、
下記と同じ意味になります。
get 'books/index' to: 'books#index'
get 'books/new', to: 'books#new'

1,登録ボタンが押される
2,フォームに入力されたデータを、create アクションに飛ばす

という流れで、データが動いているので最後に、create アクションの中で
情報をデータベースに保存する処理を書きます。Modelを呼び出します。

書き込みの仕方は、seed.rb でもやりましたね。

seed.rb
@book = Book.new
@book.country = "jp"
@book.title = "吾輩は猫である"
@book.author = "夏目漱石"
@book.year = 1905
@book.user_id = 1
@book.comment = "おもしろかった"
@book.save

 【備考】
 ちなみに、このままなにも受け取り処理を書かずに情報を送信してみるとどうなるでしょうか。
 試しに、本の情報をフォームに入力し送信してみると、なにも動きません。

 送信しているので、フォームから パラメーターは飛んでいます。
 このパラメーターですが、実は見ることが出来るんです。

 rails s を起動している、 terminalをみてください。
 いつの間にか、いろんな文字が出力されていますね。
 サーバーが受け取ったデータなどの情報が表示されています。
 この先、困ったときにここをみると解決できることがあると思います。

 "どういったデータがどういう形式で動いているかは、サーバーのログをみる"
 自分でサービスをつくるのに、重要なテクニックなので覚えておいてください

 

 
サーバーログの一番下をみてみましょう。(下図は、ややシンプル化しています)

terminal
Started POST "/books"
Processing by BooksController#create as HTML
  Parameters: {"book"=>{"country"=>"jp", "title"=>"吾輩は猫である", "author"=>"夏目漱石",
               "year"=>"1905", "user_id"=>"1", "comment"=>"おもしろかった"}}

パラメーターをみると、二重の連想配列になっているのがわかると思います。
( parameters{} の中に book{} が入っている )

通常の連想配列の場合は、params[:title] として取得できていました。
二重の連想配列の場合は、params[:book][:title] とすることで取得できます。
bookキー の中のtitleキー のデータを取得しています )
 連想配列について詳しくはこちらを参照してください Array(配列)とHash(連想配列)入門

さて、ここまで理解できていれば create アクションの中身も想像できると思います。
( seed.rb の、本の情報が書かれていた部分をパラメーターの中身に書き換えるだけです )

app/controllers/books_controller.rb
  def create
    @book = Book.new
    @book.country = params[:book][:country]
    @book.title = params[:book][:title]
    @book.author = params[:book][:author]
    @book.year = params[:book][:year]
    @book.user_id = params[:book][:user_id]
    @book.comment = params[:book][:comment]
    @book.save
    redirect_to '/books/show/1'
  end

最後の、redirect_to '/books/show/1' の部分ですが、
登録ボタンを押したあとに、画面遷移してほしいので、リダイレクト先を指定しました。

http://localhost:3000/books/new ここで本の情報を登録
http://localhost:3000/books/show/1 ここで登録内容を確認
(Userの欄を、1以外にした人は、該当ユーザーのページを開いてください)

課題

1, show ページに、タイトルと著者以外の情報も表示してください。
2, リダイレクト先を、 http://localhost:3000/books/index に変更してください。
3, index ページに、全蔵書のタイトルを表示させてださい。

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

Before Rails Tutorial3章 Scaffold を用いない開発

本投稿は講義資料であり、Rubyの基礎は理解しているが、rails tutorialで躓く読者を対象としています。
1章:環境構築
2章:Scaffold を用いた高速なアプリケーション構築 及び MVCの理解
3章:Scaffold を用いない開発方法 及び 応用

前章では、Scaffoldを用いて高速でアプリケーションを構築しました。
自動化された部分も多く、現段階では、大まかな理解であるとおもいます。

今回は、route Model View Controller それぞれ自分で作っていきます。
とはいっても、継承まで含めてすべて書くには理解することが膨大で大変ですので、
前回も使用した generate は使用していきます。

まずは、Railsの新しいアプリケーションを作成します。
1章:環境構築の内容を参考に作成してみましょう。

terminal
$ cd ~/rails_app
$ mkdir bukukore
$ bundle init
   (Gemfileにgem "rails","~> 5.2.3"を追加し保存してください)
$ bundle install --path vendor/bundlesa
$ bin/rails s

http://localhost:3000/ にアクセスして表示されていればokです。

$ rails s -p 3100 などとポートを変更することでサーバーを複数起動可能です。
この場合は http://localhost:3100/ にアクセスします。

MVC 復習

前章では、アプリ制作を通して、MVCの流れを理解してきました。

Modelは、データベースとのやりとりを担う
Viewは、表示関連全般を担う
Controllerは、橋渡しの役割を担う

前回、制作したながれを図を見ながら復習していきます。
それぞれの番号が、コードではどうなっていたか思い出しながら見てください。

MVC-02.png

1, ブラウザから /users へのリクエストを Rails sever に送信する
2, router によって、/users は Users controller の index アクションを呼び出す
3, index アクションから、User Model が呼び出される
4, User Model は DataBase からデータを取り出す
5, 取り出したデータを、Controller に返す
6, 受け取ったデータを @users に保存し、 View (indexアクションなので index.html.erb) に渡す。
7, ERBを実行し、@users のデータを含んだ HTMLを生成し、Controller へ返す
8, Controller は、受け取ったHTMLをブラウザにわたす

今回も Model View Controller を構築していくので、適宜図を見返してください。

Controller と View の作成

作成は基本的に generate コマンドを用います。
(gは generateコマンドの短縮形です)
g の後は、controller に続けて、コントローラーの名前(複数形)を書きます。
rails g controller Books index show new
そのうしろ(引数といいます)に書いたものは、同名のviewとアクションが自動生成されます。

tarminal
$ bin/rails g controller Books index show new
     create    app/controllers/books_controller.rb
     route     get 'books/index'
               get 'books/show'
               get 'books/new'
     create    app/views/books
     create    app/views/books/index.html.erb
     create    app/views/books/show.html.erb
     create    app/views/books/new.html.erb

ルーティングが生成されているので、確認してみましょう。

terminal
$ bin/rails routes
  Verb  URI             Pattern
  GET   /books/index    books#index
  GET   /books/show     books#show
  GET   /books/new      books#new

これは具体的には、 http://localhost:3000/books/index
というURLに対するリクエストがあれば、 books_controller の index アクション を呼び出すというものでした。
今回アクションの中身が空なので、Model は呼び出されず、単に対応するView (index.html.erb) が実行されるだけです。

ルーティングがうまくいってみるか確認するために、試しにアクセスしてみましょう。
http://localhost:3000/books/index

Books#index
Find me in app/views/books/index.html.erb

と表示されているとおもいます。

Viewの変更

erbの変更は前回行ったとおりですね。とてもシンプルなものでした。
Books#index の部分を、全蔵書一覧などに変更しておきましょう。

app/views/books/index.html.erb
<h1>全蔵書一覧</h1>

 erb の中身がシンプルすぎて不思議な方もいらっしゃるかもしれません。
 【気になって次にすすめない人向けの解説】
  app/views/layouts/application.html.erb を開いてください。
 コード内の、<%= yiels %> の部分に、 index.html.erb が移植されて HTML を生成しています。 
 いまはこの程度の理解で問題ありません。 https://railstutorial.jp/ でより深く理解できます。

Controllerの変更

コントローラーの中にアクションが本当に作られているかも確認してみましょう。
app/controllers/books_controller.rb を開いてみてください。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
  end

  def show
  end

  def new
  end
end

データベース内のデータを取り出すには、Controllerのアクションから
Model を呼び出せば良いのでした。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
  end

  def show
   @book = Book.find(1)
  end

  def new
  end
end

このままではエラーがでます。(Modelも、本の情報も作っていないので)
generate コマンドを使用して、Model を作成していきます。

Model の作成

booksテーブルには、前章と同じカラムを持たせていきます。

terminal
$ bin/rails g model Book country:text title:text author:text year:integer user_id:integer comment:text
      create    db/migrate/20200202020202_create_books.rb
      create    app/models/book.rb

g の後は、model に続けて、Model名(単数形)を書きます。
データベースにテーブルを作成するには migrate を実行するのでしたね。

terminla
$ bin/rails db:migrate
== 20200202020202 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0008s
== 20200202020202 CreateBooks: migrated (0.0009s) =============================

補足. migrate はマイグレーションファイルを実行しています。
db/migrate/(作成日時)_create_books.rb というファイルです。
ひらいてみると、下記のようなコードがあると思います。

class CreateBooks < ActiveRecord
 def change
   create_table :books do |t|
    t.text :country
    t.text :title
    t.text :author
    t.integer :year
    t.integer :user_id
    t.text :comment
    t.timestamps
   end
 end
end

上記は text型であるcountryというカラム(他6つ)を持つ books テーブルを作成
せよという意味です。 change 意外に up down などもあります。詳細は下記サイトなどを参照ください
https://www.sejuku.net/blog/14229

これでbooksテーブルが作成されました。

テーブルは作成したけども、本の情報はまだなにもいれてませんね。
初期データをいれるには db/seeds.rb ファイルを使用します。
( seed = 種 という意味です )
大量のデータを入力するのは手間なのでseed.rbを作成していきます。
(そもそもブラウザからデータをいれる機能もまだ作っていません)

下記の seed ファイルを実行すると、bookテーブルに本の情報が3件保存されます

db/seeds.rb
@book = Book.new
@book.country = "jp"
@book.title = "吾輩は猫である"
@book.author = "夏目漱石"
@book.year = 1905
@book.user_id = 1
@book.comment = "おもしろかった"
@book.save

@book = Book.new
@book.country = "jp"
@book.title = "人間失格"
@book.author = "太宰治"
@book.year = 1948
@book.user_id = 2
@book.comment = "すばらしかった"
@book.save

@book = Book.new
@book.country = "gb"
@book.title = "Alice's Adventures in Wonderland"
@book.author = "Lewis Carroll"
@book.year = 1865
@book.user_id = 1
@book.comment = "Amazing"
@book.save

seed ファイルは、下記のコマンドで実行できます。

terminal
$ bundle exec rake db:seed

データが保存されたかの確認は。rails c でコンソールを起動すると確認できます。

railsConsole
@book = Book.find(1)

ブラウザ(show)で表示してみましょう。

app/view/books/show.html.erb
<p>本のタイトル: <%= @book.title %></p>
<p>著者: <%= @book.author %></p>

本のタイトル: 吾輩は猫である

著者: 夏目漱石

と表示されていればOKです。

課題1,

@book はどこで定義したか、再確認してください。

URLからパラメーターの取得

本のデータを取り出して表示するところまでうまくいきました!
すごい進歩です!

ただ、まだ不満点があります。
それは、 id:1 の本のデータしか表示できていない点です。
理由は、Contoroller の show アクションをみると、
@book = Book.find(1)
となっているためです(id:1 の本の情報しか見つけてきていない)

ここをidパラメーターが1以外も入力できるように変更するにはどうすればよいでしょうか。
@book = Book.find(params[:id])
とすることで、1以外のパラメーターを見つけてこれます。

app/controllers/books_controller.rb
 def show
    @book = Book.find(params[:id])
 end

パラメーター(数値)は、どこで指定すればよいでしょうか。
URL にパラメーターを含めて渡すテクニックがあるので覚えておきましょう。
例)ID:2の本の情報を取得したい場合 http://localhost:3000/books/show/2
  このままではルーティングエラーが表示されます。
  books/show へのリクエストに関しては記述していますが、
  books/show/(数字) へのリクエストについてはなにも書いていない為です。

config/routes.rb を開いてください。
URLに含まれる数字をパラメーターとして取得するには、下記のように記述します。

config/routes.rb
#   get 'books/show'(# でコメントアウトしてます)
get 'books/show/:id', to:'books#show'

こうすることで /books/show/2 の 2の部分が、:id パラメーターとして利用可能になります。
( 利用するときは、params[:id] とかきます)

to: 'books#show' 
books Cコントローラーの show アクションを呼び出す指示になります。

これで、任意のidの本情報を取得できるようになりました。

http://localhost:3000/books/show/1
http://localhost:3000/books/show/2

Model 検索

いま本の情報の取り出しに id を利用していますが、
id 以外のカラムを用いて取り出す方法も覚えておくと便利です。

コンソールを起動してください。 ( rails c )
下記を参考に色々と試してみてください。

railsConsole
>> Book.all
   #bookテーブルのすべてのデータを取得します。
>> Book.find_by(title: "人間失格")
   #条件に合ったデータを、1件だけ取得します。
>> Book.where(country: "jp")
   #条件に合ったデータを、すべて取得します。
>> Book.where(country: "jp").where(year: 1905)
   #AND検索。 和書で 1905年出版の本を取得しています。
>> Book.where("(country = ?) OR (year = ?)","jp",1905)
   #OR検索。 和書 または 1905年出版の本を取得しています。
>> Book.order(create_at: :asc)
   #並び順を指定します。 作成日時の昇順で取得しています。
>> Book.where(user_id: 1).order(id: :desc)
   #user_id 1さんの蔵書を、idの降順で取得しています。 

where の利用

検索条件を学んだのでこれ使って、蔵書管理アプリケーションらしく
個人の蔵書を一覧表示する機能を構築してみましょう。

app/controllers/books_controller.rb
def show
#   @book = Book.find(params[:id]) コメントアウトしました。
    @book = Book.where(user_id: params[:id])
end

user_id が パラメーター番号の人の蔵書データをすべて取得して@bookに格納しています。
View も編集していきましょう。 @book のなかに複数のデータがある場合(連想配列)
each を用いて、順に表示させていくのでしたね。(2章の復習です)

app/views/books/show.html.erb
<% @book.each do |book| %>
  <p>本のタイトル: <%= book.title %></p>
  <p>著者: <%= book.author %></p>
<% end %>

http://localhost:3000/books/show/1
にアクセスしてみると、 user_id:1 になっている本のデータがすべて表示されました。

 本のタイトル: 吾輩は猫である
 著者: 夏目漱石

 本のタイトル: Alice's Adventures in Wonderland
 著者: Lewis Carroll

【補足】「あれ? has_many とかつけなくてよいんだっけ?」
と思った方は、前章の内容を覚えてくれていますね。
ただ少し勘違いがあります。 has_many,belongs_to は異なるテーブルの紐付けに使います。
今回は、 Book Modelを経由し、Bookテーブルだけにアクセスしているので不要です。
User Model から、Bookテーブルのデータを取り出すならば必要になってきます。

データ登録フォームの作成

さて、データベースから情報を取り出したり加工したりする方法は覚えました。
しかしこのままではユーザーは、本を登録することができません。

本を登録するための機能を作成する方法を学んでいきましょう。

登録形式は、フォーム。
URL は http://localhost:3000/books/new
とします。

form_for

データベースに新しいデータを保存したいので、 Model を呼び出しましょう。
場所はどこに書きましょう。
new.html.erb に登録フォームを置くので、 new アクションの中ですね。
新しいデータを登録するときは、 Model名.new とします。下記のコードを追記してください。

app/controllers/books_controller.rb
  def new
    @book = Book.new
  end

new.html.erb に 入力フォームを作成していきましょう。
rails では、フォームを簡単につくれる form_forヘルパーというものがあります。

app/views/books/new.html.erb
<%= form_for(@book) do |f| %>
  <%= f.label :country %>
  <%= f.text_field :country %>

  <%= f.label :title %>
  <%= f.text_field :title %>

  <%= f.label :author %>
  <%= f.text_field :author %>

  <%= f.label :year %>
  <%= f.number_field :year %>

  <%= f.label :user_id %>
  <%= f.number_field :user_id %>

  <%= f.label :comment %>
  <%= f.text_area :comment %>

  <%= f.submit "送信" %>
<% end %>

form_for に関する詳しい使い方は、こちらのサイトで学習してください。
form_forの使い方を徹底解説!

ここでは重要な部分だけ、簡単に解説します。
上の form_forヘルパーにより、下記のような HTML が生成されます。(少しシンプル化してます)

new.html
<form class="new_book" id="new_book" action="/books" method="post">
   <label for="book_country">Country</label>
   <input type="text" name="book[country]" id="book_country" />

   <label for="book_title">Title</label> 
   <input type="text" name="book[title]" id="book_title" /> 

   <label for="book_author">Author</label> 
   <input type="text" name="book[author]" id="book_author" /> 

   <label for="book_year">Year</label> 
   <input type="number" name="book[year]" id="book_year" /> 

   <label for="book_user_id">User</label> 
   <input type="number" name="book[user_id]" id="book_user_id" /> 

   <label for="book_comment">Comment</label> 
   <textarea name="book[comment]" id="book_comment">  </textarea> 

   <input type="submit" name="commit" value="送信" /> 
 </form> 

ここで重要な属性は、
< form class="new_book" id="new_book" action="/books" method="post" >
の中の、 action="/books"method="post" の2つです。

この2つは送信ボタンが押されたら、
/books に対して、 POST リクエストを送信する
という指示をしています。

なので、 /books 対して、 POST リクエストがあった場合の処理を書いていきましょう。
送信したあとは、books Controller の create アクションに飛ばしたいです。
(のちほどデータベースに保存する処理を create アクションに書いていきます)

post 'books', to: 'books#create'
コードはこのようになるので、 routes.rb に追記しましょう。
(ルーティング関係は、 routes.rb でしたね)

config/routes.rb
Rails.application.routes.draw do
  get 'books/index'
  get 'books/show/:id', to: 'books#show'
  get 'books/new'
  post 'books', to: 'books#create'
end

index と new は to: が書かれていませんね。
前章でも説明したとおり、Railsは、アクションが呼び出されると、
自動的に アクション名.html.erbが呼び出されるので、
下記と同じ意味になります。
get 'books/index' to: 'books#index'
get 'books/new', to: 'books#new'

1,登録ボタンが押される
2,フォームに入力されたデータを、create アクションに飛ばす

という流れで、データが動いているので最後に、create アクションの中で
情報をデータベースに保存する処理を書きます。Modelを呼び出します。

書き込みの仕方は、seed.rb でもやりましたね。

seed.rb
@book = Book.new
@book.country = "jp"
@book.title = "吾輩は猫である"
@book.author = "夏目漱石"
@book.year = 1905
@book.user_id = 1
@book.comment = "おもしろかった"
@book.save

 【備考】
 ちなみに、このままなにも受け取り処理を書かずに情報を送信してみるとどうなるでしょうか。
 試しに、本の情報をフォームに入力し送信してみると、なにも動きません。

 送信しているので、フォームから パラメーターは飛んでいます。
 このパラメーターですが、実は見ることが出来るんです。

 rails s を起動している、 terminalをみてください。
 いつの間にか、いろんな文字が出力されていますね。
 サーバーが受け取ったデータなどの情報が表示されています。
 この先、困ったときにここをみると解決できることがあると思います。

 "どういったデータがどういう形式で動いているかは、サーバーのログをみる"
 自分でサービスをつくるのに、重要なテクニックなので覚えておいてください

 

 
サーバーログの一番下をみてみましょう。(下図は、ややシンプル化しています)

terminal
Started POST "/books"
Processing by BooksController#create as HTML
  Parameters: {"book"=>{"country"=>"jp", "title"=>"吾輩は猫である", "author"=>"夏目漱石",
               "year"=>"1905", "user_id"=>"1", "comment"=>"おもしろかった"}}

パラメーターをみると、二重の連想配列になっているのがわかると思います。
( parameters{} の中に book{} が入っている )

通常の連想配列の場合は、params[:title] として取得できていました。
二重の連想配列の場合は、params[:book][:title] とすることで取得できます。
bookキー の中のtitleキー のデータを取得しています )
 連想配列について詳しくはこちらを参照してください Array(配列)とHash(連想配列)入門

さて、ここまで理解できていれば create アクションの中身も想像できると思います。
( seed.rb の、本の情報が書かれていた部分をパラメーターの中身に書き換えるだけです )

app/controllers/books_controller.rb
  def create
    @book = Book.new
    @book.country = params[:book][:country]
    @book.title = params[:book][:title]
    @book.author = params[:book][:author]
    @book.year = params[:book][:year]
    @book.user_id = params[:book][:user_id]
    @book.comment = params[:book][:comment]
    @book.save
    redirect_to '/books/show/1'
  end

最後の、redirect_to '/books/show/1' の部分ですが、
登録ボタンを押したあとに、画面遷移してほしいので、リダイレクト先を指定しました。

http://localhost:3000/books/new ここで本の情報を登録
http://localhost:3000/books/show/1 ここで登録内容を確認
(Userの欄を、1以外にした人は、該当ユーザーのページを開いてください)

課題

1, show ページに、タイトルと著者以外の情報も表示してください。
2, リダイレクト先を、 http://localhost:3000/books/index に変更してください。
3, index ページに、全蔵書のタイトルを表示させてださい。

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

RailsでスコープとGraphQL::Batch::Loaderを使うときの注意点

RailsでGraphQL::Batch::Loaderを使うときの注意点です。

問題

N + 1問題を回避しようとしてGraphQL::Batch::Loaderを導入したが、N + 1問題が回避されなかった。

GraphQL::Batch::Loaderは以下のように、N + 1問題を回避するためのクラスです。
https://github.com/Shopify/graphql-batch#schema-configuration

原因

GraphQL::Batch::Loaderのforの引数にスコープ(ActiveRecord::Relation)を渡していたことが原因です。

GraphQL::Batch::Loaderでは、forの引数が同じ場合にBatch Loadされます。

同じというのは正確には、ハッシュのキーとして一致するという意味です。
ActiveRecord::Relationはハッシュのキーとして使えるように実装されていないので、実質的に同じでもハッシュのキーとして一致することはありません。

そのため、同じと認識されずBatch Loadされない。

解決策

ActiveRecord::Relationをハッシュのキーとして使えるようにするために、
以下のモンキーパッチを当てました。

module ActiveRecord
  class Relation
    def hash
      to_sql.hash
    end

    def eql?(other)
      self == other
    end
  end
end

Railsで実装検討されたこともあったみたいですが、放置されていました。
https://github.com/rails/rails/pull/33638

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