- 投稿日:2021-01-05T23:37:58+09:00
【初学者必見】Railsで「複数のカラムからの」検索機能を実装してみた。
はじめに
あけましておめでとうございます?
Railsで定番の検索機能。
検索機能に関しての記事はQiita上でもたくさんあるが、どれも一つのテーブルの中の一つのカラムから検索する方法ばかり。(あとは、もう古くなったform_tagが使われていたり)
でも実際は一つのテーブルの中の複数のカラムから検索したいケースもあるはず。
例えば、db/schema.rbActiveRecord::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上記のagentsテーブルは「氏」と「名」と「氏のフリガナ」と「名のフリガナ」が独立しているので、agentを特定する際に一つのカラムから検索することはできません。
ましてやこの場合は一つの検索フォームから会社名(company_name)だったり会社の所在地(company_location)からagentを特定できればもっと便利になるはずです。順序
- Viewに検索フォームを配置する
- searchアクションのルーテインングを設定する
- 検索するためのメソッド(searchメソッド)をモデルに定義する ←ココ重要
- searchアクションをコントローラーに定義する
- 検索結果画面の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を使っています。
↓以下のように検索ボックスが作れました!
2. searchアクションのルーテインングを設定する
searchアクションはRailsのデフォルトである7つのアクション(index, show, new, create, edit, update, destroy)以外なので”search”と言う名前でアクションを定義します。
自分で定義したアクションにはmemberまたはcollectionというオプションを使う必要があります。
詳細は ココで確認してみてください。
今回は:idを関連付ける必要が無いので、collectionを使います。config/routes.rbRails.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 end3. 検索するためのメソッド(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.rbclass 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 endif文を用いて、検索結果が一つも無い場合は全てのagentを出力するようにしました。
4. searchアクションをコントローラーに定義する
以下のようにsearchアクションをコントローラーに追加します。
app/models/agent.rbdef 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を使っています。
↓以下のような検索結果画面ができました。
これは検索ボックスに「木」と入力して出た検索結果です。
最後に
自分がRailsで検索機能を実装していてあったら良いなと思うものを書いてみました。
いかがでしたでしょうか。
間違っている点やわかりにくい点があったらお気軽に教えてください!
では次回の投稿でお会いしましょう。
- 投稿日:2021-01-05T21:02:46+09:00
【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参考文献
- 投稿日:2021-01-05T19:45:03+09:00
Herokuにデプロイしたのにうまく反映できない時
- 投稿日:2021-01-05T14:43:43+09:00
【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.rbRails.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#destroyresourcesメソッドのオプション
ここからは、
resources
メソッドのオプションをめもしておきます。only
7つのアクションのうち、特定のアクションのみを指定したい時に使用できます。
routes.rbRails.application.routes.draw do #createアクションとnewアクションのみ resources :users, only:[:create, :new] endルーティングを見てみると、こんな感じになっています。
POST /users(.:format) users#create new_user GET /users/new(.:format) users#newmemeber
member
メソッドは、7つのアクション以外のアクションを追加することができます。
まずは、定義方法について、以下記載します。routes.rbRails.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#destroycollection
collection
メソッドもmember
メソッドと同様に7つのアクション以外のアクションを追加することができます。
member
メソッドと違う点は、collection
メソッドはリソース全体に対するアクションを定義するという点です。定義方法も
member
メソッドと同様です。routes.rbRails.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.2Rails tutorial 第14章 ユーザーをフォローする
https://railstutorial.jp/chapters/following_users?version=6.0#cha-following_users【Rails】resourcesメソッドを使ってルーティングを定義しよう!
https://pikawaka.com/rails/resources
- 投稿日:2021-01-05T14:04:49+09:00
デプロイ前にファイルの容量を確認
- 投稿日:2021-01-05T10:25:13+09:00
【応用】課題図書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.rakenamespace :greeting do desc "挨拶" task hello: :environment do puts "こんにちは" end task goodbey: :environment do puts "さようなら" end end④応用課題図書のexception_sampleというアプリケーションにおいて、以下のタスクの処理の流れをbeginとrescueに触れながら説明してください。
(※カリキュラムでアプリケーションの仕様の確認をすることは可能です。)lib/tasks/distribute_ticket.rakenamespace :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.rakenamespace :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.rakenamespace :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)
- 投稿日:2021-01-05T09:01:46+09:00
条件分岐を使ったプログラム
ペッパー君に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
- 投稿日:2021-01-05T08:39:44+09:00
railsでレコード登録前に確認画面を表示する
はじめに
Ruby on Rails5 速習実践ガイドのアウトプットで投稿しています。
今回は、レコードの登録前に「こちらの内容で登録します」などの確認画面を表示する機能について投稿します!例として、タスク管理アプリケーションを作成しています。目次
アクションを作成する
まずは、確認画面に遷移するアクションをcontrollerに作成します。
app/controller/tasks_controller.rbdef confirm_new @task = current_user.tasks.new(task_params) render :new unless @task.valid? endルーティングの追加
今回はタスクにネストさせるため以下のように設定します。
config/routes.rbresources :tasks do post :confirm, action: :confirm_new, on: :new endこの記述によって、
/tasks/new/confirm
というURLが生成されます。ビューの追加と編集
追加
まず、確認画面のビューを作成します。
app/views/tasks/confirm_new.html.slimh1 登録内容の確認 = 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.rbdef 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以上で、確認画面の実装は終了です!
参考文献
- 投稿日:2021-01-05T07:36:32+09:00
SystemSpec導入と書き方
はじめに
これまでに引き続き、現場で使える Ruby on Rails 5速習実践ガイドのアウトプットを投稿します!
今回はrspecについてです!
タスク管理アプリケーションを例に進めていきます。目次
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.rbrequire '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.rbdescribe '一覧表示機能' do context 'Aさんがログインしているとき' do before do # テスト条件を満たすよう処理を記述する end it 'Aさんの投稿だけが表示される' do # 期待する動作を記述する end end end上記の例では条件が一つですが、複数ある場合はcontextをネストすることもできます。
FactoryBotでテストデータを作成
spec/factories/users.rb
を作成し、Userモデルのデータを記述します。spec/factories/users.rbFactoryBot.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.rbFactoryBot.define do factory :task do name { 'テストを作成する' } description { '必要なものをインストールし、作成する。' } user end end上記の
user
は、先ほど作成した:user
のデータに紐づくものと定義しています。こちらも、モデル名と違うテストデータを紐付けるときはuser
の箇所を以下のように書きます。association :user, factory: :admin_userテストを書く
まずは、日本語で枠組みを作成していきます。
spec/system/tasks_spec.rbrequire '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.rbrequire '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参考文献
- 投稿日:2021-01-05T05:31:08+09:00
Rails + GraphQLでAPI作成
各バージョン
ruby: 2.7.1 rails: 6.0.3.4 graphql-ruby: 1.11.6GraphQL Ruby
RailsでGraphQLを扱う場合↑のgemを使ってAPIを実装していきます。
graphiql-rails
合わせて graphiql-rails gemを入れておくとブラウザ上で実装したGraphQLの
確認ができるIDEが使えるようになります
※graphql-ruby
のinstall時にgraphiql-rails
のgemをGemfileに追加してくれます環境構築
Gemfilegem '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実装
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↑これでも大丈夫でした
生成されたファイル
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の関連テーブルの場合
例として
Post
がLabel
と1:1で関連付されている場合label_type.rbmodule Types class LabelType < Types::BaseObject field :id, ID, null: false field :name, String, null: false ... end endmodule Types class PostType < Types::BaseObject field :label, LabelType, null: true end end↑の様に
label
をLabelType
として定義できます。
この場合の Query のイメージとしては{ posts { id label { id name } } }上記の様に
label
をLabelType
として必要な値をQueryできます。
- 1:Nの関連テーブルの場合
例として
User
がPost
と1:Nの場合module Types class PostType < Types::BaseObject field :id, ID, null: false field :label, LabelType, null: true end endmodule 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への問い合わせが大量に発生してしまう場合があります。
User
がPost
と1:Nの場合の例でPost
が100件ある場合、それぞれ100回問い合わせが発生してしまいます。そこで解決方法の一つである複数問い合わせをまとめやってくれる graphql-batch を導入してみます。
gem 'graphql-batch'Gemをインストールしたら、
loader
を作成していきます。
loader
は「複数問い合わせをまとめる」部分の実装になります。graphql/loaders/record_loader.rbmodule 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これを先程の
Post
がLabel
と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こんな感じで書けます。
User
がPost
と1:Nの場合には別途loaderを作成します。graphql/loaders/association_loader.rbmodule 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.rbRails.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' endRailsを再起動して、http://localhost:3000/graphdoc でドキュメントが生成されればOKです
バッドノウハウ
http://localhost:3000/graphiql
アクセス時に以下エラーが発生する場合Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
解決方法1
app/assets/config/manifest.js
に以下を追加する//= link graphiql/rails/application.css //= link graphiql/rails/application.jsAssetNotPrecompiled error with Sprockets 4.0 · Issue #75 · rmosolgo/graphiql-rails
-> ただこれだとProduction時にSprockets::FileNotFound: couldn't find file 'graphiql/rails/application.css'
エラーが出て使えない...解決方法2 (うまくいった方法)
gem 'sprocket'のバージョン3.7.2に下げる
gem 'sprockets', '~> 3.7.2' [#1098: slowdev/knowledge/ios/FirebaseをCarthageで追加する](/posts/1098)↑を追加し、
bundle update
Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む) - Qiitagraphiqlの画面に
TypeError: Cannot read property 'types' of undefined
が表示される
-> 手元の環境だとRails再起動で治りましたgraphiqlの画面に
SyntaxError: Unexpected token < in JSON at position 0
が表示される
-> エラーが発生してる可能性がるのでログを見て修正する参考になったURL
- 【Rails】graphql-rubyでAPIを作成 - Qiita
- REST APIが主流のプロジェクトの中でGraphQLを導入してみた話(サーバーサイド編) - Sansan Builders Blog
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ - エンジニアHub|若手Webエンジニアのキャリアを考える!
- GraphQLを使ったAPI仕様中心開発の導入とその効果の紹介 - Kaizen Platform 開発者ブログ
- 雑に始める GraphQL Ruby【class-based API】 - Qiita
- hawksnowlog: Ruby (Sinatra) で GraphQL 入門
- 既存のRailsプロジェクトにGraphQL APIを追加してみた - Qiita
- Ruby on Rails で sprockets から Webpacker へ移行し、移行できないものは共存させる方法 - Qiita
- Reading: 初めてGraphQL - 型の基礎|tkhm|note
- https://github.com/loopstudio/rails-graphql-api-boilerplate
- https://github.com/rmosolgo/graphql-ruby-demo
- 投稿日:2021-01-05T02:43:44+09:00
姓と名を別々に入力させて、保存する前に結合する
忘備録です。
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.rbclass 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 endmodels/user.rb# 姓と名をDBに保存する前に結合 before_create :create_name def create_name self.name = "#{firstName} #{lastName}" end
- 投稿日:2021-01-05T00:42:23+09:00
【Ruby】ブロック
ブロック
ブロック付きのメソッド呼び出し
主な用途は次の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 # bazmap もどき
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オブジェクト
参照