- 投稿日:2020-09-28T23:29:24+09:00
【Ruby】リファクタリングに強くなるための基本のキ
恥ずかしい話ですが、つい最近まで「綺麗なコードを書け」と言われると、条件反射的に「エラーがなけりゃいいじゃん。」と思っていました・・・。
しかし、リファクタリングという言葉を理解しながら、いくつかの記法やメソッドに触れていくうちに、美しいコードを美しいと思えるようになってきた(すこし大袈裟)ので、基本的なリファクタリングの事例を紹介したいと思います。今回設定するインプットとアウトプット
1から5までの連続する配列の合計を出力せよ。(答えは15)
これをいかに綺麗に記述して導き出すかに挑戦してみたいと思います。その1:ザ・ごり押し
p 1 + 2 + 3 + 4 + 5…プログラムというか電卓?レベルの一番シンプルな記述。当然、1~100までの数字の合計を求める場合等に対応できないのでボツ。
拡張性を持たせていきたい。その2:配列とeach文を使う
numbers = [1,2,3,4,5] sum = 0 #変数sumに0を代入 numbers.each do |n| sum += n #変数sumにブロック変数nを繰り返し足しこむ end p sumeach文で一番最初に習うコードかもしれない。プログラミングだけんども、「配列の要素を1,2,3…と書くのが面倒」、「each文まわりが数行にわたっててコンパクトにしたい」という気持ちが湧いてくる。
その3:範囲オブジェクトと{}のブロック記法でシンプルに
numbers = (1..5) #範囲オブジェクトの作成。1~5までの値が連続する配列を意味する。 sum = 0 numbers.each {|n| sum += n } #{}のブロック記法で1行に収める。 p sum(最初の値..最後の値)で、値の範囲を表すことができる。
※範囲オブジェクトは、Rangeクラスのオブジェクト。
※ちなみに、(最初の値...最後の値)と書くと、最後の値を含まない。ドット.の数に注意
また、each文のdo・・・endまでの間を、{}で代わりに囲むこともできる。
※do・・・endまでを「ブロック」と呼び、取り出した要素を扱う「作業部屋」のような役割。
だいぶスッキリしました。けど、もう一息!その4:eachの代わりにinjectメソッドを使う
numbers = (1..5) p sum = numbers.inject(0) {|n, s| n + s} #injectメソッドは下記の流れで動く。 #メソッドの第1引数が、ブロックの第1引数に入る。 #ブロックの第2引数には、配列の各要素が順番に入る。 #ブロックの戻り値が、ブロックの第1引数に引き継がれる。 #繰り返し処理が終わると、ブロックの戻り値がinjectメソッドの戻り値となる。injectメソッドを使うことで、引数を複数扱うことができ、2行に収めることができた。
今後は、良著と聞いた「リーダブルコード」も読み進めながら、胸を張って見せられる綺麗なコードを書けるよう頑張っていきます。
「こういう風に書いた方がもっと簡潔だよ。」という意見等ありましたら、ぜひ教えてください!
- 投稿日:2020-09-28T23:27:42+09:00
【Rails】ビューファイルは使用できない、パラメータIDも存在しない。そんな中でアソシエーションのデータを取得する。【APIモードで使用するやつ】
はじめに
備忘録です。
ここでは、ホーム画面("/")で
「データ全件」
「1つのデータに関連付けされているデータの総数」
「1つのデータに関連付けされているハッシュのデータ」
を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、
「記事全件」
「1つの記事に関連付けされているコメントの総数」
「1つの記事に関連付けされているタグのハッシュデータ」
を取得していくようなイメージです。
全てのデータはハッシュとして取得します。
環境
Ruby: 2.6
Rails: 5.2モデルの例(作成の過程は省略)
コード
1.まずは記事全件を取得する
def index @all_article = Article.all end2.1つの記事に関連付けされているコメントの総数を取得する
def index @all_article = Article.all # [ { 1: "コメント数" }, { 2: "コメント数" }, ... ]という形のデータを作成する。 # article.commentsでループ処理した記事データからコメントを呼び出している。 @article_count = @all_article.map{ |article| [article.id, article.comments.count.to_s] #もしコメント数を表示させるならto_sで文字列化する。 }.to_h # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する @article_count.map do |key, value| @all_article.map do |article| if article.id === key article["count"] = value end end end # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる end3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)
def index # 記事全件 @all_article = Article.all # コメント数 @article_count = @all_article.map{ |article| [article.id, article.comments.count.to_s] }.to_h @article_count.map do |key, value| @all_article.map do |article| if article.id === key article["count"] = value end end end # 1つの記事が所有するタグをハッシュで取得 @all_article.map{ |article| [article.id, article.article_tags.all] }.to_h # => { # "1": [ # { "id": 1, "tag_name": "Rails" }, # { "id": 2, "tag_name": "React" }, # ... , # ], # "2": [ # { "id": 1, "tag_name": "JavaScript" }, # { "id": 2, "tag_name": "Qiita" }, # ... , # ] # } end以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。
最後に
バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?♂️
- 投稿日:2020-09-28T23:27:42+09:00
【Rails】ビューファイルは使わず、パラメータIDも存在しない中で、アソシエーションのデータを取得する。【APIモードで使う】
はじめに
備忘録です。
ここでは、ホーム画面("/")で
「データ全件」
「1つのデータに関連付けされているデータの総数」
「1つのデータに関連付けされているハッシュのデータ」
を表示させたいとして、indexアクションにて上記のデータを取得する方法について見ていきます。
例えばQiitaのようなアプリケーションを作成するとするならば、
「記事全件」
「1つの記事に関連付けされているコメントの総数」
「1つの記事に関連付けされているタグのハッシュデータ」
を取得していくようなイメージです。
全てのデータはハッシュとして取得します。
環境
Ruby: 2.6
Rails: 5.2モデルの例(作成の過程は省略)
コード
1.まずは記事全件を取得する
def index @all_article = Article.all end2.1つの記事に関連付けされているコメントの総数を取得する
def index @all_article = Article.all # [ { 1: "コメント数" }, { 2: "コメント数" }, ... ]という形のデータを作成する。 # article.commentsでループ処理した記事データからコメントを呼び出している。 @article_count = @all_article.map{ |article| [article.id, article.comments.count.to_s] #もしコメント数を表示させるならto_sで文字列化する。 }.to_h # 上記のコメントデータのidと記事のidが一致した場合に、コメント数をcount属性に格納する @article_count.map do |key, value| @all_article.map do |article| if article.id === key article["count"] = value end end end # => [ { "id": 1, "count": "コメント数" }, { "id": 2, "コメント数" }, ... ]のように得られる end3.1つの記事に関連付けされているタグのハッシュデータを取得する(上と大体一緒)
def index # 記事全件 @all_article = Article.all # コメント数 @article_count = @all_article.map{ |article| [article.id, article.comments.count.to_s] }.to_h @article_count.map do |key, value| @all_article.map do |article| if article.id === key article["count"] = value end end end # 1つの記事が所有するタグをハッシュで取得 @all_article.map{ |article| [article.id, article.article_tags.all] }.to_h # => { # "1": [ # { "id": 1, "tag_name": "Rails" }, # { "id": 2, "tag_name": "React" }, # ... , # ], # "2": [ # { "id": 1, "tag_name": "JavaScript" }, # { "id": 2, "tag_name": "Qiita" }, # ... , # ] # } end以上ですが、上のままだとコントローラがごちゃごちゃしてしまうので、モデルファイルにインスタンスメソッドとして抽出するなどすると良いかと思います。
最後に
バックエンドとフロントエンドを切り離してSPAアプリケーションを作成する際にはビューファイルは使わないので、ルートのURLを持つホーム画面でアソシエーションされているデータを取得する方法を見つけるのにとても苦労しました。
で、結果このようにする結論に辿り着いたのですが、間違いやもっといい方法があるなどあれば、是非教えて頂けるととても助かります!?♂️
- 投稿日:2020-09-28T23:20:44+09:00
【No.003】発注者の注文一覧画面を作る
概要
発注者の管理画面をつくる
ToDoリスト
- Order::OrderingOrgSidesController追加
- routes.rbをいい感じに
- Orderモデル追加
- Orderのレコードを作成する内容をseedに追加
- Slimの導入
- TailWindの導入
- 見た目をFigmaに近づける
ToDo詳細
Order::OrderingOrgSidesController追加
ターミナルで下記をたたく。
bin/rails g controller order::ordering_sidesapp/controllers/orders/ordering_org_sides_controller.rbを、下記に修正。
module Orders class OrderingOrgSidesController < ApplicationController end endroutes.rbをいい感じに
config/routes.rbを以下のように。
Rails.application.routes.draw do root 'orders/ordering_org_sides#index' # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html namespace :orders do resources :ordering_org_sides, only: %i[index] end endOrderモデル追加
図のOrderモデルを追加するために、ターミナルで下記をたたく
bin/rails g model Order trade_no:string title:string postal:string address:string name:string phone:string color_size:string status:integerOrderのレコードを作成する内容をseedに追加
db/seeds.rbに以下追加。
orders = Order.create( [ {trade_no: '59466918', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1}, {trade_no: '56654093', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2}, {trade_no: '46263602', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3}, {trade_no: '76537895', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4}, {trade_no: '56939175', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1}, {trade_no: '83265169', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2}, {trade_no: '68545632', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3}, {trade_no: '86154160', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4}, {trade_no: '73779350', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1}, {trade_no: '16022030', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 2}, {trade_no: '48758961', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 3}, {trade_no: '94813841', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 4}, {trade_no: '79330602', title: '◯◯◯◯◯◯◯◯◯◯', postal: 'XXX-XXXX', address: '◯◯県◯◯市◯◯区◯丁目◯◯-◯◯', name: 'YYYYYY', phone: 'XXX-XXXX-XXXX', color_size: '△△△', quantity: 1, status: 1}, ] )ターミナルで以下を叩く。
rails db:seedSlimの導入
一部、erbファイルを生成済みだったため、下記を参考に対応した。
https://qiita.com/rinkun/items/391ab7e8e63a7f20339c
Gemfileに下記を追加。gem 'slim-rails' gem 'html2slim'ターミナルで、下記を打つ。
bundle exec erb2slim app/views app/views -dTailwindの導入
1.Install Tailwind via npm
# Using npm npm install tailwindcss # Using Yarn yarn add tailwindcss2.Add Tailwind to your CSS
app/javascript/src/scss/application.scssを追加
@import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities";app/javascript/packs/application.jsに下記追加
import '../src/scss/application.scss'3.Create your Tailwind config file (optional)
npx tailwindcss init4.Process your CSS with Tailwind
postcss.config.jsに下記追加
module.exports = { plugins: [ // ... require('tailwindcss'), require('autoprefixer'), // ... ] }見た目をFigmaに近づける
目標物
下図の見た目に近づける。
https://www.figma.com/proto/k7tWzvsQYtRHSwyi877OyV/import_agent_app?node-id=2%3A0&scaling=min-zoom参考
受入基準確認
準備
bin/rails db:migrate bin/rails db:reset受入基準
- 投稿日:2020-09-28T22:40:25+09:00
中二男子が一度は言ったことがある「無限ループ」
無限ループとは
eachメソッドやwhile文で、繰り返し処理をしたときに、終わりがなく、永遠に処理を繰り返している状態。もちろん、パソコンへの負荷がかかる。
具体例
number = 0 while number >= 0 puts number number += 1 endnumberに1をたし続けて、出力していく。
while文
while 条件式 #条件式が真であるときに繰り返す処理 endif文のように、条件式が真でないと、処理は実行されない。trueと条件式に入れれば、確実に繰り返される。
無限ループを止める方法
breakを使う。
例えば、number = 0 while number >= 0 if number == 100 break end puts number number += 1 endif文を用いて、numberが100になったら、処理を終了させる。つまり、最後の出力は99になる。
ポイント
- while文は条件式が真のときのみ、処理を行う。
- 無限ルートはbreakで止められる。
最後に
無限ループになってしまう状況は中々ないのかなぁと思う。
- 投稿日:2020-09-28T21:34:50+09:00
[rails6.0.0]ウィザード形式でActiveStorageを使用して画像を保存する方法
概要
ユーザー登録とプロフィールをさせる時に、ウィザード形式でフォームを作成。
プロフィールに画像を保存したかったが上手くいかなかったため備忘録として記載。
※検索しても同じ状況の方がいなかったので役に立てばと思います。ウィザード形式とは何か?という方はこちらを参考にしてください
内容
開発環境
MacOS Catalina 10.15.6
Rails 6.0.0
Ruby 2.6.5テーブル構成
deviseを使ってユーザー登録をさせようとしています。
userテーブルはnicknameのみ追加。
profileテーブルにはtwitterのリンクなど記載。
profileテーブルにactive_strageで画像を保存したい。ウィザード形式を実装
ここらへんの機能の実装に関しては
こちらの記事がかなり近いので参考にしました。問題点
ウィザード形式で、画像以外はしっかり保存できたけど、画像は保存されない。
registrations_controllerclass Users::RegistrationsController < Devise::RegistrationsController def create @user = User.new(sign_up_params) unless @user.valid? render :new and return end session["devise.regist_data"] = {user: @user.attributes} session["devise.regist_data"][:user]["password"] = params[:user][:password] @profile = @user.build_profile render :new_profile end def create_profile @user = User.new(session["devise.regist_data"]["user"]) @profile = Profile.new(profile_params) unless @profile.valid? render :new_profile end @user.build_profile(@profile.attributes) @user.save session["devise.regist_data"]["user"].clear sign_in(:user, @user) redirect_to root_path end private def profile_params params.require(:profile).permit(:avatar, :favorite_beer, :twitter_link, :info) endこの中の以下の記述が悪さをしていた様子。
registrations_controller@user.build_profile(@profile.attributes) @user.saveここでは、@userと@profileをbuildで関連付けさせて@user.saveでまとめて保存をしているのですが、ここでの保存が原因で上手く行ってない様子。
この記事を見つけて原因
ActiveStorageに実際にファイルが保存されるタイミングはmodelをsaveして処理がコミットされた時なので、保存される前のmodelから画像をattachしてもファイルが未完成の状態になるっぽいです。どうもActiveStrageはモデルをちゃんとsaveしないと行けなさそうなので以下のように記述を変更したら解決しました。
registrations_controller# それぞれのモデルで保存させた # @user.build_profile(@profile.attributes) @user.save @profile.user_id = @user.id @profile.saveまとめ
正直buildを使っての保存の仕組みがよくわかってなかったのに使用してしまったのがエラーの原因かもしれないです。
少しでも参考になれば幸いです。
- 投稿日:2020-09-28T21:30:24+09:00
[Ruby]sprintfで四桁のランダムな番号生成
- 投稿日:2020-09-28T21:23:36+09:00
Markdown記法について
今回がQiita初投稿になります。よろしくおねがいします。
「プログラミング知識をMarkdown記法で書いて共有しよう」
と、Qiitaの投稿画面の一番初めにもデフォルトである通りQiitaにしろREADMEにしろGitHubのプルリクエストにしろプログラミング業界ではMarkdown記法というのがちょいちょい出てくるんですね。
自分も初めの頃は参考のサイトを見て少しやろうと思いましたが、案外やり方が多くて面倒になり別にまだ覚えなくてもいいかと思っていたのですが最近になり少しずつやり方が分かってきたので備忘録的な意味と自分の練習もかねて投稿しようと思いました。まず初めにMarkdownについてですが
Markdownとは
Markdown(マークダウン)は文章を記述するための記法(マークアップ言語)の1つです。
Markdownとは、メールを記述する時のように書きやすくて読みやすいプレーンテキストをある程度見栄えのするHTML文書へ変換出来るフォーマットとしてジョン・グルーバーによって開発されました。
以下の特徴があります。
- 簡単で覚えやすい記述
- 文章の構造を明示出来る
- Markdownそのままでも理解出来る
- 対応アプリを使うことでより快適に読み書き出来る
- 拡張子は「.md」
とのことです。
例として以下にRailsのアプリケーションのREADMEの一例を載せます。
README
users テーブル
Column Type Options user_name string null: false string null: false password string null: false Association
- has_many :friends
- has_many :comments
というようにアプリケーションにはREADMEが必須でありその中の記述はMarkdown記法のためMarkdown記法を最低限は覚える必要があるということですね。
では自分がよく使うものを抜粋して紹介します。
「#」を先頭に入れて半角スペース1つ分空けると見出しの意味になり大文字になり強調されます。(h1タグと同じ意味)
「#」が一番大きくて以下1つずつ増やすごとに大文字がだんだんと小さくなり「######」最大6つまで連続で記述出来ます。「-」を先頭に入れて半角スペース1つ分空けると「・」が付与されます。
「---」と「-」を3つ以上連続で入れると水平線が出来ます。
と上記が自分がよく使用している記述でした。上記の記述方法は本当にほんの一部なので興味のある人はちゃんとしたサイトから学習頂ければと思います。(笑)
と言ってもQiitaにいる人達はみんなMarkdown記法で記事を投稿しているのだから恐らくQiitaに投稿した人の中で自分が一番Markdown記法を知らない(使いこなしていない)と思いますのでこれから精進して参りたいと思います。ここまでお付き合い頂きありがとうございました。注:Markdown記法の「#」や「-」などは全て半角で記述しないと正しく反映されません。さらには半角スペースも1つ分空けてください。全角では反映されませんので注意!
今回参考にさせて頂いたサイト
- 投稿日:2020-09-28T20:14:50+09:00
データベースのカラムなどを修正したい場合
db:migrateしたデータベースを修正したい場合
プログラミングスクールでのチーム開発学習中にデータベースの修正を行いたかったのですが、
細かい部分の知識が抜け落ちていたので、備忘録用にまとめてます。データベースの操作に慣れていない方や、これからデータベースを学習する方の参考にもなれば幸いです。
開発環境
DB: MySQL
Rails: 5.2.4.3まず自分が修正したいデータベース(テーブル)の確認を行いましょう
ターミナル% rails db:migrate:statusすると、テーブルがこの様に出てくると思います(出てくるテーブルの数や名前はそれぞれ違ってきます)
ターミナルStatus Migration ID Migration Name -------------------------------------------------- up 20200823051138 Devise create ----s up 20200824122031 Create -------s up 20200824122659 Add ancestry to ------s up 20200824123715 Create -----s up 20200829083145 Create -----s up 20200906141656 Create -----s 今回はこのテーブルを修正したい up 20200907114227 Create -----s down 20200927061950 Create -----s down 20200927065357 Create -----s ※----は自分で作成したテーブル名ここでupとdownに注目です。
マイグレーションの修正を行うには、statusをdownの状態にしておく必要があります。
次に自分が修正したいデータベース(テーブル)をdownにしましょう
downの状態にするにはターミナルでこの様なコマンドを実行しましょう
ターミナル% rails db:rollbackもう一度statusを確認してみましょう
ターミナル% rails db:migrate:statusターミナルStatus Migration ID Migration Name -------------------------------------------------- up 20200823051138 Devise create ----s up 20200824122031 Create -------s up 20200824122659 Add ancestry to ------s up 20200824123715 Create -----s up 20200829083145 Create -----s up 20200906141656 Create -----s 今回はこのテーブルを修正したい down 20200907114227 Create -----s down 20200927061950 Create -----s down 20200927065357 Create -----s ※----は自分で作成したテーブル名あれ?
一つ下しかdownに変わってません。
というのもrollbackコマンドは一つずつしかdownに変えられないのです。
なので、もう一度やってみましょう。
ターミナル% rails db:rollbackもう一度statusを確認してみましょう
ターミナル% rails db:migrate:statusターミナルStatus Migration ID Migration Name -------------------------------------------------- up 20200823051138 Devise create ----s up 20200824122031 Create -------s up 20200824122659 Add ancestry to ------s up 20200824123715 Create -----s up 20200829083145 Create -----s down 20200906141656 Create -----s 今回はこのテーブルを修正したい down 20200907114227 Create -----s down 20200927061950 Create -----s down 20200927065357 Create -----s ※----は自分で作成したテーブル名今度は無事に目的のテーブルをdownに出来ました。
修正が終わった後
今回はカラム名の修正を行いたかったので、この後にマイグレーションファイルのカラム名の変更を行いました。
最後に
ターミナル% rails db:migrateもう一度statusを確認しておきましょう
ターミナル% rails db:migrate:statusターミナルStatus Migration ID Migration Name -------------------------------------------------- up 20200823051138 Devise create ----s up 20200824122031 Create -------s up 20200824122659 Add ancestry to ------s up 20200824123715 Create -----s up 20200829083145 Create -----s up 20200906141656 Create -----s 修正したテーブル up 20200907114227 Create -----s up 20200927061950 Create -----s up 20200927065357 Create -----s ※----は自分で作成したテーブル名rails db:migrateコマンドの場合は、downのテーブルを全てupに変更します。
db:migrateは1度で全てupにするけど
db:rollbackは1つずつしかdownに出来ないんですね。一度にrollbackをまとめて行いたい場合
今回の様に複数回rollbackを行わないといけない場合にまとめて行える方法も紹介します
ターミナル% rails db:rollback STEP=2※STEP=2を入力する事でrollbackを2回分まとめて実行してくれます。
rollbackコマンドに慣れてきたら、STEPオプションも積極的に使って、作業性をあげていきましょう。
- 投稿日:2020-09-28T20:14:41+09:00
form_objectで親子関係のあるフォームを作成する(テストも書いてます)
背景
@shop
に紐づく@comment
、@employee
など、様々な子要素があるテーブルがあります。初回情報登録時には、@shop
、@comment
、@employee
など、親要素と一緒に全ての子要素も一緒に保存できるようにしていたのですが、下記の記事のような形で、
accepts_nested_attributes_for
を用いてこれらを実現していたものの、▼こんなふうに実現していました
fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]そのうち、
@shop
だけの編集フォームや、@comment
、@employee
などの投稿・編集フォームも必要になってきたため、だんだんmodel
が様々な記述で肥大化してきました。form_objectとは?
↑上記のような状態の時に、特定のフォームに関するバリデーションやデフォルト値の設定などを一箇所に集め、モデルの記述を簡素化できるのが、
form_object
です。個人的には、導入にかなりつまづいてしまったので、記事を書いて記録を残しておこうと思います。なお、実行環境は下記の通りです。
- Rails 5.2.4.2
- rspec-rails 4.0.1
導入方法
form_object
,controller
,view
の基本の書き方は下記の通りです。なお、今回は@shop
の初回登録時に@comment
も1件登録できるようなフォームを例にしたいと思います。実装にあたって、一番参考にさせていただいたのは、こちらの記事です。
accepts_nested_attributes_forを使わず、複数の子レコードを保存する
DB構造
shops name string category integer ↑ categoryはショップ種別。
enum
のカラム。
comments content text shop_id integer 作成したファイル
form_object
/forms/shop_entry_form.rbclass ShopEntryForm include ActiveModel::Model # @shopに関する記述 ----------------------------- concerning :ShopBuilder do def initialize(params = {}) super(params) @category = params[:category] end def facility @shop ||= Shop.new end end attr_accessor :name, :category validates :name, presence: true validates :category, presence: true # @commentに関する記述 ----------------------------- concerning :CommentBuilder do attr_reader :comments_attributes def comments @comments_attributes ||= Comment.new end def comments_attributes=(attributes) @comments_attributes = Comment.new(attributes) end end attr_accessor :content # 実装のロジック ------------------------------------ def save # バリデーションエラーならfalseを返して以下の処理は行わない return false if invalid? shop.assign_attributes(shop_params) build_asscociation shop.save ? true : false end private def shop_params { name: name, category: @category, } end def build_asscociations # shopの子要素にcommentを追加する。ただし、中身が空なら追加しない。 shop.comments << comments if comments[:content].present? end endこれだけでつまづきどころがかなりありました。。。。
まず、concerning :ShopBuilder do ... end
の部分ですが、以下のような意味を持ちます。# この記述は... concern :ShopBuilder do ... end # 下記と同じ module ShopBuilder extend ActiveSupport::Concern ... end詳しくは、実装にあたって参考にした、こちらの記事をご覧ください。
次に、
initialize(params = {}) ... end
の部分なのですが、以下のような意味を持ちます。def initialize(params = {}) # @shopのparamsにアクセスできるようにする super(params) # DBでデフォルト値が設定されているカラム用の記述 @category = params[:category] endまず、
super(params)
については、こちらも実装にあたって大変参考にさせていただいた記事である以下の記事によると
super(params)
でパラメーターを格納する記述で、以下の記述と同じ意味を持ちます。@attributes = self.class._default_attributes.deep_dup assign_attributes(params)また、db側でデフォルト値が設定されているカラムは、以下のように明示的にparamsにアクセスすることを書かないとparamsにアクセスできず、値を入力してもDBのデフォルト値になってしまいました...。
@category = params[:category]この謎は解けず。今後の課題としたいです。。。
enumを使ったカラムにdb側でデフォルト値が必要な理由は、こちらの記事をご覧ください。そして
def comments_attributes=(attributes) ... end
の部分なのですが、def comments_attributes=(attributes) @comments_attributes = Comment.new(attributes) endこちらはRailsばかりやっているとなかなか目にしない、
セッターメソッド
という書き方で、=でおわるメソッド(引数)
の形で、引数によって@のつく要素を変更することができます。
個人的には、こんなことをやっているイメージに近いのではないかなと思いました。def comments_attributes=(attributes) # ... 以下略 # こんなイメージ comments_attributes = attributes # なので、こんな感じに呼び出せる self.comments_attributes # => attributesの中身Rubyのゲッターとセッターを正しく理解していなかったせいですね。。。。トホホ。。。頑張ります。。。
なお、=でおわるメソッド
については、『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』のp215
を15回ぐらい読み直しました。controller
次は、コントローラーの記述です。コントローラーはこのような形になりました。
app/controllers/shops_controller.rbclass ShopsController < ApplicationController def new @shop = ShopEntryForm.new end def create @shop = ShopEntryForm.new(shop_entry_params) if @shop.save # 成功したときの処理 else # 失敗したときの処理 end end private def shop_entry_params params.require(:shop_entry_form).permit(:caregory, :name, comments_attributes: [:content]) end endこちらは、意外に記述が減らなかった印象があります。当初
shop_entry_params
がcontrollerから減ってくれればいいなーと期待したものの、結局controllerからは消せず。アソシエーションを作るメソッドだけはcontrollerから削除することができました。なお、Modelに関しては、バリデーションとデフォルト値設定のメソッド、アソシエーションなども全て消すことができました!増えた記述は、なし!!やはり、
form_object
はモデルをスリム化するために便利な書き方なのですね!!View
最後に、Viewはこのようになっています。
app/views/shops/new.html.haml= form_with model: @shop, url: shops_path, local: true do |f| = f.text_field :name = f.fields_for :shop_comments, local: true do |comment_form| = comment_form.text_field = f.submit "送信"
fields_for
を使うあたりは、accept_nested_attributes_for
を使った実装と変わらないのですね^^テスト
テストも至ってシンプルでした!
spec/forms/shop_entry_form_spec.rbrequire 'rails_helper' RSpec.describe ShopEntryForm, type: :model do before do @shop_form = ShopEntryForm.new(category: "category1", name: "テストのお店") end describe "バリデーションのテスト" do it "名前とカテゴリーがあればバリデーションを通過すること" do @shop_form.valid? expect(@shop_form).to be_valid end # 以下略 end endファイルの置き場所と、
RSpec.describe ShopEntryForm ...
の部分, テスト用のインスタンス生成時の記述に注意すれば良いだけでした^^これは、少し古いのですがこちらの記事を参考に作成しました。
感想・参考資料など
さて、、、、本当に長い時間が実装にかかりました。実際のフォームはネストした子要素が3種類もあったり、形もかなり複雑だったのもあるのですが、何よりも素のRubyの書き方に慣れていなかったのが大きかったと思います。。。落ち着いたら、またRubyを復習したいです。
今回、参考にした記事や資料まとめです。
▼全体的な書き方
accepts_nested_attributes_forを使わず、複数の子レコードを保存する▼paramsへのアクセス方法
フォームクラスを使う
『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』(p.215)▼Concerningについて
Bite-sized separation of concerns▼テストの書き方
フォームオブジェクトのテストをRSpecで書く
この後、editとupdateのフォームも残っているので、次はそちらを取り組みたいです^^
- 投稿日:2020-09-28T19:13:57+09:00
「もっとRust脳を鍛える数学パズル」の試み。
「もっとプログラマ脳を鍛える数学パズル」をRustで書き直すのは、ボケ防止にちょうど良いかもしれない、と思った。
P.13 例題1 : メモ化と動的計画法
元のRubyコード(p.015)。
pre1_2.rbM, N = 10, 100 @memo = {} def check(remain, pre) return @memo[[remain, pre]] if @memo[[remain,pre]] return 0 if remain < 0 return 1 if remain == 0 cnt = 0 pre.upto(M) do |i| cnt += check(remain - i, i) end @memo[[remain, pre]] = cnt end puts check(N, 2)Rustベタ移植。
main.rsuse std::collections::HashMap; fn main() { let mut memo: HashMap<(i64, i64), i64> = HashMap::new(); println!("{}", check(&mut memo, 10, 100, 2)); } fn check( memo: &mut HashMap<(i64, i64), i64>, max_seat_at_one_table: i64, remain: i64, pre: i64, ) -> i64 { match memo.get(&(remain, pre)) { Some(cnt) => return *cnt, _ => { if remain < 0 { return 0; } else if remain == 0 { return 1; } else { let mut count = 0; for i in pre..=max_seat_at_one_table { count += check(memo, max_seat_at_one_table, remain - i, i); } memo.insert((remain, pre), count); return count; } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_check() { let max_seat_at_one_table = 10; let number_of_people = 100; let mut memo: HashMap<(i64, i64), i64> = HashMap::new(); assert_eq!( check(&mut memo, max_seat_at_one_table, number_of_people, 2), 437_420 ); } }Rubyの
upto
と同様の動きをRustのfor
にやらせるには、+1しないといけない。
と思ってたら @scivola さんからご指摘。ありがとうございます!Rubyのショートコードでは何とも思わないグローバル変数だが、さすがにRustでやると気持ち悪い、というかやり方が分からないので、持ち回っている。Amazonの書評にもあったけど、変数を数学っぽく省略すると訳が分からなくなるので、ひどいところだけ、何となく長い名前にした。
改めて、条件分岐の処理にRubyの気楽さ(=プログラマへの信頼)を感じる。Rustはコンパクトながら抜けを許さないところが、これはこれで良い感じ。
2020/09/29追記
グローバル変数の扱いの指摘を受けて再度実装。グローバル変数を
struct
として実現する案。これはもうデザインパターンに片足突っ込んでますね。main.rsuse std::collections::HashMap; struct Checker { memo: HashMap<(i64, i64), i64>, max_seat_at_one_table: i64, } impl Checker { pub fn check(&mut self, remain: i64, pre: i64) -> i64 { match &self.memo.get(&(remain, pre)) { Some(cnt) => return **cnt, _ => { if remain < 0 { return 0; } else if remain == 0 { return 1; } else { let mut count = 0; for i in pre..=self.max_seat_at_one_table { count += self.check(remain - i, i); } &self.memo.insert((remain, pre), count); return count; } } } } } fn main() { let mut chk = Checker { memo: HashMap::new(), max_seat_at_one_table: 10, }; println!("{}", chk.check(100, 2)); } #[cfg(test)] mod tests { use super::*; #[test] fn test_check() { let number_of_people = 100; let mut chk = Checker { memo: HashMap::new(), max_seat_at_one_table: 10, }; assert_eq!(chk.check(number_of_people, 2), 437_420); } }
- 投稿日:2020-09-28T18:46:17+09:00
【Rails】 binding.pryの活用方法
この記事では、binding.pryの使い方を解説しています。
binding.pryを活用することで、
・一次ソースに触れる機会が増え、学習効率が上がる
・binding.pryを複数設置して、paramsの流れが理解しやすくなるなど、たくさんメリットがあります。
自分みたいな、初学者方の参考になればと思い、記事にしてみました。前提
チャットアプリを題材にbinding,pryの使い方を学びます。
(注意:この記事ではチャットアプリは完成しません!チャットアプリ作成の記事ではありません)開発環境
・ruby 2.6.5
・Rails 6.0.3.3完成イメージ
ER図
必要なテーブル
・usersテーブル
・roomsテーブル
・entriesテーブル (中間テーブルです!)流れ
①user, room, entryモデル、テーブルを作成(下準備)
②アソシエーションを書く(下準備)
③Gemfileにpry-railsを追加してbundle installを実行(下準備)
④ビューにform_withを用意
⑤roomsコントローラーにcreateアクションを書く
①user, room, entryモデル、テーブルを作成(下準備)
userモデルのマイグレーションを編集
userモデルは、deviseを使って作成しているものとします!db/migrate/xxxx_devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :nickname, null: false t.string :email, null: false, default: "" #省略 end #省略 end endusersテーブルには、nicknameとemailのカラムを用意しています。
roomモデルのマイグレーションを編集
db/migrate/xxxx_create_rooms.rbclass CreateRooms < ActiveRecord::Migration[6.0] def change create_table :rooms do |t| t.string :name, null: false t.timestamps end end endroomsテーブルには、nameのカラムを用意しています。
entryモデルのマイグレーションを編集(下準備)
db/migrate/xxxx_create_entries.rbclass CreateEntries < ActiveRecord::Migration[6.0] def change create_table :entries do |t| t.references :room, foreign_key: true t.references :user, foreign_key: true t.timestamps end end endentriesテーブルは、usersテーブルとroomsテーブルを繋ぐ中間テーブルなので、
user, roomそれぞれを外部キーとして、references型で保存するようにしています。②アソシエーションを書く(下準備)
一人のuserは複数のroomに入れて、
一つのroomは複数人のuserが入るので、
usersテーブルとroomsテーブルは「多対多」の関係になります。
以下のように、アソシエーションを記述します。userモデル
app/models/user.rbclass User < ApplicationRecord #省略 has_many :entries has_many :rooms, through: :entries endroomモデル
app/models/room.rbclass Room < ApplicationRecord has_many :entries has_many :users, through: :entries endentryモデル
app/models/entry.rbclass Entry < ApplicationRecord belongs_to :room belongs_to :user end③gem 'pry-rails' をインストール(下準備)
pry-railsをインストールできるようGemfileに記述して、bundle installを実行します。
参考 : rweng/pry-rails: Rails >= 3 pry initializer - GitHubGemfilegem 'pry-rails'ターミナル% bundle install④ビューにform_withを用意
新しくルームを作成するために、roomsコントローラーにnewアクションを定義します。
app/controllers/rooms_controller.rbclass RoomsController < ApplicationController def new @room = Room.new #newメソッドでインスタンスを作成 @users = User.all #全ユーザーのレコードを取得 end end
ビューには、フォームを設置します。
チャットルーム名(name)を入力し、チャットしたい相手(user_ids)を選べるようにします。app/views/rooms/new.html.erb<%= form_with model: @room, local: true do |f| %> <%= f.label :チャットルーム名%> <%= f.text_field :name%> <%# 入力したチャットルーム名を取得 %> <label>チャットしたい相手</label> <select name="room[user_ids][]"> <%# 選択したユーザーを取得 %> <option value="">未選択</option> <% @users.each do |user| %> <option value=<%= user.id %>><%= user %></option> <% end %> </select> <%= f.submit %> <% end %>ここで、
user_ids
と複数形になっているのは、自分と相手の2人分保存するからです!
userモデルにて、has_many
を定義したことで、_ids
メソッドが使えるようになりました!
参考 : Active Record の関連付け - Railsガイド実際にフォームへ入力してみます。
すると下記のように、ユーザーが誰が誰だか分からないではありませんか!では、何が原因でこの出力が得られたのか推測します。
考えやすくするために、ユーザー選択の記述を、rubyの文法で書き直してみます。
@users.each do |user| user end
@users
は、roomsコントローラーのnewアクションで定義しているインスタンス変数で、
@users = User.all
と定義しています。
ビューの中では、each文による繰り返し処理によって、@users
から一人ずつ取り出しています。では、
binding.pry
を使って、出力される値を確認してみましょう。app/views/rooms/new.html.erb<%= form_with model: @room, local: true do |f| %> <%= f.label :チャットルーム名%> <%= f.text_field :name%> <%# 入力したチャットルーム名を取得 %> <label>チャットしたい相手</label> <select name="room[user_ids][]"> <%# 選択したユーザーを取得 %> <option value="">未選択</option> <% @users.each do |user| %> <% binding.pry %> <%# ?each文の中にbinding.pryを設置!! %> <option value=<%= user.id %>><%= user %></option> <% end %> </select> <%= f.submit %> <% end %>
ブラウザをリロードすると、ターミナルに以下のような出力が表示されます。ターミナル3: <%= f.text_field :name%> 4: <p><label>チャットしたい相手</label></p> 5: <select name="room[user_ids][]"> 6: <option value="">未選択</option> 7: <% @users.each do |user| %> => 8: <% binding.pry %> 9: <option value=<%=user.id%>><%= user %></option> 10: <% end %> 11: </select> 13: <p><%= f.submit%></p> [1] pry(#<#<Class:xxxx>>)>
=>
で、ビューの8行目で処理を止めてるよ!とターミナルが教えてくれています。
7〜10行間は、each文で繰り返し処理していることから、
繰り返し処理の1回目
で、一時的に処理を止めてくれています。したがって、
user
には一人目のデータ
が格納されていると考えられます。
では、実際にuser
の値を確認してみましょう。ターミナル[1] pry(#<#<Class:xxxx>>)> user => #<User id: 1, nickname: "user_1", email: "test@1"> [2] pry(#<#<Class:xxxx>>)> user.nickname => "user_1"
[1]pry>
の後に、式
を入力することで、
=>
後に、値
を出力してくれます。
user
には、idが1であるユーザーのレコード
が格納されていることが確認できました。
今回はユーザー名を一覧表示させたいので、nickname
の値だけを取り出すことにします。
2回目のpryで、user.nickname
と記述すると、ユーザー名を取り出せると確認できました。
したがって、ビューファイルを下記のように書き換えます。app/views/rooms/new.html.erb<%= form_with model: @room, local: true do |f| %> <%= f.label :チャットルーム名%> <%= f.text_field :name%> <%# 入力したチャットルーム名を取得 %> <label>チャットしたい相手</label> <select name="room[user_ids][]"> <%# 選択したユーザーを取得 %> <option value="">未選択</option> <% @users.each do |user| %> <option value=<%= user.id %>><%= user.nickname %></option> <%# ?user.nicknameを表示するように変更!! %> <% end %> </select> <%= f.submit %> <% end %>無事にユーザー名を一覧表示することができました!
しかし、この一覧表示には、一つだけ問題があります。
それは、自分自身も表示されている
ことです。
このままでは、自分しかいない孤独なチャットルームが作成されてしまいます...。
このような事態を防ぐために、自分以外のユーザーを一覧表示
するようにします。どんな式が必要か、
binding.pry
を活用して探していきます。ターミナル3: <%= f.text_field :name%> 4: <p><label>チャットしたい相手</label></p> 5: <select name="room[user_ids][]"> 6: <option value="">未選択</option> 7: <% @users.each do |user| %> => 8: <% binding.pry %> 9: <option value=<%= user.id %>><%= user.nickname %></option> 10: <% end %> 11: </select> 12: <p><%= f.submit%></p> [1] pry(#<#<Class:xxxx>>)>まず初めに、
@users
の中身を確認しましょう。ターミナル[1] pry(#<#<Class:xxxx>>)> @users => [#<User id: 1, nickname: "user_1", email: "test@1">, #<User id: 2, nickname: "user_2", email: "test@2">, #<User id: 3, nickname: "user_3", email: "test@3">, #<User id: 4, nickname: "user_4", email: "test@4">, #<User id: 5, nickname: "user_5", email: "test@5">] # ?現在のユーザー(current_user)
@users
には、全ユーザーのデータ
が、一人ずつ配列で格納されていることが確認できます。
ではそもそも、インスタンス変数@users
とは何と定義していたかというと、
User.all
と等しいよ!と定義していましたね。
では、User.all
の内容を確認しましょう。ターミナル[2] pry(#<#<Class:xxxx>>)> User.all => [#<User id: 1, nickname: "user_1", email: "test@1">, #<User id: 2, nickname: "user_2", email: "test@2">, #<User id: 3, nickname: "user_3", email: "test@3">, #<User id: 4, nickname: "user_4", email: "test@4">, #<User id: 5, nickname: "user_5", email: "test@5">]
@users
と全く同じデータが出力されることが確認できました。続いて、
現在のユーザーのデータ
を取り出せないか試してみます。ターミナル[3] pry(#<#<Class:xxxx>>)> User.all.where(id: current_user) => [#<User id: 5, nickname: "user_5", email: "test@5">]
.where
はActive Recordのメソッドの一つで、条件に該当するレコードを配列に格納して出力してくれます。
超便利なのでどんどん使っていきましょう。
参考 : Active Record の基礎 - Railsガイドさて、現在のユーザーのデータを取り出すことができました。
ということは、現在のユーザー以外のデータも取り出せるのでは?と思いつきます。
.where.not
メソッドを使うと良さそうです。
.where.not
は、条件に該当しないレコードを配列に格納して出力してくれる、.where
と対をなすメソッドです。ターミナル[4] pry(#<#<Class:xxxx>>)> User.all.where.not(id: current_user) => [#<User id: 1, nickname: "user_1", email: "test@1">, #<User id: 2, nickname: "user_2", email: "test@2">, #<User id: 3, nickname: "user_3", email: "test@3">, #<User id: 4, nickname: "user_4", email: "test@4">]現在のユーザーは
user_5
なので、現在のユーザー以外のデータ
が出力されています。
これで、現在のユーザー以外を一覧表示させる式を見つけることができました!
したがって、@users
の定義を変更しましょう。app/controllers/rooms_controller.rbclass RoomsController < ApplicationController def new @room = Room.new #newメソッドでインスタンスを作成 @users = User.all.where.not(id: current_user) #現在のユーザー以外のレコードを取得 end end
これで自分だけのチャットルームを作らないよう設定できました。
めでたしめでたし...と言いたいところですが、
roomを保存できるか確認してみましょう。⑤roomsコントローラーにcreateアクションを書く
roomsコントローラーにcreateアクションを定義します。
app/controllers/rooms_controller.rbclass RoomsController < ApplicationController def new @room = Room.new @users = User.all.where.not(id: current_user) end def create #createアクションを定義 binding.pry end endフォームで入力した情報(リクエスト)を確認したいので、
この段階では、createアクションには何も処理は定義せず、
binding.pryを設置しておきます。
こうすることで、
「フォームで入力された情報が届いたよー!」
と、ルーティングを介して、roomsコントローラーのcreateアクションを実行する瞬間に、
処理を止めることができます。
では、フォームにルーム名room1
、チャットしたい相手user_2
と選択して送信します。
すると下記のようにターミナル上で、createアクション内で定義したbinding.pry
で処理を止めてるよと教えてくれます。ターミナル7: def create => 8: binding.pry 9: end [1] pry(#<RoomsController>)>
では、リクエストのパラメータを確認してみましょう。ターミナル[1] pry(#<RoomsController>)> params => <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false>
params
(パラムス)はパラメーターズの略です。
authenticity_token
は、セキュリティのために生成されるトークンなので、今回は無視します。
room
の中に、form_withで入力したパラメータが、配列としてハッシュで管理されています。
このroom
とは、form_withで用意した、model: @room
と対応しています。では、paramsの中の、
room
の情報だけ見てみます。ターミナル[2] pry(#<RoomsController>)> params[:room] => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2"]} permitted: false>
params[:xxxx]
とすることで、見たいパラメータxxxx
だけを確認できます。
上記の結果から、チャットルームroom1
にuser_idが2
のユーザーが入ったことが確認できます。
ちゃんとルームに人を呼べていることが確認できましたね。
めでたしめでたし.....
って、自分自身がルームに入ってないじゃん!!!
ここから、自分もルームに入れるよう、ビューを書き換えきます!app/views/rooms/new.html.erb<%= form_with model: @room, local: true do |f| %> <%= f.label :チャットルーム名%> <%= f.text_field :name%> <%# 入力したチャットルーム名を取得 %> <label>チャットしたい相手</label> <select name="room[user_ids][]"> <%# 選択したユーザーを取得 %> <option value="">未選択</option> <% @users.each do |user| %> <option value=<%= user.id %>><%= user.nickname %></option> <% end %> </select> <input name="room[user_ids][]" type="hidden" value=<%=current_user.id%>> <%# ?現在のユーザーもroomに追加するように変更!! %> <%= f.submit %> <% end %>
input
は、formにおけるテキストフィールドの種類を指定します。
hidden
属性を指定することで、ブラウザには表示せずにパラメータとしてデータを受け渡すことができます。
この記述では、user_ids
にcurrent_user(現在のユーザー)
も含まれるよう記述しています!
ではもう一度、フォームにルーム名room1
、チャットしたい相手user_2
と選択して送信します。ターミナル[1] pry(#<RoomsController>)> params => <ActionController::Parameters {"authenticity_token"=>"xxxxxxx==", "room"=>{"name"=>"room1", "user_ids"=>["2", "5"]}, "commit"=>"Create Room", "controller"=>"rooms", "action"=>"create"} permitted: false> [2] pry(#<RoomsController>)> params[:room] => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false>無事に
現在のユーザー(user_5)
がuser_ids
に含まれていることが確認できました!
続いて、createアクションを定義し直して、テーブルにデータが保存できるようにしましょう。app/controllers/rooms_controller.rbclass RoomsController < ApplicationController #省略 def create @room = Room.new(room_strong_params) if @room.save redirect_to root_path else render :new end end private def room_strong_params params.require(:room).permit(:name, user_ids: []) end endストロングパラメータは、
roomモデル
の、name
とuser_ids
のパラメータだけ許可するとしています。
createアクションの内部で、どのようにパラメータの受け渡しがされているか、binding.pry
を使って確認しましょう。app/controllers/rooms_controller.rb#省略 def create @room = Room.new(room_strong_params) binding.pry # ?binding.pryを設置!! if @room.save binding.pry # ?binding.pryを設置!! redirect_to root_path else render :new end endインスタンス変数
@room
が、保存される前、後でパラメータをそれぞれ確認してみます。
では、フォームにルーム名room1
、チャットしたい相手user_2
と選択して送信します。ターミナル7: def create 8: @room = Room.new(room_strong_params) => 9: binding.pry 10: if @room.save 11: binding.pry 12: redirect_to root_path 13: else 14: render :new 15: end 16: end [1] pry(#<RoomsController>)>
@room
保存前の各パラメータを確認します。ターミナル[1] pry(#<RoomsController>)> params[:room] => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false> [2] pry(#<RoomsController>)> @room => #<Room:xxxx id: nil, name: "room1", created_at: nil, updated_at: nil> [3] pry(#<RoomsController>)> room_strong_params => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>2回目のpryに注目してください。
@room
は、id: nil
であることから、この時点では、レコードは作成されていないと分かります。
3回目のpryでは、ストロングパラメータを確認していますが、name
とuser_ids
に値が正しく格納されていることが確認できます。
リクエストしたパラメータが、コントローラーのcreateアクションに正しく受け渡されているのに、まだレコードが作成されていない理由は、
.new
メソッドでインスタンスを作成する場合、
.save
メソッドを実行して初めてデータベースにレコードとしてコミットされるからです。では、
@room.save
後を確認してみます。ターミナル7: def create 8: @room = Room.new(room_strong_params) 9: binding.pry 10: if @room.save => 11: binding.pry 12: redirect_to root_path 13: else 14: render :new 15: end 16: end [1] pry(#<RoomsController>)> params[:room] => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: false> [2] pry(#<RoomsController>)> @room => #<Room:xxxx id: 1, name: "room1"> [3] pry(#<RoomsController>)> room_strong_params => <ActionController::Parameters {"name"=>"room1", "user_ids"=>["2", "5"]} permitted: true>2回目のpryに注目してください。
@room
は、id: 1
であることから、レコードは正常に保存されました!今回は無事に保存できたのですが、
レコードを保存できなかった時に使える便利なメソッドも、合わせて紹介します!ターミナル[4] pry(#<RoomsController>)> @room.valid? => true [5] pry(#<RoomsController>)> @room.errors => #<ActiveModel::Errors:xxxx @base=#<Room:xxxx id: 1, name: "room1">, @details={}, @messages={}>[4] pryの
@room.valid?
では、「@room
のバリデーションはOK?」みたいな感じで、
バリデーションを実行してエラーがあるかを判別します。
エラーが無ければtrue
を,
エラーが有ればfalse
を返します。[5] pryの
@room.errors
では、@room.valid?でfalseが返された時に、エラーメッセージを出力してくれます。
今回はエラーはないので、エラーメッセージは出力されていません。
エラーがある時はmessages{}
の中にエラーメッセージが格納されます。最後に、保存されたデータをコンソールで確認してみましょう。
ターミナル% rails c [1] pry(main)> Room.all => [#<Room:xxxx id: 1, name: "room1">] [2] pry(main)> Entry.all => [#<Entry:xxxx id: 1, room_id: 1, user_id: 2>, #<Entry:xxxx id: 2, room_id: 1, user_id: 5>]コンソールでも、Active Recordのメソッドを使うことができます。
Room.all
で全てのルームを表示させると、
room1が保存されていることが確認できます。
Entry.all
で全てのレコードを表示させると、
2つのレコードが保存されていることが確認できます。
「ルーム1にユーザー2と5がいるよーっ!」と教えてくれています。コンソールで確認した内容は、以下の表と同じ内容です!
roomsテーブル
id name 1 room1 entriesテーブル
id room_id user_id 1 1 2 2 1 5 最後までお付き合いいただきありがとうございました!
参考資料
- 投稿日:2020-09-28T18:35:23+09:00
rails5.2.4 で作成した既存のアプリを5.1.6にダウングレードする
rails5.2.4で開発を進めていたのですが、既存のサービスとバージョンを合わせて管理がしたかったため、ダウングレードを選択しました。
環境はdockerで構築しているため、 ローカルの場合は使用しているPC等のrailsのバージョン変更が必要になります。手順
1.Gemfileのrailsのバージョンを書き換える
Gemfilegem "rails", "5.1.6"2.bundle update (dockerのサービス名はwebにしています)
$ docker-compose run web bundle update3.configの書き換え
application.rbのconfig.load_defaultsが5.2になっているので、5.1に変更
application.rbconfig.load_defaults 5.14.Active Recordの設定解除
application.jsのactive_storageの行を削除
application.js//=require active_storage
configのactive_storageの行を削除
production.rbconfig.active_storage.service = :localdevelopment.rbconfig.active_storage.service = :local5.secrets.ymlを作成
config配下にsecrets.ymlを作成し、
bundle exec rake secret
で鍵の作成をする。参考記事
https://qiita.com/tanishilove/items/2801059830e5af1262d76.最後にdevelopment.rbに残っているActive_storageの設定を削除
config.active_record.verbose_query_logsをfalseに変更
development.rbconfig.active_record.verbose_query_logs = false最後に
バージョンを下げる機会はなかなかないかと思いますが、困った時に参考になればと。
- 投稿日:2020-09-28T14:53:22+09:00
【Rails】検索フォームで、ひらがな・カタカナ・漢字の区別なく検索(精度は100%ではないよ)
自分がPFとして作っているアプリに検索フォームを実装しました。
ただ、【rails 検索 フォーム】 とかで検索すると、部分一致や完全一致が多く出てきます。
なんとかして漢字の部分をひらがなやカタカナで検索できないかな、と思いやってみました。流れとしては、動画のタイトルを保存する時に、そのタイトルをローマ字に変換して専用のカラムに保存し、検索するときも検索ワードをローマ字に変換して専用のカラムと参照する、という感じです。
最初は検索する時にタイトルを全部変換しようかと思いましたが、動画が増えると時間がかかりそうだな・・・と思ったので上記の方法にしました。如何せん初学者なので、そんな冗長なことしなくてもみたいな部分はあるのと思いますが、忘備録の意味合いも込めて書くので大目に見てやって下さい。
参考にしたサイト
- ひらがな-カタカナ-漢字-ローマ字を変換するgemつくったよ
- Web上のコンテンツや入力情報などが、英語か日本語か判別したいときのメモ
- Rubyの正規表現の使い方をマスターしよう!match/gsub
検索フォームを作る
検索フォーム自体は色々と記事があるので簡単に作れると思います。
自分の場合は動画の投稿サイトです。検索で、検索ワードが動画のタイトルに一致する、という検索フォームを作ります。/application.html.erb<div id="search-box"> <%= form_tag(search_path, :method => 'get') do %> <div class="input-tag"> <%= text_field_tag :search, '', placeholder: '検索', value: params[:title] %> </div> <div class="submit tag"> <%= button_tag type: 'submit', class: 'btn btn-default' do %> <i class="fas fa-search"></i> <% end %> </div> <% end %> </div>ルーティングを、videosコントローラーのサーチアクションに飛ばします。
routes.rb# 検索機能 get "search" => "videos#search"コントローラーに追記していく
gemを導入します。
今回、 miyabi というgemを使いました。ひらがな〜カタカナ〜ローマ字に変換したり判定したりできるgemです。
今回使ったメソッド.to_roman #文字列をローマ字に変換 .to_kanhira #漢字が含まれた文字列をひらがなに変換 .is_hira? #文字列がひらがなか判定 .is_kana? #文字列がカタカナか判定gemを導入したらコントローラーのcreateとsearchを書いていきます。
Videoというモデルには
user_id title introduction
のカラムがあり、そこにタイトルをローマ字に変換した物を保存する conversion_title というカラムを追加しました。schema.rbcreate_table "videos", force: :cascade do |t| t.integer "user_id" t.string "title" t.text "introduction" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "conversion_title" endvideos.controllerのcreateを作るのですが、問題が発生しました。
gem 'miyabi' では、【漢字が含まれている文字列を変換】はできるのですが、【漢字が文字列に含まれているか】は判定することができません。
rubyの持つ、正規表現で漢字が含まれているかどうかを判断します。
今回は漢字がタイトルに含まれているかどうかを調べたいので、ピンポイントで漢字だけを判定させます。@video.title.match(/[一-龠々]/)これで漢字が含まれているかどうか判定できます。
createで投稿された動画を保存します。
/videos_controller.rbdef create @video = Video.new(video_params) @video.user_id = current_user.id if @video.title.match(/[一-龠々]/) @video.conversion_title = @video.title.to_kanhira.to_roman elsif @video.title.is_hira? || @video.title.is_kana? @video.conversion_title = @video.title.to_roman else @video.conversion_title = @video.title end if @video.save redirect_to video_path(@video) else render :new end end private def video_params params.require(:video).permit(:title, :introduction, :video) end上から順に、まず、タイトルに漢字が含まれるか判定します。
含まれていれば、タイトルをひらがなに変換した後さらにローマ字に変換して保存します。漢字が含まれておらず、全てひらがな、カタカナの場合はローマ字に変換し保存します。
どちらにも当てはまらない場合は、ローマ字で投稿されていると判断してそのまま保存します。
searchアクションも同様に書いていきます。
/videos_controller.rbdef search word = params[:search] unless word.blank? if word.match(/[一-龠々]/) conversion_word = word.to_kanhira.to_roman elsif word.is_hira? || word.is_kana? conversion_word = word.to_roman else conversion_word = word end end @search_video = Video.search(conversion_word) endフォームで検索されたワードを、wordに代入して、wordが入っていればローマ字に変換します。
createと同様に、上から順番に条件にあった変換をします。
もし検索ワードが何も無しで検索された場合は動画を全て返しています。検索結果のviewはこんな感じ。
/videos/search.html.erb<h2>検索結果</h2> <% unless @search_video.blank? %> <div class="row"> <% @search_video.each do |video| %> ===== 省略 ===== <% end %> </div> <% else %> <p>検索結果はありません</p> <% end %> </div>検索したワードに一致するものがなければ、その旨を表示するようにしてあります。
これで一通りできました。完成!
実際にやってみます。
ひらがなで "うみがめ" と入力
"umigame" という conversion_title を持っている動画を返してくれました。
(海亀のタイトルを持っている動画がたくさんありますが、これは conversion_title を追加する前の動画です。ご愛敬。)ローマ字でも検索してみます。
表示されました。
その他
テストをしながら、ブラウザバック等が入るとパラメーターの動きが変わるのか、全ての動画が読まれたりというとが発生します。
多分キャッシュとかなんだろうな...JSも勉強しないとなぁ...と思うところであります。漢字に関しては、タイトルにもありますが100%完璧に変換してくれる訳ではないようです。
(実際、 "最強" という文字が "saikiu" と変換されていました)
ちょっとした検索を作りたい時などに利用できるかと思います。もっといい方法があれば、ぜひお願いします。
- 投稿日:2020-09-28T14:37:45+09:00
active recordで特定カラムを空で保存できるように修正
解決策
下記のようにallow_blankを追加したら解決できました
validates :something, allow_blank: true参考記事
Validate attribute only if it present (only if user fill in it)
- 投稿日:2020-09-28T13:40:15+09:00
【ActiveAdmin】コピペ新規作成するcloneアクションを追加する
はじめに
rails_adminの方ではclone機能をgemで追加して実装出来たのですが……
→ https://qiita.com/MATO/items/116bda1f3629ece0812cactive_adminの方ではgemではなく設定ファイルをいじって実装出来たのでメモです。
かなり汎用性あるコード書けたと思います。ソースコード
関係ない所もありますが、見返す用に全部載せておきます。
app/admin/items.rbActiveAdmin.register Item do permit_params :kind_id, :name, :memo # 一覧ページで検索フィルター要らないので消しておく config.filters = false # 一覧ページでのデフォルトソート config.sort_order = 'updated_at_desc' # 間違って削除しないように処理自体を消しておく actions :all, :except => [:destroy] # カスタムアクション、clone member_action :clone, method: :get do from_item = Item.find(params[:id]) @item = Item.new # ここでコピペしておきたいカラムをもう入れてしまう @item[:kind_id] = from_item.kind_id @item[:name] = from_item.name render :new, layout: false end # 詳細ページにもCloneボタンを追加 # indexページでエラーが出る、、、のでIF文入れておく action_item :only => :show do if params[:id].present? link_to "Clone Item", clone_admin_item_path(id: params[:id]) end end index do # selectable_column # id_column column :kind column :name column :memo actions defaults: false do |item| item 'View', admin_item_path(item), class: 'view_link member_link' item 'Edit', edit_admin_item_path(item), class: 'edit_link member_link' item 'Clone', clone_admin_item_path(id: item.id), class: 'clone_link member_link' end end form do |f| inputs do input :name input :kind, collection: Kind.all.order(:name) input :memo end actions end endこれで一覧ページでレコードごとにCloneリンクも追加されて、機能します。
/adminから/manageとかに名前を変更した場合でも、rails routes
で表示されるパスリンク?を使えばいけます。カスタムアクションとして
今回は「新規作成ページ」というnewと同じ処理する時にちょっとデータを入れただけ、ですね。
ただこれを元にしたら、そのレコードに対しての処理を好き放題にControllerに書くように書けたので、かなり楽しくカスタムできそうです。参考ページ
GitHubにドンピシャの質問あったのですが、色々と方法ありすぎたり、2013年とちと古かったり、英語だし……でちょっと違いますね。
https://github.com/activeadmin/activeadmin/issues/972俺のコードの方がキレイ?
終わり
- 投稿日:2020-09-28T12:48:13+09:00
【第2回】RSpecビギナーが、ビギナーなりにSystemSpecを書いてみた(沼編)
はじめに
こちらは前回の記事【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみたの第2回、SystemSpec(システムスペック)編です。
ModelSpec(モデルスペック)について知りたい方は前回の記事へどうぞ!また先日、RSpecの雄である伊藤淳一 @jnchito さんのご厚意で開催された初学者向けの勉強会(RSpecビギナーズ!!)にも参加致しましたので、こちらの動画も見て頂けるとより理解が深まると思うので、もしよろしければご覧下さい。
この記事で扱うこと
- SystemSpec(システムスペック)
モデル、コントローラ、ビュー、全部テストできるよ!
- システムスペックの具体的な記述例
自身のポートフォリオを参考にして記述していきます。今回扱うのは主に個人会員と法人会員の認証のテストです。記事の投稿、編集、DM、通知のテストは次回掲載予定です。この記事で扱わないこと
- ModelSpec(モデルスペック)
前回の記事をご覧下さい。- RSpecのセットアップ、準備
(後述する参考書籍『EverydayRails-RSpecによるRailsテスト入門』にて詳しく記載してあるのでそちらを参考にしてください)前提
- 対象
RSpec書こうとしてるけど何が何だかさっぱりなんじゃあ〜という初学者の方。 ただ、伊藤さんの使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 こちらの記事内容をある程度は見ていたり、なんかやったことはあるな〜とか、最低限describe,it,expectの役割が分かる方が望ましいです。参考コード
前述したように自身のポートフォリオを参考に記述するので、こちらにGitHubのリンクを貼っておきますが、テストの対象はサイトの基幹機能に絞っていますのでご了承ください。
【サイトの基幹機能】(個人・法人会員の新規登録/ログイン/編集、法人会員登録の申請、記事の投稿/編集、DM、通知など)テストを記述するための準備
RSpecによるテストを記述するためには、gem 'rspec-rails'をはじめ、いくつかgemを入れたり設定をする必要があります。
まずはテストを書く準備を整えてからお読みください。
必要なものは『EverydayRails-RSpecによるRailsテスト入門』に記載してあります。
というか、これを見ればこの記事を見なくても分かる人は分かると思います。
具体例としてコードを見たいという方はそのままお読みいただけると嬉しいです。(本当に参考程度ですが)システムスペックについて
そもそもシステムスペックって何やねん?って方もいるかと思います。
例えば皆さんがWebサイトを利用する場面を想像してください。
アカウント登録やログインをしたり、マイページを編集したり、何か投稿したり、誰かにDMを送ったり、、、挙げればきりがないんですが、実際のブラウザ上で何かしらのアクションを起こしますよね?
システムスペックは、この実際のブラウザ上でのユーザーのアクションが正しく動作するかということをテストします。
ユーザーが実際にWebサイトを見ながら動かすものが対象なので、そりゃ大事ですよね。
そのため前回記事の終わりに、モデルスペックを書くのも大事だけどシステムスペックを書くのが大事だと言及しました。またRSpecの記事を探していると、「フィーチャスペック」というものも出てきますが、これはシステムスペックの前身だと思ってください。
RSpec 3.7から、Rails 5.1以降のアプリケーションに対してシステムスペック(system spec)をテストスイートに追加できるようになりましたので、バージョンが上記以降のものを使っている方はシステムスペックを書いていきましょう。ちなみにですが、システムスペックを記述していくにもいろいろ設定が必要ですので、必要なgemや設定はEverydayRailsを参考にしてください。
users_spec.rbのテスト
①FactoryBotを使用し、userデータをあらかじめ用意しておく
前回、FactoryBotを使用し事前データを用意しました。システムスペックを書く際にも使用するので、参考までに以下に載せておきます。
FactoryBotの詳細は、前回の記事を参考にしてください。①spec/factories/users.rbFactoryBot.define do #FactoryBotを使用し、userデータをあらかじめ用意しておく factory :user do last_name { "テスト" } first_name { "太郎" } kana_last_name { "テスト" } kana_first_name { "タロウ" } email { "test@example.com" } postal_code { "1234567" } address { "東京都千代田区123-12-1" } phone_number { "12345678910" } password { "testtaro" } end end②具体的なコードの記述
それではテストコードを書いていきます。
$ rails g rspec:system users を実行すると spec/system フォルダ内に users_spec.rb が作成されます。
テストコード(実際のブラウザ上の動きなど)はこちらのファイルに記述していきます。
以下、完成例です。②spec/system/users_spec.rbrequire 'rails_helper' RSpec.describe 'User', type: :system do let(:user){FactoryBot.create(:user)} describe 'ユーザー認証のテスト' do describe 'ユーザー新規登録' do before do visit new_user_registration_path end context '新規登録画面に遷移' do it '新規登録に成功する' do fill_in 'user[last_name]', with: "テスト" fill_in 'user[first_name]', with: "太郎" fill_in 'user[kana_last_name]', with: "テスト" fill_in 'user[kana_first_name]', with: "タロウ" fill_in 'user[email]', with: "test@example.com" fill_in 'user[postal_code]', with: "1234567" fill_in 'user[address]', with: "東京都足立区123-12-1" fill_in 'user[phone_number]', with: "12345678910" fill_in 'user[password]', with: "testtaro" fill_in 'user[password_confirmation]', with: "testtaro" click_button '新規登録' expect(page).to have_content 'アカウント登録が完了しました。' end it '新規登録に失敗する' do fill_in 'user[last_name]', with: "" fill_in 'user[first_name]', with: "" fill_in 'user[kana_last_name]', with: "" fill_in 'user[kana_first_name]', with: "" fill_in 'user[email]', with: "" fill_in 'user[postal_code]', with: "" fill_in 'user[address]', with: "" fill_in 'user[phone_number]', with: "" fill_in 'user[password]', with: "" fill_in 'user[password_confirmation]', with: "" click_button '新規登録' expect(page).to have_content '個人会員 は保存されませんでした。' end end end describe 'ユーザーログイン' do before do visit new_user_session_path end context 'ログイン画面に遷移' do it 'ログインに成功する' do fill_in 'user[email]', with: user.email fill_in 'user[password]', with: user.password click_button 'ログイン' expect(page).to have_content 'ログインしました。' end it 'ログインに失敗する' do fill_in 'user[email]', with: '' fill_in 'user[password]', with: '' click_button 'ログイン' expect(current_path).to eq(new_user_session_path) end end end end describe 'ユーザーのテスト' do before do visit new_user_session_path fill_in 'user[email]', with: user.email fill_in 'user[password]', with: user.password click_button 'ログイン' end describe 'マイページのテスト' do it 'ヘッダーにマイページと表示される' do expect(page).to have_content('マイページ') end it 'マイページに遷移し編集リンクが表示される' do visit user_path(user) expect(page).to have_content('編集する') end end describe '編集のテスト' do context '編集画面へ遷移' do it '遷移ができる' do visit edit_user_path(user) expect(current_path).to eq('/users/' + user.id.to_s + '/edit') end end context '表示の確認と編集' do before do visit edit_user_path(user) end it '登録情報編集と表示される' do expect(page).to have_content('登録情報編集') end it '画像編集フォームが表示される' do expect(page).to have_field 'user[profile_image]' end it '名前編集フォームに自分の姓が表示される' do expect(page).to have_field 'user[last_name]', with: user.last_name end it '名前編集フォームに自分の名が表示される' do expect(page).to have_field 'user[first_name]', with: user.first_name end it '名前編集フォームに自分のカナ姓が表示される' do expect(page).to have_field 'user[kana_last_name]', with: user.kana_last_name end it '名前編集フォームに自分のカナ名が表示される' do expect(page).to have_field 'user[kana_first_name]', with: user.kana_first_name end it 'メールアドレス編集フォームに自分のメールアドレスが表示される' do expect(page).to have_field 'user[email]', with: user.email end it '郵便番号編集フォームに自分の郵便番号が表示される' do expect(page).to have_field 'user[postal_code]', with: user.postal_code end it '住所編集フォームに自分の住所が表示される' do expect(page).to have_field 'user[address]', with: user.address end it '電話番号編集フォームに自分の電話番号が表示される' do expect(page).to have_field 'user[phone_number]', with: user.phone_number end it '自己紹介文編集フォームに自分の自己紹介文が表示される' do expect(page).to have_field 'user[introduction]', with: user.introduction end it '編集に成功する' do # 名前を二郎に変更 fill_in 'user[first_name]', with: '二郎' fill_in 'user[kana_first_name]', with: 'ジロウ' click_button '変更を保存する' expect(page).to have_content '会員情報の更新が完了しました。' expect(page).to have_content 'テスト 二郎 (テスト ジロウ)' expect(current_path).to eq('/users/' + user.id.to_s) end it '編集に失敗する' do # first_name 名前(名)を空欄で入力 fill_in 'user[first_name]', with: '' click_button '変更を保存する' expect(page).to have_content '件のエラーが発生したため 個人会員 は保存されませんでした。' expect(page).to have_content '名前(名)を入力してください' expect(current_path).to eq('/users/' + user.id.to_s) end end end end end「お、なんか FactoryBot の書き方が前回と違うぞ let(:user) って何だ??」って思った方へ。
RSpecには let という機能があります。
これを使うとインスタンス変数をlet(:変数)という形に置き換え、後のコードで変数として使用することができます。
詳しくはこちらの記事を参考にしてください。RSpecのletを使うのはどんなときか?(翻訳)
beforeブロックで囲む場合と比べるとコード量も少なくなりますし、よりRSpecらしくなるので是非使ってみましょう。
ただ参考記事にも記述してある通り、letには「遅延評価される」という特徴があるので少し注意が必要です。こちらについては後のコード例で説明します。(私が沼にハマった原因です)
では、まずはユーザー認証のテストから説明していきます。
以下のコードをご覧下さい。②spec/system/users_spec.rblet(:user){FactoryBot.create(:user)} describe 'ユーザー認証のテスト' do describe 'ユーザー新規登録' do before do visit new_user_registration_path # 新規登録画面へ遷移 end context '新規登録画面に遷移' do it '新規登録に成功する' do # fill_in で登録情報をテキストボックスへ入力 fill_in 'user[last_name]', with: "テスト" fill_in 'user[first_name]', with: "太郎" fill_in 'user[kana_last_name]', with: "テスト" fill_in 'user[kana_first_name]', with: "タロウ" fill_in 'user[email]', with: "test@example.com" fill_in 'user[postal_code]', with: "1234567" fill_in 'user[address]', with: "東京都足立区123-12-1" fill_in 'user[phone_number]', with: "12345678910" fill_in 'user[password]', with: "testtaro" fill_in 'user[password_confirmation]', with: "testtaro" click_button '新規登録' # ボタンをクリック expect(page).to have_content 'アカウント登録が完了しました。' endまず、beforeブロック内で visit new_user_registration_path という記述があります。
この visit というものは Capybara の機能で、 visit + path で特定のページに移動できます。
Capybaraとは、ユーザーが実際にWebサイトを使用しているかのように、様々なページを遷移しその際にどこか不具合がないか調べてくれる便利な機能です(カピバラがいろんなページを走り回って調べてくれているところを想像するとほっこりしますね)。Capybaraを使うと visit以外にも様々な便利な機能が使えます。
Railsアプリケーションを作成した際に標準で gem 'capybara' がついているかと思いますが、確認してみてください。
参考記事:使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」
さて、 ここからは実際の画面を想像しながら新規登録に成功するテストコードを記述していきます。
visit new_user_registration_path
⇨新規登録画面へ遷移しました。fill_in 'user[last_name]', with: "テスト"
⇨テストという文字列を名前(姓)テキストボックスへ入力。以下の登録情報も同じく入力する。(fill_in 'user[カラム]', with: "入力値" という形になっています。)click_button '新規登録'
⇨ 新規登録ボタンを押下します。expect(page).to have_content 'アカウント登録が完了しました。'
⇨既に使われているアドレスやパスワードを入力しない限り、アカウント登録が成功するので アカウント登録が完了しました。というメッセージが画面上に表示されるはずです。
have_content はページ内に特定の文字列が表示されていることを確かめることができます。以降、have_◯◯という記述がたくさん出ますので、わからなければ都度上記の参考記事を確認してください。
はい、これで新規登録に成功するテストがシステムスペックで書けました!
$ bundle exec rspec spec/system/users_spec.rb をターミナルで実行してみましょう!
うまくできていればターミナルに 1 examples, 0 failures という結果が出力されます。
エラーが出た場合は、エラー文を読み、コードで間違っている箇所がないか、もしくは設定の時点で何か漏れがないかなど確認しましょう。
また、慣れてきたらexpect(page).not_to have_content 'アカウント登録が完了しました。'
のように、expect(page) の後をnot_toに変更してみて"テストがちゃんと失敗すること"を再確認することも大事です。間違ったテストコードを記述してしまっている場合、わざとnot_toにしてあげているのにテストが成功してしまうことがあります。これを防ぐために敢えてnot_toなどに変更し、テストが失敗することを確認してあげましょう。ログインやマイページ上での登録情報の編集なども、基本は同じなのでこちらを参考にしながら記述してみてください!
ちなみにclick_buttonは submitボタンやbuttonタグ の時に使います。
対してCSSによって見た目は同じように見えてもそれがリンクであるときはclick_linkを使います。
どちらでも使えるのが click_on になっています。じゃあ、全部click_onでよくね?と思ったそこのあなた。私も思いました データを送信するためのボタンと遷移のためののリンクを明示的に分けてあげるためとか? そんな感じなのかなと自分で考えたりしたのですが、分けていることで他に何かメリットがあるのでしょうか。詳しい方教えて欲しいです。companies_spec.rbのテスト
①FactoryBotを使用し、companyデータをあらかじめ用意しておく
前回作成したものです。
①spec/factories/companies.rbFactoryBot.define do factory :company do company_name { "テスト株式会社" } kana_company_name { "テストカブシキガイシャ" } email { "testcompany@example.com" } postal_code { "1234567" } address { "東京都千代田区123-12-1" } phone_number { "12345678910" } password { "testcompany" } approved { true } is_active { true } end end②具体的なコードの記述
法人の新規登録は個人会員の新規登録とは異なり、以下の流れになっています。
- 法人が新規登録のためにフォームを入力し、申請するボタンをクリックする。
- トップ画面へリダイレクトされ、「承認済メールが届くまで今しばらくお待ちください。」というメッセージが表示される。同時に管理者へ申請通知が送られる。
- 承認済メールが届くまでは法人ログインが制限される。
- 管理者が申請通知を確認し、申請ステータスを承認済に更新する。同時に法人の登録済アドレスへ承認済メールが送信される。
- ログインが可能になる。
ベーコンレタスエッグつくねライスバーガーか!
取り乱しました。ややこしすぎる物事に対するくりぃむしちゅー上田さんのツッコミが出てしまいました。基本は今までと同じなので慣れたらどうってことないのでしょうが、初学者にとっては難しく感じ苦労しました。
またこのような仕組みの参考記事が見つからなかったのが、今回の記事を書くきっかけにもなりました。(探し方が下手くそなだけかもしれませんが)
もし似たような仕組みを作っている初学者の方がいれば、こちらが参考になればと思います。以下完成コード例です。
②spec/system/companies_spec.rbrequire 'rails_helper' RSpec.describe "Companies", type: :system do let!(:admin){FactoryBot.create(:admin)} let(:company){FactoryBot.create(:company)} describe 'ユーザー認証のテスト' do describe '法人の新規登録申請' do before do visit new_company_registration_path end it '登録申請に成功する' do fill_in 'company[company_name]', with: "テスト2株式会社" fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ" fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[postal_code]', with: "2222222" fill_in 'company[address]', with: "東京都千代田区222-22-2" fill_in 'company[phone_number]', with: "22222222222" fill_in 'company[password]', with: "test2company" fill_in 'company[password_confirmation]', with: "test2company" click_button '申請する' expect(page).to have_content '登録申請ありがとうございます。法人会員専用ページは運営にて申請が承認がされた後に閲覧可能になります。承認済メールが届くまで今しばらくお待ちください。' end it '登録申請に失敗する' do fill_in 'company[company_name]', with: "" fill_in 'company[kana_company_name]', with: "" fill_in 'company[email]', with: "" fill_in 'company[postal_code]', with: "" fill_in 'company[address]', with: "" fill_in 'company[phone_number]', with: "" fill_in 'company[password]', with: "" fill_in 'company[password_confirmation]', with: "" click_button '申請する' expect(page).to have_content "法人会員 は保存されませんでした。" end end end describe '法人がログイン可能になるまでのテスト' do before do # 法人が登録申請フォーム入力 visit new_company_registration_path fill_in 'company[company_name]', with: "テスト2株式会社" fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ" fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[postal_code]', with: "2222222" fill_in 'company[address]', with: "東京都千代田区222-22-2" fill_in 'company[phone_number]', with: "22222222222" fill_in 'company[password]', with: "test2company" fill_in 'company[password_confirmation]', with: "test2company" click_button '申請する' # 通知が送信される end describe '管理者:通知の確認〜申請承認のテスト' do before do # 管理者でログイン visit new_admin_session_path fill_in 'admin[email]', with: admin.email fill_in 'admin[password]', with: admin.password click_button 'ログイン' end it 'ヘッダーに法人登録申請と表示される' do expect(page).to have_content('法人登録申請') end it '法人登録申請一覧に申請してきた法人名が表示される' do visit admin_notifications_path expect(page).to have_content("テスト2株式会社 様からの法人登録申請があります") end it 'リンクから企業詳細ページへ遷移できる' do visit admin_notifications_path notification = Notification.find_by({receiver_id: admin.id, receiver_class: "admin", sender_id: Company.last.id, sender_class: "company"}) # find("#request_message").click # ページに同一の文言のリンクがある場合(今回の場合「法人登録申請」)、idを指定してあげる find("#request_message").click expect(current_path).to eq('/admin/companies/' + notification.sender_id.to_s) end it '編集画面へ遷移する' do visit admin_company_path(Company.last.id) click_link '編集する' expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s + '/edit') end it '申請ステータスを承認済にする' do visit edit_admin_company_path(Company.last.id) choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid) click_button '変更を保存する' expect(page).to have_content '企業情報の更新が完了しました。' expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s) end end describe '法人:ログインのテスト' do context '承認前の法人ログイン' do it 'ログインに失敗し、メール受信後に再度ログインするようメッセージが出る' do visit new_company_session_path fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[password]', with: "test2company" click_button 'ログイン' expect(page).to have_content '登録申請が未承認です。申し訳ございませんが、承認済メールが届くまで今しばらくお待ちください。' end end context '承認後の法人ログイン' do before do login_as(admin) # 管理者ログイン visit edit_admin_company_path(Company.last.id) choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid) click_button '変更を保存する' click_on 'ログアウト' visit new_company_session_path end it 'ログインに成功する' do fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[password]', with: "test2company" click_button 'ログイン' expect(page).to have_content 'ログインしました。' end it 'ログインに失敗する' do fill_in 'company[email]', with: "" fill_in 'company[password]', with: "" click_button 'ログイン' expect(current_path).to eq(new_company_session_path) end end end end describe '法人会員のテスト' do before do visit new_company_session_path fill_in 'company[email]', with: company.email fill_in 'company[password]', with: company.password click_button 'ログイン' end describe 'マイページのテスト' do it 'ヘッダーにマイページと表示される' do expect(page).to have_content('マイページ') end it 'マイページに遷移し編集リンクが表示される' do visit corporate_company_path(company) expect(page).to have_content('編集する') end end describe '編集のテスト' do before do visit edit_corporate_company_path(company) end context '編集画面へ遷移の確認' do it '遷移ができる' do expect(current_path).to eq('/corporate/companies/' + company.id.to_s + '/edit') end end context '表示及び編集の確認' do it '登録情報編集と表示される' do expect(page).to have_content('登録情報編集') end it 'プロフィール画像編集フォームが表示される' do expect(page).to have_field 'company[profile_image]' end it 'ヘッダー画像編集フォームが表示される' do expect(page).to have_field 'company[background_image]' end it '企業名編集フォームに企業名が表示される' do expect(page).to have_field 'company[company_name]', with: company.company_name end it 'フリガナ編集フォームに自分の企業カナ名が表示される' do expect(page).to have_field 'company[kana_company_name]', with: company.kana_company_name end it 'メールアドレス編集フォームに自分のメールアドレスが表示される' do expect(page).to have_field 'company[email]', with: company.email end it '郵便番号編集フォームに自分の郵便番号が表示される' do expect(page).to have_field 'company[postal_code]', with: company.postal_code end it '住所編集フォームに自分の住所が表示される' do expect(page).to have_field 'company[address]', with: company.address end it '電話番号編集フォームに自分の電話番号が表示される' do expect(page).to have_field 'company[phone_number]', with: company.phone_number end it '自己紹介文編集フォームに自分の自己紹介文が表示される' do expect(page).to have_field 'company[introduction]', with: company.introduction end it '編集に成功する' do fill_in 'company[introduction]', with: "テスト株式会社のマイページへようこそ!" click_button '変更を保存する' expect(page).to have_content '企業情報の更新が完了しました。' expect(current_path).to eq('/corporate/companies/' + company.id.to_s) end it '編集に失敗する' do fill_in 'company[company_name]', with: "" click_button '変更を保存する' expect(page).to have_content '件のエラーが発生したため 法人会員 は保存されませんでした。' end end end end end
まず、法人がログインできるようになるには管理者による申請の承認が必要なため、先に管理者を FactoryBot で作成します。$ bin/rails g factory_bot:model admin を実行し、必要なサンプルデータを入れましょう。以下、サンプルデータを入れたファイルです。spec/factories/admins.rbFactoryBot.define do factory :admin do email { "testadmin@example.com" } password { "testadmin"} end end法人の新規登録申請と、最後の登録情報編集のテストに関しては users_spec.rb のものとほぼ同じなので説明は省略します。
その間の法人がログイン可能になるまでのテストを説明していきます。
以下のコードをご覧下さい。②spec/system/companies_spec.rbdescribe '法人がログイン可能になるまでのテスト' do before do # 1.法人が登録申請フォーム入力 visit new_company_registration_path fill_in 'company[company_name]', with: "テスト2株式会社" fill_in 'company[kana_company_name]', with: "テストツーカブシキガイシャ" fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[postal_code]', with: "2222222" fill_in 'company[address]', with: "東京都千代田区222-22-2" fill_in 'company[phone_number]', with: "22222222222" fill_in 'company[password]', with: "test2company" fill_in 'company[password_confirmation]', with: "test2company" click_button '申請する' # 通知が送信される end describe '管理者:通知の確認〜申請承認のテスト' do before do # 2.管理者でログイン visit new_admin_session_path fill_in 'admin[email]', with: admin.email fill_in 'admin[password]', with: admin.password click_button 'ログイン' end # 3.法人登録申請という文言があることから、管理者ログインができていることを確認 it 'ヘッダーに法人登録申請と表示される' do expect(page).to have_content('法人登録申請') end # 4.法人登録申請のリンクをクリックし、申請企業名を確認 it '法人登録申請一覧に申請してきた法人名が表示される' do visit admin_notifications_path expect(page).to have_content("テスト2株式会社 様からの法人登録申請があります") end # 5.申請一覧から法人登録申請というリンクをクリックする it 'リンクから企業詳細ページへ遷移できる' do visit admin_notifications_path notification = Notification.find_by({receiver_id: admin.id, receiver_class: "admin", sender_id: Company.last.id, sender_class: "company"}) # find("#request_message").click # ページに同一の文言のリンクがある場合(今回の場合「法人登録申請」)、idを指定してあげる find("#request_message").click expect(current_path).to eq('/admin/companies/' + notification.sender_id.to_s) end # 6.登録情報編集ページへ it '編集画面へ遷移する' do visit admin_company_path(Company.last.id) click_link '編集する' expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s + '/edit') end # 7.ラジオボタンに注意 it '申請ステータスを承認済にする' do visit edit_admin_company_path(Company.last.id) choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid) click_button '変更を保存する' expect(page).to have_content '企業情報の更新が完了しました。' expect(current_path).to eq('/admin/companies/' + Company.last.id.to_s) end end describe '法人:ログインのテスト' do context '承認前の法人ログイン' do it 'ログインに失敗し、メール受信後に再度ログインするようメッセージが出る' do visit new_company_session_path fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[password]', with: "test2company" click_button 'ログイン' expect(page).to have_content '登録申請が未承認です。申し訳ございませんが、承認済メールが届くまで今しばらくお待ちください。' end end context '承認後の法人ログイン' do before do login_as(admin) # 管理者ログイン visit edit_admin_company_path(Company.last.id) choose "company_approved_true" # 申請ステータスを承認済にチェック(company_approved_trueはラジオボタン要素のid) click_button '変更を保存する' click_on 'ログアウト' visit new_company_session_path end it 'ログインに成功する' do fill_in 'company[email]', with: "test2company@example.com" fill_in 'company[password]', with: "test2company" click_button 'ログイン' expect(page).to have_content 'ログインしました。' end it 'ログインに失敗する' do fill_in 'company[email]', with: "" fill_in 'company[password]', with: "" click_button 'ログイン' expect(current_path).to eq(new_company_session_path) end end end end
私「よし、ファイルも作成されたし let(:admin){FactoryBot.create(:admin)} と書いてっと...これでadminという変数が使えるようになったぜ!」一見問題ないように見える上記の記述、これが沼への入り口だったのです。
法人が登録申請フォームを入力
beforeブロックでは法人が登録申請フォームを入力し、申請するボタンを押したところまでを記述します。
この時点で管理者へ申請通知が送信されている状態です。つまづきポイント 管理者でログイン「letの遅延評価」
私「よし、申請承認するためにまずは管理者でログインっと...あれ?なんかめちゃくちゃテスト失敗するんですが、なぜ...??」
私はここでテストが失敗する理由をしばらく見つけることができず、負のスパイラルへ陥りました。
よく調べてみると先ほどの let(:admin){FactoryBot.create(:admin)} という記述、注目すべきはlet(:admin)の部分。一見何も問題がないように見えますが、先にちょこっと話していた通り、letには「遅延評価される」という特徴があるのです。
遅延評価って何やねん?という方はこちらの記事をどうぞ。
RSpecのletでcreateした時に気をつけること 〜遅延評価〜
RSpecのletを使うのはどんなときか?(翻訳)
こちらの記事によると、ただのletでcreateした時は、代入した変数が参照されたタイミングで評価されるんだそう=遅延評価。
つまり、fill_in 'admin[email]', with: admin.email
のように admin という変数が使用されたタイミングでなければ、adminのデータ内容というのは無いものとみなされます。なるほど、でもadminって書いてるんだよな〜と思いながらlet!にしてみると無事テストが通りました。
うむ、、通ったけどなぜだろう... describeがいくつか重なっていることが原因かなとも思いましたが、結局わかりませんでした。。詳しい方、教えてください。。法人登録申請という文言があることから、管理者ログインができていることを確認
管理者ログイン後のヘッダーには法人登録申請というリンクがあるため、その表示があることを確かめ、ログインができたことを確認します。法人登録申請のリンクをクリックし、申請企業名を確認
ヘッダーのリンクをクリックすると、企業名が表示されるので 1で申請した企業であるかを確認つまづきポイント「申請一覧から法人登録申請というリンクをクリックする」
ここでもつまづきました。注目すべきはfind("#request_message").clickの部分。今やりたいことは「法人登録申請というリンクをクリックし、企業詳細ページへ遷移する」ということです。普通であればclick_linkかclick_onで遷移できるはずです。しかし上手くいかずエラーが出ました。この時のエラー文がこちら。
Capybara::Ambiguous:
Ambiguous match, found 2 elements matching visible link "法人登録申請"これを要約すると
カピバラ「あの、"法人登録申請"というリンクが2つあるんですが、僕はどっちに行けばいいですか??」
という意味になります。私はここで気づきました。
2つあるやん...カピバラごめん...
ということで改善法をググった結果、click_linkではなく、ビューの対象部分に固有のid名(request_message)を指定し、テストコードはfind("#request_message").clickという記述で解決しました。idを指定して、対象の方をクリックしてねということですね。<strong><%= notification.sender_name %></strong> 様からの<%= link_to '法人登録申請', admin_company_path(notification.sender_id), id: "request_message" %>があります。登録内容を確認してください。6.登録情報編集ページへ
ここは簡単ですね。特に説明はしません。7.つまづきポイント「ラジオボタンに注意」
Capybara の choose という機能を使い、申請ステータスを承認済にチェックします。
choose "company_approved_true"
という形で記述します。 "company_approved_true" ってどこからきたかというと、googleの検証ツールを使うと、inputタグの中にid名が割り振られていることが分かります。ラジオボタンはそのidを指定して選択しているという形になっています。ただ、他の記事を見た際にlabelタグ内の文字列をターゲットに選択している記事もあります。今回の場合であれば、"承認済"という文字列です。ブラウザ上で見る際は、普通であれば文字列を見て選択すると思います。その為、「検証ツールでidを指定して選択」という方法はできるだけしない方が良いのかもしれません。私は文字列を指定するとテストが通らなかった為、このような形にしています。こちらも詳しい方がいらっしゃいましたら、教えていただきたいです。エラーは解決できても、"それがなぜなのか"という部分が解決できていないのでこの記事を見ていらっしゃる方には申し訳ないです。。逆にわかったら教えてくださいはい!ということで管理者による通知の確認〜申請承認のテストはこれで終わりです!
最後に承認前と承認後でログイン制限がされているか否かを確認するテストを書きます。
ここまで見てくださった方は、恐らく分かると思いますのでコードを参考にしながら記述してみてください!ポイントとして、承認後の法人ログインのテストにあるbeforeブロック内の
login_as(admin)
という記述だけチラッと説明します!
やっていることは単純に管理者でログインしているだけなのですが、"login_as"という機能は前回の記事でお話しした "deviseのヘルパーメソッドをRSpec内で使用可能にする設定" がされていないと、使うことができませんのでここだけ注意です!
何だっけ?と思った方は、前回の記事【第1回】RSpecビギナーが、ビギナーなりにModelSpecを書いてみたをご覧下さい。終わりに
今回は主にシステムスペックによる個人会員と法人会員の認証のテストについてまとめました。
記事の投稿、編集、DM、通知のテストも書こうと思ってたのですが、想像よりも長くなってしまったので一旦ここで分けます。
こちらの機能のシステムスペックに関しては次回掲載致しますので、もしよろしければ見て頂けると嬉しいです。最後までご覧頂きありがとうございました!
参考記事
使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」
RSpecのletでcreateした時に気をつけること 〜遅延評価〜
RSpecのletを使うのはどんなときか?(翻訳)
- 投稿日:2020-09-28T11:50:41+09:00
【Rails6】GraphQLを使用したAPI開発(Query編)
はじめに
rails6でGraphQLを用いた開発を行ったので、導入方法や使い方をまとめてみました。
今回はQuery/アソシエーション/N+1問題を取り上げて、ご紹介します。開発環境
ruby2.7.1
rails6.0.3
GraphQL1. GraphQLって何?
GraphQLとはAPIリクエストのためクエリ言語で下記の特徴があります。
- エンドポイントは
/graphql
1つのみ- Query: データの取得(Get)
- Mutation: データの作成、更新、削除(Create, Update, Delete)
RESTとの大きな違い一つ目のエンドポイントが一つということです。
RESTの場合、/sign_up
,/users
,/users/1
など複数のエンドポイントが存在しますが、
GraphQLの場合は、エンドポイントは/graphql
のみです。RESTでは複数のリソースで必要な場合、複数のAPIリクエストが必要ですが、
GraphQLはエンドポイントが一つなので、必要なデータを一回で取得できコードがシンプルになります。2. 今回使用するテーブル
親のUserテーブルと子のPostテーブルの二つで実装します。
Userが複数のPostを投稿できる1対多の関係とします。ターミナル$ rails g model User name:string email:string $ rails g model Post title:string description:string user:references $ rails db:migrateUserテーブル
カラム 型 name string string user.rbclass User < ApplicationRecord has_many :posts, dependent: :destroy endPostテーブル
カラム 型 title string description string post.rbclass Post < ApplicationRecord belongs_to :user end3. railsにGraphQLを導入する
gemをインストールします。
Gemgilegem 'graphql' #追加 group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'graphiql-rails' #開発環境に追加 endconfig/application.rbrequire "sprockets/railtie" #コメントアウトを外すターミナル$ bundle install $ rails generate graphql:install #GraphQLに関するファイルが作成されます
routes.rb
に下記を追加
エンドポイントが開発環境では/graphiql
, 本番環境では/graphql
となります。routes.rbRails.application.routes.draw do if Rails.env.development? # add the url of your end-point to graphql_path. mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end post '/graphql', to: 'graphql#execute' #ここはrails generate graphql:installで自動生成される end詳細は下記にまとめましたので参考にしてください。
Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む)4. User一覧を取得するQuery
GraphQLではType(modelごとに定義する)を持っており、そのTypeに従い、Queryを実行してデータを取得します。
UserのObjectTypeを作成
下記のコマンドでUserのTypeを作成します。
!
をつけるとnull:false
が追加されます。ターミナル$ rails g graphql:object User id:ID! name:String! email:String!下記のファイルが生成されます。
user_type.rbmodule Types class UserType < Types::BaseObject field :id, ID, null: false # `!`をつけると`null:false`が追加されます。 field :name, String, null: false field :email, String, null: false end endUserのQueryを作成
先ほど作成した
user_type
を元にqueryを作成します。query_type.rbmodule Types class QueryType < Types::BaseObject field :users, [Types::UserType], null: false # userを配列で定義する def users User.all # user一覧を取得 end end endUserのデータをコンソールで作成します。
$ rails c $ > User.create(name: "user1", email: "user-1@test.com") $ > User.create(name: "user2", email: "user-2@test.com")Queryを実行する
準備は整いましたので、サーバーを立ち上げてGraphiqlで確認します。(http://localhost:3000/graphiql)
$ rails s下記のqueryを実行します。
query{ users{ id name email } }するとjson形式のレスポンスが返ってきます。
usersのTypeを配列にしているので配列となっています。{ "data": { "users": [ { "id": "1", "name": "user1", "email": "user-1@test.com" }, { "id": "2", "name": "user2", "email": "user-2@test.com" } ] } }http://localhost:3000/graphiql に接続して、実行した結果です。
必要なデータだけリクエストし受け取る場合
すべてカラムのデータが必要でない場合はqueryを変更します。
例えばuserのidだけ取得することもできます。query{ users{ id } }レスポンス
{ "data": { "users": [ { "id": "1", }, { "id": "2", } ] } }5. アソシエーション
次にUserに紐づくPostを取得してみます。
Userを同じようにObjectTypeとQueryを作成します。PostのObjectTypeを生成
ターミナル$ rails g graphql:object Post id:ID! title:String! description:String!下記のファイルが生成されます。
Userのデータを取得するためにfield :user, Types::UserType, null: false
を追加します。post_type.rbmodule Types class PostType < Types::BaseObject field :id, ID, null: false field :title, String, null: false field :description, String, null: false field :user, Types::UserType, null: false # この一文を追加。belongs_to :userのようなもの end endUserTypeには
field :posts, [Types::PostType], null: false
を追加します。
こちらは複数データが紐づくので配列で定義します。user_type.rbmodule Types class UserType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :email, String, null: false field :posts, [Types::PostType], null: false # この一文を追加。has_many :postsのようなもの end endターミナル$ rails c $ > Post.create(title: "title", description: "description", user_id: 1)PostのQueryを生成する
query_type.rbmodule Types class QueryType < Types::BaseObject field :users, [Types::UserType], null: false def users User.all end # 下記を追加 field :posts, [Types::PostType], null: false def posts Post.all end end endQueryを実行する
実行するqueryです。
usersにpostをネストさせてリクエストします。query{ users{ id name email post{ id title description } } }実行するとネストしたPostのデータが返ってきます。
RESTの場合でいうuser.posts
のデータを取得できます。
post.user
のデータが必要な場合は、下記のようなqueryで取得できます。query{ posts{ id title description user{ id } } }6. N+1を検知するBulletを導入する
GraphQLのクエリは木構造になっているので、アソシエーションがあるとN+1問題が発生しやすいです。
そこで、N+1を検知するgemであるBulletを導入することをおすすめします。Bulletをインストール
Gemfilegroup :development do gem 'bullet' endターミナル$ bundle installconfig/environments/development.rbに下記の設定を追加
config/environments/development.rbconfig.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.console = true Bullet.rails_logger = true endN+1を確認してみる
Bulletの導入が完了したら、N+1が発生していないか確認してみましょう
query{ users{ id name email posts{ id title description } } }先ほどと同様のqueryを実行すると下記のようなログが出ます。
ターミナルProcessing by GraphqlController#execute as */* Parameters: {"query"=>"query{\n users{\n id\n name\n email\n posts{\n id\n title\n description\n }\n }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n users{\n id\n name\n email\n posts{\n id\n title\n description\n }\n }\n}", "variables"=>nil}} User Load (0.2ms) SELECT "users".* FROM "users" ↳ app/controllers/graphql_controller.rb:15:in `execute' Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]] ↳ app/controllers/graphql_controller.rb:15:in `execute' Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 2]] ↳ app/controllers/graphql_controller.rb:15:in `execute' Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 1.7ms | Allocations: 18427) POST /graphql USE eager loading detected User => [:posts] Add to your query: .includes([:posts]) Call stack
Add to your query: .includes([:posts])
と言われているのでN+1が発生しています。
SQLも三回発行されています。ターミナルUser Load (0.2ms) SELECT "users".* FROM "users" Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]] Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 2]]N+1を解消する方法
N+1を解消するには通常通り、
includes
すれば大丈夫です。
dataloaderというものでも解消できますが、今回はincludes
を使用します。
User一覧を取得している部分を下記のように変更します。query_type.rbdef users # User.all # 変更前 User.includes(:posts).all # 変更後 endでは、N+1が解消されたかログで確認してみましょう。
警告がなくなっています。ターミナルProcessing by GraphqlController#execute as */* Parameters: {"query"=>"query{\n users{\n id\n name\n email\n posts{\n id\n title\n description\n }\n }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n users{\n id\n name\n email\n posts{\n id\n title\n description\n }\n }\n}", "variables"=>nil}} User Load (0.7ms) SELECT "users".* FROM "users" ↳ app/controllers/graphql_controller.rb:15:in `execute' Post Load (1.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?) [["user_id", 1], ["user_id", 2]] ↳ app/controllers/graphql_controller.rb:15:in `execute' Completed 200 OK in 57ms (Views: 0.2ms | ActiveRecord: 1.9ms | Allocations: 15965)SQL文も2つに減ったので無事にN+1が解消されました。
ターミナルUser Load (0.7ms) SELECT "users".* FROM "users" Post Load (1.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?) [["user_id", 1], ["user_id", 2]]7. resolverの切り出し
通常のQuery
普通にコードを書いていくと、モデルに関係なく、fieldsとメソッドが
query_type
に追加されるのでquery_type
がどんどん肥大化します。query_type.rbmodule Types class QueryType < Types::BaseObject field :users, [Types::UserType], null: false def users User.includes(:posts).all end field :posts, [Types::PostType], null: false def posts Post.all end end endResolverを使ったQuery
GitHubのissueでは、Resolverを使用することで、query_type.rbの肥大化を回避するベストプラクティスが紹介されています。
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410
query_type
にはfieldのみを定義します。query_type.rbmodule Types class QueryType < BaseObject field :users, resolver: Resolvers::QueryTypes::UsersResolver field :posts, resolver: Resolvers::QueryTypes::PostsResolver end endそして、メソッドの部分はObjectTypeごとにResolverに切り出します。(新たにresolversディレクトリを作成しました。)
GraphQL::Schema::Resolver
の記載を忘れるとエラーが出るので忘れないように注意してください。resolvers/query_types/users_resolver.rbmodule Resolvers::QueryTypes class UsersResolver < GraphQL::Schema::Resolver type [Types::UserType], null: false def resolve User.includes(:posts).all end end endresolvers/query_types/posts_resolver.rbmodule Resolvers::QueryTypes class PostsResolver < GraphQL::Schema::Resolver type [Types::PostType], null: false def resolve Post.all end end end終わりに
GraphQLついて、自分の復習もかねてまとめていたら思いの外、長文となってしまいました。
rspecやmutationについても書きたかったですが、次回にしたいと思います。
- 投稿日:2020-09-28T11:43:54+09:00
プログラムにおける処理の順番
学習期間が現在11週目に突入。今まで学んだ事の振り返りと、よく間違えている事について紹介します。
今回は「処理の順番」です。基本中の基本で初歩的な内容ですが、記述の順番を間違えておりエラーが発生していたというケースも多々あるので、学習し始めたばかりの方の参考になれば幸いです。
また現在学習している言語は「Ruby」ですので、内容も「Ruby」のものとなります。使用しているrubyのバージョンは以下の通り。
ruby 2.6.5処理の順番について
まずソース内でのプログラム処理の順番は、基本的に「上から下へ1行ずつ順番に」です。
これは複雑な記述やRuby on Railsを使ってアプリを作成した際でも変わりません。(※メソッドの呼び出しや条件分岐、繰り返し処理を行う際は下から上へ戻ったり行が飛んだりしますが、基本的な処理の順番を意識しておくと理解しやすいと思います。)
簡単なプログラムで表現すると、puts "1行目" puts "2行目" puts "3行目"と記述した場合、コンソールで実行すると
1行目 2行目 3行目となります。この記述で
puts "3行目"
というプログラムの出力結果が1行目に来ることはありません。あったら大変です。
これに条件分岐(ifやwhileなど)や繰り返し処理(timesやeachなど)が重なってくると、どうしても処理の順番を疎かにしがちです。(私だけかも知れませんが・・・)例えば、有名な「FizzBuzz問題」を例に出すと、
FizzBuzz問題 1~100の数字を出力させるプログラムを作成します。 3の倍数の時は「Fizz」と表示し5の倍数の時は「Buzz」と 表示させるプログラムを作りなさい。 ただし15の倍数の時は「FizzBuzz」と表示させなさい。この答えの一例は、
#正しいコード def fizz_buzz num = 0 while num <= 99 num += 1 if num % 15 == 0 puts "FizzBuzz" elsif num % 3 == 0 puts "Fizz" elsif num % 5 == 0 puts "Buzz" else puts num end end end puts fizz_buzz出力結果は以下の通り
このプログラムの条件式の順番を以下のように変えてみましょう。
#間違ったコード def fizz_buzz num = 0 while num <= 99 num += 1 if num % 3 == 0 # num % 15 == 0の条件式から変更 puts "Fizz" elsif num % 5 == 0 puts "Buzz" elsif num % 15 == 0 #num % 3 == 0の条件式から変更 puts "FizzBuzz" else puts num end end end puts fizz_buzz上記のように記述した場合の出力結果がこちら。
15の倍数の時に"FizzBuzz"と表示されず"Fizz"と表示されます。
こうなってしまう理由は「プログラムの処理の順番を理解していないから」。上記の間違ったコードの処理の順番を考えると、
例えばnum == 3の場合、#間違ったコード if num % 3 == 0 # この式が適用される puts "Fizz" => "Fizz"num == 5の場合
#間違ったコード if num % 3 == 0 # num == 5は3で割り切れないので、次の処理をする puts "Fizz" elsif num % 5 == 0 # num == 5は5で割り切れるので、この処理を実行 puts "Buzz" => "Buzz"問題のnum == 15の場合
#間違ったコード if num % 3 == 0 # num == 15は3で割り切れるから、この処理を実行! puts "Fizz" elsif num % 5 == 0 # num == 15は処理されたから仕事なくなった。 puts "Buzz" elsif num % 15 == 0 # num == 15は処理されたから仕事なくなった。 puts "FizzBuzz" end => "Fizz"こんな感じで処理がされてしまいます。なので、プログラムを書く際の順番を気をつけましょう。
if num % 15 == 0 # まずnumが15で割り切れるかどうかを判断する。 puts "FizzBuzz" # 条件式に当てはまれば"FizzBuzz"と表示当てはまらなければ次へ elsif num % 3 == 0 # 次に15で割り切れないけど3で割り切れるかを判断 puts "Fizz" # 条件式に当てはまれば"Fizz"と表示。 # これも当てはまらなければ次へ elsif num % 5 == 0 # 3でも15でも割り切れないけど、5で割り切れるか判断。 puts "Buzz" # 条件式に当てはまれば"Buzz"と表示。 # これも当てはまらなければ次へ else puts num # これまでの条件に全て当てはまらない場合、 end # そのままnumを表示以上。
基本的な事ですがこれまでの経験から、こういった基本がしっかり身についておらず困ったという場面がありましたので、もし同じような場面に直面したら参考にしていただければ幸いです。
- 投稿日:2020-09-28T10:42:30+09:00
最後に任意の文字があるかのメソッドを作る
【概要】
1.結論
2.どのようにコーディングするか
3.開発環境
1.結論
downcaseメソッド、lengthメソッドと、sliceメソッドの3種類を使う!
2.どのようにコーディングするか
def str_discrimination(str_a, str_b) a_down_str = str_a.downcase #---❶ b_down_str = str_b.downcase a_len_str = a_down_str.length #---❷ b_len_str = b_down_str.length if b_down_str.slice(-(a_len_str)..- 1) == a_down_str || a_down_str.slice(-(b_len_str)..- 1) == b_len_str #---❸ puts "True" else puts "False" end end❶:大文字小文字で区別しないためです。任意の2種類の文字列(str_a,str_b)をdowncaseメソッドで全てを小文字にしました。
変数(文字列).downcase❷:❶で小文字に変換した変数をlengthメソッドで文字数を返しています。これは❸で使うためにコーディングしています。
❸:ここで最後に任意の文字があるかを判別しています。str_aやstr_bのどちらに探されたい文字列、探したい文字が来てもいいようにOR条件にしています。またsliceメソッドを使用し、❷で使用したlengthメソッドで代入された変数を入れることで最後から任意の文字を探しています。(-(a_len_str)..- 1)は、最後から(-1)から-(a_len_str)の文字数までを示しています。
参考にしたURL:
はじめてのRuby!文字列を大文字⇔小文字に変換する方法まとめ
length、size、count メソッドの違いまとめ【Ruby】3.開発環境
Ruby 2.6.5
Rails 6.0.3.3
Visual Studio Code
- 投稿日:2020-09-28T10:26:54+09:00
CodeWarでの勉強(ruby)④ case~when
この記事について
最近始めたCodewarを通じて学べたことを少しずつアウトプット
問題
You probably know the "like" system from Facebook and other pages. People can "like" blog posts, pictures or other items. We want to create the text that should be displayed next to such an item.
Implement a function likes :: [String] -> String, which must take in input array, containing the names of people who like an item. It must return the display text as shown in the examples:引数で配列を受け取るlikesメソッドを実装して、配列に含まれる情報から誰がイイねをしたか表示させるようにさせる。
likes [] -- must be "no one likes this" likes ["Peter"] -- must be "Peter likes this" likes ["Jacob", "Alex"] -- must be "Jacob and Alex like this" likes ["Max", "John", "Mark"] -- must be "Max, John and Mark like this" likes ["Alex", "Jacob", "Mark", "Max"] -- must be "Alex, Jacob and 2 others like this"僕の回答
def likes(names) return "no one likes this" if names.empty? if names.count == 1 "#{names[0]} likes this" elsif names.count == 2 names.each { |n| }.join(" and ") << " like this" elsif names.count == 3 "#{names[0]}, #{names[1]} and #{names[2]} like this" else others_count = names.count -2 "#{names[0]}, #{names[1]} and #{others_count} others like this" end end配列の長さによって返す情報を変えていくシンプルのやり方だと思う。
強調しておきたいのが、return "no one likes this" if names.empty?
というように人生で初めて1行完結のif文を書けたことだ!!!!!ベストプラクティス
めっちゃ見やすい。。。
複数の条件でやる場合はcase~when
の方が読みやすくてイイね。def likes(names) case names.size when 0 "no one likes this" when 1 "#{names[0]} likes this" when 2 "#{names[0]} and #{names[1]} like this" when 3 "#{names[0]}, #{names[1]} and #{names[2]} like this" else "#{names[0]}, #{names[1]} and #{names.size - 2} others like this" end end
- 投稿日:2020-09-28T09:20:07+09:00
[Rails]データベースに保存された情報をviewに表示する方法
投稿の内容
今回はデータベースに保存された情報をviewに表示する方法について投稿します。
なお、既にデータベースに情報は保存されている、として投稿します。実装の流れは以下の通り。
① コントローラーアクションの記述(showアクション)
② 表示したい情報が文字列の場合
③ 表示したい情報が複数枚画像の場合環境
Rails 5.2.4.3
ruby 2.5.1
mysql 14.14
viewはhamlで実装① コントローラーアクションの記述(showアクション)
今回は
商品(item)詳細ページ
に情報を表示したいので、itemsコントローラーのshowアクション
を使います。items.controller.rbclass ItemsController < ApplicationController #省略 def show @item = Item.find(params[:id]) end #省略解説) まず
Itemモデルのpathのidをfindメソッドで検索し、該当するidに属した商品(item)のインスタンスを作成します。
← 分かりにくければすいません...。ここで作成したインスタンスをもとに、データベースから情報を引き出します。
② 表示したい情報がテキストの場合
データベースに保存されている情報が上記画像のような場合、= @item.カラム名
と指定してあげるとviewに表示できます。例えば、nameカラムの情報を表示したい場合は以下のようにすればOKです。
show.html.haml%h2.item-show-page__item-name = @item.name③ 表示したい情報が複数枚画像の場合
今回はitemsテーブルとアソシエーションを組んでいるitem_imagesテーブルに保存されている複数枚画像を表示したいとします。
show.html.haml- @item.item_images.each do |image| = image_tag image.image.url解説) まず、itemsテーブル(@item)とアソシエーションを組んでいるitem_imagesテーブルをブロック変数imageに変換します。次に、
image_tag
を使い、先ほど変換したブロック変数image(item_imagesテーブル)のimageを呼び出し、.url
を付けます。この
.url
はデータベースから情報を選択し、viewに表示させる場合必須となります。.url
がなければviewにはデータベースに保存されている画像URLが表示されてしまします。あとは
each文
で繰り返し処理ですね。最後に
今回はデータベースに保存された情報をviewに表示する方法について投稿しました。
Railsでアプリケーションを開発する際に、必ず用いる方法と言っても過言ではないと思いますので、是非参考にしていただければと思います。最後まで目を通していただきありがとうございました!
- 投稿日:2020-09-28T03:21:44+09:00
Rails6 OmniAuth activestorage ユーザー画像を取得する
永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。
この記事の目的
active storageにユーザーのプロフィール画像を保存する。
ググってみてもcarrierwaveばかりでactive storageの記事をあまり見かけなかったので、同じ境遇の人がいればと思い投稿ました。環境
Ruby 2.7.1p83
Rails 6.0.3.3
前提条件
deviseによるログイン機能実装済み。
OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。
私は、以下の記事を参考にさせて頂きました
・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240参考
・active storageに画像URLを保存する方法
https://qiita.com/gomasio1010/items/09c6ee58ed4c95f109ffとても助かりました!ありがとうございます。
active storageに画像urlを保存する
機能実装できている前提で、
app/models/user.rb
require "open-uri" #ここ class User < ApplicationRecord #省略 def self.from_omniauth(auth) where(provider: auth.provider, uid: auth.uid).first_or_create do |user| user.provider = auth.provider user.uid = auth.uid user.name = auth.info.name user.password = Devise.friendly_token[0, 20] user.email = auth.info.email user.email = User.dummy_email(auth) if user.provider == "twitter" avatar = open("#{auth.info.image}") #ここ user.image.attach(io: avatar, filename: "user_avatar.jpg") #ここ end end #省略 end記事を参考に画像を取得する事ができました。
active storageでの記事はあまり見かけなかったので、とても助かりました。画像が小さくて、荒くなるとの噂でしたので、サイズが大きくなりそうな感じにしてみました。
色々見ているとモデル側で指定している方もいらっしゃいました。config/initializers/devise.rb
config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET_KEY'], :image_size => 'large'#これ config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'] config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY'], callback_url: "http://localhost:3000/users/auth/twitter/callback", :image_size => 'original'#これログイン認証関連の実装は、時間帯によってうまくいかなかったり、ブラウザにcookieが残っていると正しく動かない時がありましたので気長にやると良さそうです。
- 投稿日:2020-09-28T02:44:24+09:00
Rails6 OmniAuth twitter認証 emailの条件分岐
永遠の初心者による自分のためのメモ。
Rails学習開始3ヶ月。
とりあえずこれで動きました程度に考えていただければ幸いです。
こうした方がええやんと言うご意見がある方は優しいコメント頂けると嬉しいです。
この記事の目的
twitter認証はメールアドレスを取得しないので、ランダムメールアドレスを生成する。
google、facebook認証は、正規のメールアドレスを取得する。
twitterはランダムアドレス。他は、正規のアドレスで条件分岐させる。環境
Ruby 2.7.1p83
Rails 6.0.3.3
前提条件
deviseによるログイン機能実装済み。
OmniAuthによるTwitter、google、facebookなどのログイン認証機能実装済み。
私は、以下の記事を参考にさせて頂きました
・手続き関連
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・OmniAuth導入
https://qiita.com/LuckHackMahiro/items/9dfca6e67777a2161240条件分岐 : TwitterかTwitter以外
機能実装できている前提で、
app/models/user.rb
def self.from_omniauth(auth) where(provider: auth.provider, uid: auth.uid).first_or_create do |user| user.provider = auth.provider user.uid = auth.uid user.name = auth.info.name user.password = Devise.friendly_token[0, 20] user.email = auth.info.email #google,facebookの時 user.email = User.dummy_email(auth) if user.provider == "twitter" #twitterの時 avatar = open("#{auth.info.image}") user.image.attach(io: avatar, filename: "user_avatar.jpg") end endとしたら、twitterではランダムアドレス。
google、facebookでは正規のアドレスが取得する事ができました。条件分岐がうまくいって嬉しかったです。