20210105のRubyに関する記事は12件です。

【初学者必見】Railsで「複数のカラムからの」検索機能を実装してみた。

はじめに

あけましておめでとうございます?
Railsで定番の検索機能。
検索機能に関しての記事はQiita上でもたくさんあるが、どれも一つのテーブルの中の一つのカラムから検索する方法ばかり。(あとは、もう古くなったform_tagが使われていたり)
でも実際は一つのテーブルの中の複数のカラムから検索したいケースもあるはず。
例えば、

db/schema.rb
ActiveRecord::Schema.define(version: 2020_12_18_025546) do

  create_table "agents", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "last_name", null: false
    t.string "first_name", null: false
    t.string "last_name_kana", null: false
    t.string "first_name_kana", null: false
    t.string "company_name", null: false
    t.string "company_location", null: false
    t.bigint "user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_agents_on_user_id"
  end
end

テーブルはこのような感じ↓
スクリーンショット 2021-01-05 16.53.46.png

上記のagentsテーブルは「氏」と「名」と「氏のフリガナ」と「名のフリガナ」が独立しているので、agentを特定する際に一つのカラムから検索することはできません。
ましてやこの場合は一つの検索フォームから会社名(company_name)だったり会社の所在地(company_location)からagentを特定できればもっと便利になるはずです。

順序

  1. Viewに検索フォームを配置する
  2. searchアクションのルーテインングを設定する
  3. 検索するためのメソッド(searchメソッド)をモデルに定義する ←ココ重要
  4. searchアクションをコントローラーに定義する
  5. 検索結果画面のViewを作成する

では早速やっていきましょう。

1. Viewに検索フォームを配置する

検索フォームを作りたいViewの中に検索フォームを書いていきます。

app/views/agents/index.html.erb
<%= form_with(url: search_agents_path, local: true, method: :get, class: "field has-addons") do |form| %>
  <%= form.text_field :keyword, placeholder: "営業マンを検索する", class: "input" %>
  <%= form.submit "検索", class: "button is-info" %>
<% end %>

※ class名はCSSフレイムワークのBULMAを使っています。
↓以下のように検索ボックスが作れました!
スクリーンショット 2021-01-05 18.17.21.png

2. searchアクションのルーテインングを設定する

searchアクションはRailsのデフォルトである7つのアクション(index, show, new, create, edit, update, destroy)以外なので”search”と言う名前でアクションを定義します。

自分で定義したアクションにはmemberまたはcollectionというオプションを使う必要があります。
詳細は ココで確認してみてください。
今回は:idを関連付ける必要が無いので、collectionを使います。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'homes#top'
  resources :agents, only: [:index, :show, :new, :create] do
    resources :reviews, only: [:index, :create, :edit, :update, :destroy]
    collection do     #←ココ
      get 'search'    #←ココ
    end               #←ココ
  end
end

3. 検索するためのメソッド(searchメソッド)をモデルに定義する ココ重要

whereメソッドとLike句を使います。

whereメソッドとは

モデルが使用できる、ActiveRecordメソッドの1つ。
モデル名.where(条件)のの形で引数部分に条件を指定すること、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できます。引数の条件には、「検索対象となるカラム」を必ず含めなければなりません。

Like句とは

曖昧な文字列で検索しても、テーブルの対象のカラムから検索したいもの探し出せるようにするために使用するSQLのクエリです。whereメソッドの条件の中で使います。

一つのカラムから検索する場合は、以下のような構文をモデル内で用います。
モデル名.where('探し出したいカラム名 LIKE(?)', "%#{search}%")

複数のカラムから検索したい場合は、(例として3つのカラムから探したい時)
モデル名.where('探し出したいカラム名 LIKE(?) OR 探し出したいカラム名 LIKE(?) OR 探し出したいカラム名 LIKE(?)', "%#{search}%", "%#{search}%", "%#{search}%")
のような構文になります。
「探し出したいカラム名 LIKE(?)」を「OR」で繋いで、カラムの数の分だけ「"%#{search}%"」を書くことによって複数のカラムから検索をすることができます。

具体的には、以下のようにsearchメソッドをモデル内で定義しました。

app/models/agent.rb
class Agent < ApplicationRecord
  with_options presence: true do
    validates :user_id
    validates :company_name
    validates :company_location

    with_options format: { with: /\A(?:\p{Hiragana}|\p{Katakana}|[ー-]|[一-龠々])+\z/ } do
      validates :first_name
      validates :last_name
    end
    with_options format: { with: /\A[ァ-ヶー-]+\z/ } do
      validates :last_name_kana
      validates :first_name_kana
    end
  end

  def avg_score
    if reviews.empty?
      0.0
    else
      reviews.average(:score).round(1).to_f
    end
  end

  def review_score_percentage
    if reviews.empty?
      0.0
    else
      reviews.average(:score).round(1).to_f / 5 * 100
    end
  end

#↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ココ
  def self.search(search)
    if search != ""
      Agent.where('last_name LIKE(?) OR first_name LIKE(?) OR last_name_kana LIKE(?) OR first_name_kana LIKE(?) OR company_name LIKE(?) OR company_location LIKE(?)', "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%")
    else
      Agent.all
    end
  end
#↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ココ

  has_many :reviews, dependent: :destroy
  belongs_to :user
end

if文を用いて、検索結果が一つも無い場合は全てのagentを出力するようにしました。

4. searchアクションをコントローラーに定義する

以下のようにsearchアクションをコントローラーに追加します。

app/models/agent.rb
def search
  @agents = Agent.search(params[:keyword])
end

検索フォームから送られて来ているパラメーターである:keywordを引数にします。

5. 検索結果画面のViewを作成する

新しくsearch.html.erbを作成することで、検索結果を表示するviewを作成します。
コントローラーで定義した@agentsをeach文を用いて全て表示していきます。

app/views/agents/search.html.erb
<div class="section pb-0">
  <div class="container">
    <div class="columns is-centered">
      <div class="column is-6">
        <%= form_with(url: search_agents_path, local: true, method: :get, class: "field has-addons") do |form| %>
          <%= form.text_field :keyword, placeholder: "営業マンを検索する", class: "input" %>
          <%= form.submit "検索", class: "button is-info" %>
        <% end %>
        <div>
          次の検索結果を表示しています:<span class="has-text-weight-bold is-size-4"> <%= params[:keyword] %></span>
        </div>
      </div>
    </div>
  </div>
</div>

<section class="section">
  <div class="container">
    <div class="columns is-centered">
      <div class="column is-5">
        <% @agents.each do |agent| %>
          <div class="card mb-6">
            <header class="card-header">
              <p class="card-header-title">
                <%= agent.last_name %> <%= agent.first_name %>
                <span class="has-text-weight-light is-italic is-size-7">(<%= agent.last_name_kana %> <%= agent.first_name_kana %>)</span>
              </p>
            </header>
            <div class="card-content">
              所属会社: <%= agent.company_name %><br>
              場所: <%= agent.company_location %><br>
              <div class="content">
                <div class="content average-score">
                  <div class="star-rating mb-2">
                    <div class="star-rating-front" style="width:  <%= agent.review_score_percentage %>%">★★★★★</div>
                    <div class="star-rating-back">★★★★★</div>
                  </div>
                  <div class="average-score-display ml-3 pt-2">
                  <%= agent.avg_score %>点(<%= agent.reviews.count %>件のレビュー)
                  </div>
                </div>
              </div>
            </div>
            <footer class="card-footer">
            <%= link_to agent_reviews_path(agent), class: "button card-footer-item" do %>
            レビューを見る
            <% end %>
            <%= link_to agent_path(agent), class: "button card-footer-item" do %>
            レビューを書く
            <% end %>
            </footer>
          </div>
        <% end %>
        <div class="is-centered">
          <%= link_to new_agent_path, class: "button is-primary  mt-3" do %>
            新しい営業マンを追加する
          <% end %>
        </div>
    </div>
  </div>
</div>

※ class名はCSSフレイムワークのBULMAを使っています。

↓以下のような検索結果画面ができました。
これは検索ボックスに「木」と入力して出た検索結果です。
スクリーンショット 2021-01-05 23.26.52.png

最後に

自分がRailsで検索機能を実装していてあったら良いなと思うものを書いてみました。
いかがでしたでしょうか。
間違っている点やわかりにくい点があったらお気軽に教えてください!
では次回の投稿でお会いしましょう。

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

【Ruby】配列から重複していない要素を取得する

概要

配列内の重複していない要素を取得し、計算するというプログラムを書きました。
重複しているものがほしいときには比較演算子の記述を変更すれば取得することができます。認識に間違いがありましたらご指摘いただけたら幸いです。

目次

  • 実践

    • 問題
    • 解答
  • 参考文献

実践

問題

任意の3つの数値の合計を出力するメソッドを作成してください。ただし、同じ数が含まれている場合はカウントしない。

解答

ハッシュを始めに生成しているのは、要素数を計算し、条件に合うものを select で取得したいからです。

def uniq_num(ary)
  counts = Hash.new(0)                             # ハッシュを生成
  ary.each { |v| counts[v] += 1 }                  # 重複している要素を検索
  i = counts.select { |v, count| count == 1 }.keys # 重複していないものだけ取得
  p i.sum                                          # 合計を出力
end

# メソッド呼び出し
uniq_num([1, 2, 3])
uniq_num([3, 2, 3])
uniq_num([3, 3, 3])

# ターミナル出力結果

# 6
# 2
# 0

参考文献

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

Herokuにデプロイしたのにうまく反映できない時

はじめに

忘備録として記載します。

結論

「Command+z」で戻りすぎていた。

過程

デプロイした後、
heroku open
でブラウザを立ち上げたところ、なぜか反映されていない。
そこで、念のためローカルホストでまずは確認した。
すると、書いたはずのコードが消失していた。
「Command+z」で戻りすぎてしまい、消えたと思われる。

こんなミスで1時間もの間、Pushしたり、マイグレートしたりと不毛な時間を過ごしてしまったので、しっかり確認してから進めていこう。

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

【Ruby on Rails】resourcesメソッドを使って、ルーティングを自動で作成する。

はじめに

resourcesメソッドについて、備忘録として残しておきます。

resourcesメソッド

resourcesメソッドはroutes.rbファイルの中に書き込むメソッドです。
・Railsの基本となる7つのアクションのルーティングをまとめて追加することができます。

Railsの基本となる7つのアクション

基本となる7つのアクションを以下に記載します。

アクション HTTP 役割 URL
index get リソースの一覧を表示する。 /users
show get リソースの詳細を表示する。 /users/:id
new get リソースを新規作成する。 /users/new
create post リソースを新規作成して、保存する。 /users
edit get リソースを編集する。 /users/:id/edit
update put/patch リソースを更新させる。 /users/:id
destroy delete リソースを削除する。 /users/:id

定義の仕方

まずは、resourcesメソッドを定義していない状態でルーティングを確認し、何も定義されていないことを確認します。

続いて、以下のようにresourcesメソッドを定義していきます。
※「users」の部分には作成したコントローラ名が入ります。今回はusers_controller.rbを例に行います。

routes.rb
Rails.application.routes.draw do
#resourcesメソッド定義する
  resources :users
end

定義した後にルーティングを確認すると・・・
基本的な7つのアクションのルーティングが追加されていることがわかります。

Prefix    Verb      URI Pattern                  Controller#Action

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

resourcesメソッドのオプション

ここからは、resourcesメソッドのオプションをめもしておきます。

only

7つのアクションのうち、特定のアクションのみを指定したい時に使用できます。

routes.rb
Rails.application.routes.draw do
#createアクションとnewアクションのみ
  resources :users, only:[:create, :new]
end

ルーティングを見てみると、こんな感じになっています。

          POST      /users(.:format)              users#create
new_user  GET       /users/new(.:format)          users#new

memeber

memberメソッドは、7つのアクション以外のアクションを追加することができます。
まずは、定義方法について、以下記載します。

routes.rb
Rails.application.routes.draw do
#memberメソッドの定義方法
  resources :users do
    member do
      get :following, :followers
    end
  end

このように定義することで、7つのアクション以外に以下のようなルーティングが得られます。
memberメソッドでは、idで指定した個々のリソースに対するアクションを定義できます。

Prefix            Verb      URI Pattern                       Controller#Action

following_user    GET       /users/:id/following(.:format)    users#following
followers_user    GET       /users/:id/followers(.:format)    users#followers
users             GET       /users(.:format)                  users#index
                  POST      /users(.:format)                  users#create
new_user          GET       /users/new(.:format)              users#new
edit_user         GET       /users/:id/edit(.:format)         users#edit
user              GET       /users/:id(.:format)              users#show
                  PATCH     /users/:id(.:format)              users#update
                  PUT       /users/:id(.:format)              users#update
                  DELETE    /users/:id(.:format)              users#destroy

collection

collectionメソッドもmemberメソッドと同様に7つのアクション以外のアクションを追加することができます。
memberメソッドと違う点は、collectionメソッドはリソース全体に対するアクションを定義するという点です。

定義方法もmemberメソッドと同様です。

routes.rb
Rails.application.routes.draw do
#collectionメソッドの定義方法
  resources :users do
    collection do
      get :following, :followers
    end
  end

ルーティングを見てみると、memberメソッド同様7つのアクション以外に追加されていることがわかります。
が、URI Patternの箇所が異なります。
collectionメソッドの方は、全てのリソースに対してアクションを定義しているので:/idの部分が省略されています。

Prefix            Verb      URI Pattern                       Controller#Action

following_user    GET       /users/following(.:format)    users#following
followers_user    GET       /users/followers(.:format)        users#followers
users             GET       /users(.:format)                  users#index
                  POST      /users(.:format)                  users#create
new_user          GET       /users/new(.:format)              users#new
edit_user         GET       /users/:id/edit(.:format)         users#edit
user              GET       /users/:id(.:format)              users#show
                  PATCH     /users/:id(.:format)              users#update
                  PUT       /users/:id(.:format)              users#update
                  DELETE    /users/:id(.:format)              users#destroy

【2つのメソッドの比較】

Prefix            Verb      URI Pattern                       Controller#Action

##memberメソッド
following_user    GET       /users/:id/following(.:format)    users#following
followers_user    GET       /users/:id/followers(.:format)    users#followers

##collectionメソッド
following_user    GET       /users/following(.:format)        users#following
followers_user    GET       /users/followers(.:format)        users#followers

参考文献

Rails tutorial 第7章 ユーザー登録
https://railstutorial.jp/chapters/sign_up?version=4.2

Rails tutorial 第14章 ユーザーをフォローする
https://railstutorial.jp/chapters/following_users?version=6.0#cha-following_users

【Rails】resourcesメソッドを使ってルーティングを定義しよう!
https://pikawaka.com/rails/resources

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

デプロイ前にファイルの容量を確認

ファイルの容量を確認する。

AWSでECRを使用してデプロイしようとしましたがbuildとpushにとても時間がかかっていました。この原因はファイルの容量が重すぎたことでした。

du -sh ./*/

このコマンドで各ディレクトにファイルサイズが確認できます。
今は軽くなりましたが、当初はpublicディレクトが7Gほどありました。

スクリーンショット 2021-01-05 13.56.36.png

publicフォルダのpacksフォルダが原因だったのですが、これはWebpackerで生成するアセットの置き場です。

古いファイルは不要なので、デプロイする前には1度public/packsフォルダでいらないファイルを削除することをおすすめします。

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

【応用】課題図書Lesson2 解説問題 〜問題〜

応用カリキュラム課題図書Lesson2の解説問題です。

この記事の目的

課題図書Lesson2(Rubyを深掘る)が「解説可能」な状態かどうか判断すること。

「対応可能」な状態から一歩進んで、「解説可能」な状態かどうかを判断するための問題を用意しています。解説できているかどうかは出題者が判断します。

・「対応可能」とは → エラーに対応できるが、内容を解説してほしいと言われると不安。

・「解説可能」とは → 実装ロジックが説明でき、ヘルプ対応も可能。

問題1 〜Rubyの記法をさらに学ぼう〜

①以下のif文をcase文に変えてください。
(※適当にrubyファイルを作成し、実装してください。)

fruits = "apple"

if fruits == "apple"
 puts "リンゴ"
elsif  fruits== "orange"
 puts "オレンジ"
elsif  fruits == "strawberry"
 puts "イチゴ"
elsif  fruits == "banana"
 puts "バナナ"
elsif  fruits == "grape"
 puts "ブドウ"
else
 puts "..."
end

②eachメソッドとwhile文の違いについて説明してください。

③「無限ループ」の意味を説明してください。

④(1)breakメソッドとはどんなメソッドですか?
 (2)以下のコードの最後の戻り値はいくつですか?

number = 10

while number <= 10
 if number == 5
   break
 end
 puts number
 number -= 1
end

問題2 〜ブロックの理解を深めよう〜

①(1)以下のコードでyieldを使って、「食べる」とターミナルで出力されるように実装してください。
(2)また完成したコードの流れを説明してください。
(※適当にrubyファイルを作成し、実装してください。)

def eat

end

eat do |text|
  puts text
end

②(1)以下のコードでcallメソッドを使って、「飲む」とターミナルで出力されるように実装してください。
(2)また完成したコードの流れを説明してください。
(※適当にrubyファイルを作成し、実装してください。)

def drink

end

drink do |text|
  puts text
end

問題3 〜クラスの理解を深めよう〜

①(1)「クラスの継承」とは何か、また、継承するメリットを説明してください。
 (2)親クラスから小クラスに継承されるものを2つ答えてください。

②「オーバーライド」とは何か説明してください。

③以下のコードの流れを説明してください。また、最終的にターミナルにでる結果も答えてください。

class People
  def initialize(name, age)
    @name = name
    @age = age
  end

  def info
    puts "私は#{@name}と申します。#{@capacity}歳です。"
  end

 end

 class Teacher < People


  def subject(subject)
    @subject = subject
  end

  def info
    puts "私は#{@subject}の先生をしている、#{@name}と申します。"
  end

end

  teacher =  Teacher.new("山田", 5)

  teacher.subject("英語")

  teacher.info

問題4 〜用意された機能を呼び出す方法を学ぼう〜

①標準ライブラリ、組み込みライブラリ、外部ライブラリの違いと利用方法(インストールと読み込みの有無)について説明してください。

問題5 〜例外処理を学ぼう〜

①rails db:seedコマンドとはどんなときに、何を行うコマンドですか?

②以下のコードの流れを説明してください

users = []
10000.times do |i|
  users << User.new(name: "dummy-#{i+1}", ticket_count: 0)
end

User.import users
# importメソッドとは、引数に配列を渡して、まとめてレコードを作成するメソッドのこと
User.find(500).update(ticket_count: 2147483647)

③以下のrakeファイルで「こんにちは」と出力するために、ターミナルに打つべき、rakeの実行コマンドを答えてください。

lib/tasks/greeting.rake
namespace :greeting do
  desc "挨拶" 
  task hello: :environment do
    puts "こんにちは"
  end

  task goodbey: :environment do
    puts "さようなら"
  end
end

④応用課題図書のexception_sampleというアプリケーションにおいて、以下のタスクの処理の流れをbeginとrescueに触れながら説明してください。
(※カリキュラムでアプリケーションの仕様の確認をすることは可能です。)

lib/tasks/distribute_ticket.rake
namespace :distribute_ticket do
  desc "全ユーザーのticket_countをrescueしながら10増加させる"
  task rescue: :environment do
    User.find_each do |user|
      begin
        user.increment!(:ticket_count, 10)
      rescue => e
        Rails.logger.debug e.message
      end
    end
  end
end

⑤raiseとはなにか説明してください。
 (以下のコードを参考にしてもよい。)

lib/tasks/distribute_ticket.rake
namespace :distribute_ticket do
  desc "全ユーザーの中にticket_countが最大値のものを含んでいれば例外を発生させる"
  task raise: :environment do
    User.find_each do |user|
      begin
        if user.ticket_count > 2147483637
          raise RangeError, "#{user.id}は、チケット取得可能枚数の上限を超えてしまいます!"
        end
      rescue => e
        Rails.logger.debug e.message
      end
    end
  end
end

⑥トランザクションとは何かと、その利点を説明してください。
 (以下のコードを参考にしてもよい。)

lib/tasks/distribute_ticket.rake
namespace :distribute_ticket do
  desc "全ユーザーのticket_countをトランザクションで10増加させる"
  task transact: :environment do
    ActiveRecord::Base.transaction do
      User.find_each do |user|
        user.increment!(:ticket_count, 10)
      end
    end
  end
end

問題5 〜オブジェクト指向に触れよう〜

①単一責任の原則とはどんな決まりのことか説明してください。

②オブジェクト指向とはどんな考え方のことか説明してください。

③以下のコードに関する質問です。
 (1)ゲッターとセッターはそれぞれどんなメソッドですか。
 (2)以下のコードでセッターを使用し、山田を田中に更新し、ゲッターを使って、田中を出力してください。
(※適当にrubyファイルを作成し、実装してください。)

class Human
  #humanクラスのインスタンス変数は@name, @ageとする。
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def name=(set)
    @name = set
  end

end

human = Human.new('山田')

④以下のコードの流れを説明してください。

class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name
  end

  def fee
    @fee
  end
end

class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end
 def pay(user)
    puts "商品を選んでください"
    chosen_drink = user.choose_drink
    change = user.money - self.drinks[chosen_drink].fee
    if change >= 0
      puts "ご利用ありがとうございました!お釣りは#{change}円です。"
    else
      puts "投入金額が足りません"
    end
  end
end

class User
  def initialize(money)
    @money = money
  end

  def money
    @money
  end

  def choose_drink
    gets.to_i
  end
end

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks

puts "あなたはお客さんです。投入金額を決めてください。"
money = gets.to_i
user = User.new(money)

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

条件分岐を使ったプログラム

ペッパー君に20時から翌朝6時まで喋ってほしくないので、その時間中は「NG」、それ以外は「OK」と出力するメソットを作成します。
ペッパー君が喋る時をtrue,喋らない時をfalseとして、時刻も同時に入力します。

出力例は、

pepper_talk(true, 5)
=>NG
pepper_talk(true, 6)
=>OK
pepper_talk(false, 5)
=>OK
pepper_talk(false, 5)
=>OK

このようなイメージです。

まず、メソッドを作ります。

def pepper_talk(talking, hour)

end

出力例になるように処理を書いていきます。
喋ってはいけない時間を0時から6時までと、20時以降で分けて考えてあげると良いと思います。

def pepper_talk(talking, hour)
  if talking && (hour < 6 || hour >= 20 )
    puts "NG"
  else
    puts "OK"
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでレコード登録前に確認画面を表示する

はじめに

Ruby on Rails5 速習実践ガイドのアウトプットで投稿しています。
今回は、レコードの登録前に「こちらの内容で登録します」などの確認画面を表示する機能について投稿します!例として、タスク管理アプリケーションを作成しています。

目次

  1. アクションを作成する
  2. ルーティングの追加
  3. ビューの追加と編集
  4. 戻るボタンの実装
  5. 参考文献

アクションを作成する

まずは、確認画面に遷移するアクションをcontrollerに作成します。

app/controller/tasks_controller.rb
def confirm_new
  @task = current_user.tasks.new(task_params)
  render :new unless @task.valid?
end

ルーティングの追加

今回はタスクにネストさせるため以下のように設定します。

config/routes.rb
resources :tasks do
  post :confirm, action: :confirm_new, on: :new
end

この記述によって、/tasks/new/confirmというURLが生成されます。

ビューの追加と編集

追加

まず、確認画面のビューを作成します。

app/views/tasks/confirm_new.html.slim
h1 登録内容の確認

= form_with model: @task, local: true do |f|
  table.table.table-hover
    tbody
      tr
        th= Task.human_attribute_name(:name)
        td= @task.name
        = f.hidden_field :name
      tr
        th= Task.human_attribute_name(:description)
        td= simple_format(@task.description)
        = f.hidden_field :description
  = f.submit '戻る', name: 'back', class: 'btn btn-secondary mr-3'
  = f.submit '登録', class: 'btn btn-primary'

この画面を表示するタイミングでは、データは保存されていません。このあとにcreateメソッドを実行するときにデータが必要なので、hidden_fieldで前の画面のデータをユーザーからは見えないように保持して渡すようにしています。

編集

次に、新規追加画面の遷移先を確認画面に設定しておきます。

app/views/tasks/new.html.slim
= form_with model: @task, local: true, url: confirm_new_task_path do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task-name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description'
  = f.submit "確認", class: 'btn btn-primary'

戻るボタンの実装

確認画面の戻るボタンが押されたときの処理を実装していきます。先ほど作成した確認画面の戻るボタンには、name属性に「back」をつけています。このボタンを押したときにparams[:back]というパラメータが送られるので、このparamsがあれば、newメソッドにレンダーするという流れで実装します。

app/controllers/tasks_controller.rb
def create
    @task = current_user.tasks.new(task_params)
    if params[:back].present?
      render :new
      return
    end
    if @task.save
      redirect_to tasks_url
    else
      render :new
    end
  end

以上で、確認画面の実装は終了です!

参考文献

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

SystemSpec導入と書き方

はじめに

これまでに引き続き、現場で使える Ruby on Rails 5速習実践ガイドのアウトプットを投稿します!
今回はrspecについてです!
タスク管理アプリケーションを例に進めていきます。

目次

  1. Rspecの準備
  2. Specの書き方
  3. FactoryBotでテストデータを作成
  4. テストを書く
  5. 参考文献

Rspecの準備

・gemをインストール
Gemfileの
group :development, :test doのブロックに以下を追記します。

gem 'rspec-rails', '~> 3.7'

記述後bundle installでgemをインストールします。完了したら、以下のコマンドを実行し、RSpecに必要なディレクトリや設定ファイルを作成します。

rails g spec:install

・testディレクトリ削除
rails newでアプリケーションを立ち上げた時に自動で作成されるtestディレクトリを削除します。なぜなら、Rspecのファイルは、specディレクトリの中に作成するからです。

rm -r ./test

・Capybaraを使うためにspec_helper.rbを編集
Capybaraはrails newをした際にインストールされているため、機能の読み込み実行するドライバの設定を記述します。

spec/spec_helper.rb
require 'capybara/spec'
config.before(:each, type: :system) do
  driven_by :selenium_chrome_headless
end

・FactoryBotのインストール

Gemfileの
group :development, :test doのブロックに以下を追記します。

gem 'factory_bot_rails', '~> 4.11'

Specの書き方

ここでは、タスク管理アプリケーションの一覧表示に関するテストを例にします。

tasks_spec.rb
describe '一覧表示機能' do
  context 'Aさんがログインしているとき' do
    before do
      # テスト条件を満たすよう処理を記述する
    end
    it 'Aさんの投稿だけが表示される' do
      # 期待する動作を記述する
    end
  end
end

上記の例では条件が一つですが、複数ある場合はcontextをネストすることもできます。

FactoryBotでテストデータを作成

spec/factories/users.rbを作成し、Userモデルのデータを記述します。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'テストユーザー' }
    email { 'test@example.com' }
    password { 'password' }
  end
end

factory :userの記述で、railsがUserモデルのテストデータだなと、解釈してくれます。もし、違う名前をつけたいときは、以下のようにclassを明記します。

factory :test_user, class: User do

次にいま作成したユーザーに紐づく投稿データを作成します。先程と同様に、spec/factories/tasks.rbを作成します。

spec/factories/tasks.rb
FactoryBot.define do
  factory :task do
    name { 'テストを作成する' }
    description { '必要なものをインストールし、作成する。' }
    user
  end
end

上記のuserは、先ほど作成した:userのデータに紐づくものと定義しています。こちらも、モデル名と違うテストデータを紐付けるときはuserの箇所を以下のように書きます。

association :user, factory: :admin_user

テストを書く

まずは、日本語で枠組みを作成していきます。

spec/system/tasks_spec.rb
require 'rails_helper'
describe '一覧表示機能' do
  before do
    # ユーザーAを作成する
    # ユーザーAのタスクを作成する
  end
  context 'ユーザーAがログインしているとき' do
    before do
      # ユーザーAでログイン
      # ログイン画面に遷移
      # メールアドレスを入力
      # パスワードを入力
      # ログインボタンを押す
    end
    it 'ユーザーAが作成したタスクが表示される'
      # 作成されたタスクが表示されている
    end
  end
end

テストの枠組みができたら、実際にテストコードを書いていきます!

spec/system/tasks_spec.rb
require 'rails_helper'
describe '一覧表示機能' do
  before do
    # ユーザーAを作成する
    user_a = FactoryBot.create(:user)
    # ユーザーAのタスクを作成する
    FactoryBot.create(:task, name: "最初のタスク", user: user_a)
  end
  context 'ユーザーAがログインしているとき' do
    before do
      # ユーザーAでログイン
      # ログイン画面に遷移(ログイン画面のpathにvisit)
      visit login_path
      # メールアドレスを入力(labelの名称を指定します)
      fill_in 'メールアドレス', with: 'a@example.com'
      # パスワードを入力(labelの名称を指定します)
      fill_in 'パスワード', with: 'password'
      # ログインボタンを押す
      click_botton 'ログインボタン'
    end
    it 'ユーザーAが作成したタスクが表示される'
      # 作成されたタスクが表示されている
      expect(page).to have_content '最初のタスク'
    end
  end
end

参考文献

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

Rails + GraphQLでAPI作成

各バージョン

ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6

GraphQL Ruby

公式ページ

RailsでGraphQLを扱う場合↑のgemを使ってAPIを実装していきます。

graphiql-rails

合わせて graphiql-rails gemを入れておくとブラウザ上で実装したGraphQLの
確認ができるIDEが使えるようになります :sparkles:
graphql-rubyのinstall時に graphiql-rails のgemをGemfileに追加してくれます

イメージ画像
graphiql-rails

:computer:環境構築


Gemfile
gem 'graphql'
gem 'graphiql-rails' # 今回は先に入れました

gemがインストールされたら rails generate graphql:install コマンドを実行し各ファイルを生成します。
生成されたファイルは以下の通り↓

$ rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_scalar.rb
      create  app/graphql/types/base_union.rb
      create  app/graphql/types/query_type.rb
add_root_type  query
      create  app/graphql/mutations
      create  app/graphql/mutations/.keep
      create  app/graphql/mutations/base_mutation.rb
      create  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/controllers/graphql_controller.rb
       route  post "/graphql", to: "graphql#execute"
     gemfile  graphiql-rails
       route  graphiql-rails

この時点での routes.rb は以下のようになっています。

Rails.application.routes.draw do

  # GraphQL
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
  end

  post '/graphql', to: 'graphql#execute'
end

:pencil: 実装


Query 作成

まずは各テーブルに対応するTypeを定義しないといけないので、
例として以下の users テーブルに対応する user_type を作成してみたいと思います。

create_table :users do |t|
  t.string :name, null: false
  t.string :email
  t.timestamps
end

以下コマンドを実行すると user_type が作成されます。
(指定する型は ID がGraphQLで定義されているid用の型です(実態はString)
また語尾に ! が付いているものはnullを許容しない型となり、! が付いてないものはnull許容になります。)

$ bundle exec rails g graphql:object User id:ID! name:String! email:String

【補足】既にDBにテーブルが存在している場合はよろしくやってくれるっぽいので

$ bundle exec rails g graphql:object User

↑これでも大丈夫でした :sparkles:

生成されたファイル graphql/type/user_type.rb は以下のようになっていました。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

既に生成されている graphql/type/query_type.rb に以下を追加します。

    field :users, [Types::UserType], null: false
    def users
      User.all
    end

http://localhost:3000/graphiql上で以下クエリを投げるとレスポンスが返ってくるかと思います。

{
  users {
    id
    name
    email
  }
}

Mutationsの作成

次にユーザーを作成するMutations CreateUser を作成してみたいと思います。

$ bundle exec rails g graphql:mutation CreateUser

graphql/mutations/create_user.rb が作成されるので、以下の様に修正します。

module Mutations
  class CreateUser < BaseMutation
    field :user, Types::UserType, null: true

    argument :name, String, required: true
    argument :email, String, required: false

    def resolve(**args)
      user = User.create!(args)
      {
        user: user
      }
    end
  end
end

既に生成されている graphql/types/mutation_type.rb に以下を追記します。

module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUser # 追記
  end
end

http://localhost:3000/graphiql上で以下を実行するとUserが作成されます。

mutation {
  createUser(
    input:{
      name: "user"
      email: "user@email.com"
    }
  ){
    user {
      id
      name 
      email
    }
  }
}

Association

  • 1:1の関連テーブルの場合

例として PostLabel と1:1で関連付されている場合

label_type.rb
module Types
  class LabelType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    ...
  end
end
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
  end
end

↑の様に labelLabelType として定義できます。
この場合の Query のイメージとしては

{
  posts {
    id
    label {
      id
      name
    }
  }
}

上記の様に labelLabelType として必要な値をQueryできます。

  • 1:Nの関連テーブルの場合

例として UserPost と1:Nの場合

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :label, LabelType, null: true
  end
end
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
  end
end

上記の様に posts[PostType] として定義でき、Queryとしては

{
  user(id: 1234) {
    id
    posts {
      id
      label {
        id
        name
      }
    }
  }
}

↑の様に呼び出す事ができます。

graphql-batch

↑の説明の様に 1:1や1:Nの関連テーブルのデータも取ってくる事ができますが
今のままだとDBへの問い合わせが大量に発生してしまう場合があります。
UserPost と1:Nの場合の例で Post が100件ある場合、それぞれ100回問い合わせが発生してしまいます。

そこで解決方法の一つである複数問い合わせをまとめやってくれる graphql-batch を導入してみます。

gem 'graphql-batch'

Gemをインストールしたら、loader を作成していきます。
loader は「複数問い合わせをまとめる」部分の実装になります。

graphql/loaders/record_loader.rb
module Loaders
  class RecordLoader < GraphQL::Batch::Loader
    def initialize(model)
      @model = model
    end

    def perform(ids)
      @model.where(id: ids).each { |record| fulfill(record.id, record) }
      ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
    end
  end
end

これを先程の PostLabel と1:1で関連付されている場合に適用すると

module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
    def label
      Loaders::RecordLoader.for(Label).load(object.label_id)
    end
  end
end

こんな感じで書けます。
UserPost と1:Nの場合には別途loaderを作成します。

graphql/loaders/association_loader.rb
module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end

※ loaderはgraphql-batchのリポジトリにサンプルがあるので、そちらを参考にして実装すると良さそうです

以下の様に書くと、まとめて問い合わせしてくれるようになります。

module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end
  end
end

スキーマファイルからドキュメント生成

最後に定義したスキーマファイルから良い感じのドキュメントを自動で生成するようにしてみたいと思います。

routes.rb にマウントできてデプロイ毎に自動でgraphdocが更新される
便利なgemを探していたらgraphdoc-rubyというgemがあったので試してみます。

Gemfile に以下を追加

gem 'graphdoc-ruby'

また、npmパッケージの@2fd/graphdocも必要なので
予めDockerイメージ内でインストールしておきます。(Docker使用してない場合はローカル環境にインストールすれば良いかと思います)

例)

RUN set -ex \
    && wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get update \
    && apt-get install -y \
                 ...
                 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g yarn \
    && npm install -g @2fd/graphdoc # インストールしとく

config/routes.rb に以下を追記

config/routes.rb
Rails.application.routes.draw do
  mount GraphdocRuby::Application, at: 'graphdoc'
end

※ エンドポイントを変更している場合、config/initializers/graphdoc.rb を修正する

例)

GraphdocRuby.configure do |config|
  config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end

Railsを再起動して、http://localhost:3000/graphdoc でドキュメントが生成されればOKです :sparkles:

graphdoc

:bomb: バッドノウハウ


  • http://localhost:3000/graphiql アクセス時に以下エラーが発生する場合

    Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
    
  • graphiqlの画面にTypeError: Cannot read property 'types' of undefined が表示される
    -> 手元の環境だとRails再起動で治りました

  • graphiqlの画面にSyntaxError: Unexpected token < in JSON at position 0 が表示される
    -> エラーが発生してる可能性がるのでログを見て修正する

:link: 参考になったURL


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

姓と名を別々に入力させて、保存する前に結合する

忘備録です。
Deviseでの新規登録
名前のフォームを姓と名に分けて入力させ、保存する前に結合させる

registrations/new.html.erb
  <%= f.label :firstName, "姓" %>
  <%= f.text_field :firstName, autofocus: true, required: true, class: 'form-control' %>
  <%= f.label :lastName, "名" %>
  <%= f.text_field :lastName, autofocus: true, required: true, class: 'form-control' %>
controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  # strong parameterで姓と名の属性(firstNameとlastName)をpermitする
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:firstName, :lastName])
  end
end
models/user.rb
  # 姓と名をDBに保存する前に結合
  before_create :create_name
  def create_name
    self.name = "#{firstName} #{lastName}"
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】ブロック

ブロック

ブロック付きのメソッド呼び出し

主な用途は次の3つ。

  1. ループの抽象化
  2. ブロックへの機能付加。典型的にはリソース管理。
  3. コールバック関数・イベントハンドラ

クロージャーとしてのブロック

ブロックはクロージャである。
つまり、ブロックの中のコードに現れる自由変数はブロックの外部環境に従う。ブロックにおける自由変数は、外部環境であるブロックの外側のコンテキストで解決される。

下記の例では、ローカル変数 count がブロックの外と中で共有されている。

str = 'Hello, World'
count = 0
str.each_line do |line|
  print line
  count += 1
end
print count # => 1

ローカル変数や self 、 self に紐づいているインスタンス変数、メソッド呼び出しなどは全てブロックの中でも外側と同じように利用できる。

def some_method
  3.times { p self } # self は 3 ではなく、some_method の self
end

環境の保存

ブロックが参照している外部環境は、ブロックが存在する限り保存されている。メソッド実行は終了しても、内部のコードブロックはメソッド実行時のローカル変数を利用する。

下記の例では、 create_counter はメソッド内部のコードブロックを Proc オブジェクトに変換して呼び出し側に返す。
Proc#call メソッドを呼ぶとコードブロックを実行する。

create_counter メソッドの実行コンテキストにおけるローカル変数 count は、 メソッドが返した Proc 以外からは参照できない。
つまり、内部状態を隠蔽することができる。

def create_counter
  count = 1
  return Proc.new do
    count += 1
    p count
  end
end
counter = create_counter
p counter.class # => Proc
counter.call # => 2
counter.call # => 3

counter2 = create_counter
counter2.call # => 2
counter.call # => 4
counter2.call # => 3

ブロックパラメータ

メソッドに渡したブロックは、メソッドから呼び返されるときに引数を受け取ることができる。また、ブロックは独自のローカル変数 = ブロックパラメータを持つことができる。

a = "str"
[1, 2, 3].each { |a| p a } # この a は、上の a とは別物
p a # => "str"

ここまでにわかることから、注意しなければならない2点。

  • 外部のローカル変数と同名のブロックパラメータを用いない
  • 外部で既出のローカル変数は、ブロック内外で共有される(クロージャー)

ブロック付きのメソッドの定義

yield 式

def foo_bar_baz
  yield "foo"
  yield "bar"
  yield "baz"
end

# または

def foo_bar_baz
  # ブロックを与えられていないとき、enum_for で Enumerator を生成して返す
  return enum_for(:foo_bar_baz) unless block_given?
  %w[ foo bar baz ].each do |item|
    yield item
  end
end

foo_bar_baz do |item|
  puts item
end
# => foo
#    bar
#    baz

map もどき

def my_map
  [yield(1), yield(2), yield(3)]
end
p my_map { |i| i + 1 }
# => [2, 3, 4]

Proc

呼び出し側のブロックをオブジェクトとして取得したい場合、Procオブジェクトを使う。

下記の例における &handler を、ブロック引数と呼ぶ。

class SleepyPerson
  def register_handler(&handler)
    @event_handler = handler
  end
  def wake_up!
    @event_handler.call Time.now, "woke up"
  end
end
john = SleepyPerson.new
john.register_handler { |time, message| p [time, message] }
john.wake_up!

Proc からブロック

proc = Proc.new { puts "Proc was called" }
3.times(&proc)
# => Proc was Called
#    Proc was Called
#    Proc was Called

まとめ

とりあえずこれだけ大事そう

  • 外部で既出のローカル変数は、ブロック内外で共有される(クロージャー)
  • & 修飾で ブロック <-> Procオブジェクト

参照

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