20211124のRailsに関する記事は10件です。

Rails scaffoldを使ってRailsに入門する

はじめに Railsのscaffoldを使用してCRUDのアプリケーションを生成してみます。 また、生成されたコードを読み解いて処理の流れをざっくりと読み解いてみます。 なお、本編の内容はRailsチュートリアルの2章を参考にしています。随時、参考になった文献を記載しています。 第2章 Toyアプリケーション - Railsチュートリアル Rails scaffoldとは Railsの機能の一つで、コマンド1つでCRUDの雛形を作成することが出来る scaffoldとは「足場」という意味を持ち、Railsアプリケーションの足場を作る機能と言える What is Scaffolding in Ruby on Rails? - RubyGuides 使ってみる scaffoldコマンドを実行してコードを生成する scaffoldコマンドは rails generate scaffold {モデル} {フィールド}:{型} の形で入力します。実行するとファイルが自動生成されます。 $ rails generate scaffold User name:string email:string マイグレーションをする マイグレーションファイルも生成されるため、マイグレーションを行います。 db/migrate/{日付}_create_users.rb class CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :name t.string :email t.timestamps end end end $ rails db:migrate サーバーを立ち上げる 以下のコマンドでサーバーを立ち上げます。 $ rails server ここまでの作業を終えると画面が閲覧できるようになります。 ユーザ一覧画面(/users) ユーザー登録画面(/users/new) 他にもありますが省略します。 読み解く ユーザ一一覧画面にユーザーが表示されるまでの流れを読み解いてみます。 以下の図がわかりやすいので引用させていただきます。 (引用: 第2章 Toyアプリケーション - Railsチュートリアル) ① ブラウザから/usersのパスでリクエスト受ける ② ルーターによってUsersコントローラー内のindexメソッドが実行される resources :{リソース名}を記述すると複数のルーティングが自動で生成される。 config/routes.rb Rails.application.routes.draw do resources :users root 'application#hello' end ルーティングの確認は rails routesで行う。 $ rails routes Prefix Verb URI Pattern Controller#Action users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy root GET / application#hello /usersでは、Usersコントローラー内のindexメソッドが実行されることがわかる。 Rails のルーティング - Railsガイド Railsのresourcesとresourceついて - Qiita ③ indexメソッドが実行され、Userモデルのallメソッドが実行される @{変数名}はインスタンス変数であることを表す。 app/controllers/users_controller.rb def index @users = User.all end ④ データベースからユーザーの情報を取得する ⑤ Usersコントローラー内のusersのインスタンス変数に格納する ⑥ users変数をindexビューに渡す @で宣言した変数はビューでも利用できる。 app/views/users/index.html.erb ... <% @users.each do |user| %> <tr> <td><%= user.name %></td> <td><%= user.email %></td> <td><%= link_to 'Show', user %></td> <td><%= link_to 'Edit', edit_user_path(user) %></td> <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> ... ⑦ ERB(Embedded RuBy: ビューのHTMLに埋め込まれているRubyコード)を実行してHTMLを生成しコントローラーに返す ⑧ HTMLをブラウザに返す 最後に 今回はRails scaffoldを試してみました。 手軽にCRUDが生成されることに驚きつつも、大半がブラックボックスになってしまうので積極的に使うべきではないと感じました。 引き続きRailsの学習を進めていきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マイグレーションエラーについて

railsでデータベースを用意する際に、 $ rails generate scaffold ・・・ コマンドなどでマイグレーションファイルを作成した後、 $ rails db:migrate でデータベースの変更を反映しないと、 ActiveRecord::PendingMigrationError マイグレーションエラーとなってしまいます。 忘れないようにしましょう。 (偉そうにすみません。自分が忘れていたので。) 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ルーティングのまとめ

概要 railsのroutes.rbに記述するルーティングについてのまとめ。 内容 ルーティングとは 受け取ったurlを認識し、適切なコントローラ内アクションやアプリケーションに割り当てる機能。 構文 httpメソッド 'urlパターン', to: 'コントローラ#アクション' ex) 以下のhttpリクエストを受け取った場合、以下のようになる。 GET /patients/17 routes.rb get '/patients/:id', to: 'patients#show' HTTPメソッド クライアントから送られたリクエストの種別を表すもの。 get  ページを表示する操作。 post  データを登録する操作。 put  データを変更する操作。 delete  データを削除する操作。 rootの設定  http://localhost:3000 / にアクセスした際にアクションを呼び出す際に使う。 root to: 'home#index' ネスト  一対多(一つの投稿に複数のコメント)の場合、urlは「https://×××××××/post/id/comment/id」のようになる。このような場合にルーティングをネストさせる。 resources :post do resources :comment end resources  7つのアクションのルーティングを自動定義する。 resources :コントローラー名 resources :コントローラー名, :コントローラー名 #複数コントローラのルーティングを一行で定義することもできる。 only  resources :tweets, only: [:index, :snow] --> #indexとshowのみを指定して定義できる resource  resourcesは複数形リソースな為、indexやid付のルーティングが生成されるが、 resourceは単数系リソースな為、idの無いリソースを生成できる。 例えば、マイページのような「ログインしているユーザー自身」のデータを表示する際はidを付ける必要が無い為、showアクションの/profile/:idではなく/profileを割り当てることができる。 namespace  指定したルーティングの配下にルーティングを設定する. 名前空間でコントローラを分ける時などに使う。 admin画面の場合 namespace :admin do    resources :articles end 生成されるurl /admin/articles/:id member  特定のデータに対するアクションに対して利用する。 ユーザーidを含んだurlにアクセスできる。 ex)ユーザーに対するふフォロー機能を追加する時   以下のようなurlを想定。 http://$(DNS)/users/1/follow memberを使ったurlは以下のようになる。 resources :users do member do post :follow end end #memberルーティングが一つしかない場合は、onオプションを利用して1行で書ける resources :users do post :follow, on: :member end collection  全てのデータに対するアクションに利用する。 ex) searchアクションでユーザー検索を行う   以下のようなurlを想定。 http://$(DNS)/users/search collectionを使った記述は以下のようになる。 resources :users do get :search, on: :collection end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

改【Railsチュートリアル】 プロフィール画像をアプリ内で設定できるように変更

はじめに 前回の記事【Railsチュートリアル】 プロフィール画像をアプリ内で設定できるように変更で、sample_appにおいて、プロフィール画像をアプリ内で設定できるようにしていったが、本番環境でアセットパイプラインに関してのエラーが発生したため、carrierwaveを用いた方法に変更する。 また、Rialsチュートリアル第6版13章においてAWSのS3を作成済なので、今回は本番環境のみそちらにアップロードするよう設定していく。 前提 第6版sample_appが完成している carrierwaveの設定 Gemfileにcarrierwaveを追加 Gemfile gem 'carrierwave' $ bundle install アップローダーの作成し、uploadersディレクトリの中にavatar_uploader.rbが作成されているのを確認する。 $ rails g uploader Avatar usersテーブルにavatarカラムを追加する。 ここで注意すべきなのが、カラム名をimageにしてしまうと何故かエラーが発生するのでそれ以外にする必要がある。 rails g migration add_avatar_to_users avatar:string db/migrate/[timestamop]_add_avatar_to_users.rb class AddImageToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :avatar, :string end end $ rails db:migrate ユーザーコントローラーのuser_paramsにavatarカラムを追加 app/controllers/users_controller.rb . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation, :avatar) end . . . ユーザーモデルにマウントを記述 app/models/user.rb class User < ApplicationRecord . . . mount_uploader :avatar, AvatarUploader . . . ユーザー編集画面で画像変更を可能にする 初期画像default_user.jpgをapp/assets/imagesディレクトリ下に用意しておく。 ├ app/ ├ assets/ ├ images/                      └ default_user.jpg app/views/users/edit.html.erb . . . <%= form_with(model: @user, local: true) do |f| %> . . . <% if @user.avatar? %> <%= image_tag @user.avatar.url, width: "100px", height: "100px", class:"icon"%> <% else%> <%= image_tag "default_user.jpeg", width: "100px", height: "100px", class:"icon"%> <% end %> <%= f.label :avatar, "Icon change" %> <%= f.file_field :avatar, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> </div> </div> 以下のようになっていれば成功。 画像を表示する gravatarで表示していたビューをimage_tagに変更していく。 画像を持たないユーザーにはデフォルトの画像を表示させる。 編集するビューは以下。 views/microposts/micropost.html.erb views/shared/_user_info.html.erb views/users/_user.html.erb views/users/edit.html.erb views/users/show_follow.html.erb views/users/show.html.erb 例えば views/microposts/micropost.html.erb <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> を views/microposts/micropost.html.erb <% if micropost.user.avatar? %> <%= link_to image_tag(micropost.user.avatar.url, width: "50px", height: "50px", class:"icon"),micropost.user%> <% else%> <%= link_to image_tag("default_user.jpeg", width: "50px", height: "50px", class:"icon"),micropost.user%> <% end %> というように変更する。 最後にcssを変更する。 自分は画像を丸く表示させたかったので以下のように記述。 app/assets/stylesheets/custom.scss . . . .icon { border-radius: 50%; object-fit: cover; float: left; margin-right: 10px; } 以下のようになっていれば成功。 本番環境でAWS S3にアップロードするよう設定 現在の設定ではアップロードされた画像はpublic/uploads/user/avatarに追加されていくが、本番環境のみRialsチュートリアル第6版13章で作成したAWS S3にアップロードするように設定していく。 Gemfileにfog-awsを追加 gem 'fog-aws' $ bundle install avatar_uploader.rbの設定を変更 今回は画像のみ受け付けたいのでdef extension_allowlistも記述したが、iPhoneで撮影した写真は拡張子がheicであることも多いため、jpgなどに変換する機能を検討中である。 app/uploaders/avatar_uploader.rb . . . if Rails.env.development? storage :file elsif Rails.env.test? storage :file else storage :fog end . . . def extension_allowlist %w(jpg jpeg png) end end チュートリアルにおいて、下記の環境変数をHerokuに設定済みなので、そちらを利用していく。 もしまだなら、 $ heroku config:add で追加していく。 config/initializers配下に、carrierwave.rbを作成し、設定を記述していく。 config/initializers/carrierwave.rb require 'carrierwave/storage/abstract' require 'carrierwave/storage/file' require 'carrierwave/storage/fog' if Rails.env.production? CarrierWave.configure do |config| config.fog_provider = 'fog/aws' config.fog_directory = ENV['AWS_BUCKET'] config.fog_credentials = { provider: 'AWS', region: ENV['AWS_REGION'], aws_access_key_id: ENV['AWS_ACCESS_KEY'], aws_secret_access_key: ENV['AWS_SECRET_KEY'], path_style: true } end end AWSを開き、Amazon S3→バケット→アクセス許可と進み、ブロックパブリックアクセス (バケット設定)を以下のように設定する。 バケットポリシーに以下のように記述 { "Version": "2012-10-17", "Statement": [ { "Sid": "statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::(AWSアカウントID):user/(IAMユーザー名)" }, "Action": "*", "Resource": "arn:aws:s3:::(バケット名)" } ] }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

改【Railsチュートリアル】プロフィール画像をアプリ内で設定できるように変更

はじめに 前回の記事【Railsチュートリアル】プロフィール画像をアプリ内で設定できるように変更で、sample_appにおいて、プロフィール画像をアプリ内で設定できるようにしたが、本番環境でアセットパイプラインに関してのエラーが発生したため、carrierwaveを用いた方法に変更する。 また、Rialsチュートリアル第6版13章においてAWSのS3を作成済みなので、今回は本番環境のみそちらにアップロードするよう設定していく。 前提 第6版sample_appが完成している carrierwaveの設定 Gemfileにcarrierwaveを追加 Gemfile gem 'carrierwave' $ bundle install アップローダーの作成し、uploadersディレクトリの中にavatar_uploader.rbが作成されているのを確認する。 $ rails g uploader Avatar usersテーブルにavatarカラムを追加する。 ここで注意すべきなのが、カラム名をimageにしてしまうと何故かエラーが発生するのでそれ以外にする必要がある。 rails g migration add_avatar_to_users avatar:string db/migrate/[timestamop]_add_avatar_to_users.rb class AddImageToUsers < ActiveRecord::Migration[6.1] def change add_column :users, :avatar, :string end end $ rails db:migrate ユーザーコントローラーのuser_paramsにavatarカラムを追加 app/controllers/users_controller.rb . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation, :avatar) end . . . ユーザーモデルにマウントを記述 app/models/user.rb class User < ApplicationRecord . . . mount_uploader :avatar, AvatarUploader . . . ユーザー編集画面で画像変更を可能にする 初期画像default_user.jpgをapp/assets/imagesディレクトリ下に用意しておく。 ├ app/ ├ assets/ ├ images/                      └ default_user.jpg ユーザー編集のビューを変更する。 app/views/users/edit.html.erb . . . <%= form_with(model: @user, local: true) do |f| %> . . . <% if @user.avatar? %> <%= image_tag @user.avatar.url, width: "100px", height: "100px", class:"icon"%> <% else%> <%= image_tag "default_user.jpeg", width: "100px", height: "100px", class:"icon"%> <% end %> <%= f.label :avatar, "Icon change" %> <%= f.file_field :avatar, class: 'form-control' %> <%= f.submit "Save changes", class: "btn btn-primary" %> <% end %> </div> </div> 以下のようになっていれば成功。 画像を表示する gravatarで表示していたビューをimage_tagに変更していく。 画像を持たないユーザーにはデフォルトの画像を表示させる。 編集するビューは以下。 views/microposts/micropost.html.erb views/shared/_user_info.html.erb views/users/_user.html.erb views/users/edit.html.erb views/users/show_follow.html.erb views/users/show.html.erb 例えば views/microposts/micropost.html.erb <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> を views/microposts/micropost.html.erb <% if micropost.user.avatar? %> <%= link_to image_tag(micropost.user.avatar.url, width: "50px", height: "50px", class:"icon"),micropost.user%> <% else%> <%= link_to image_tag("default_user.jpeg", width: "50px", height: "50px", class:"icon"),micropost.user%> <% end %> というように変更する。 最後にcssを変更する。 自分は画像を丸く表示させたかったので以下のように記述。 app/assets/stylesheets/custom.scss . . . .icon { border-radius: 50%; object-fit: cover; float: left; margin-right: 10px; } 以下のようになっていれば成功。 本番環境でAWS S3にアップロードするよう設定 現在の設定ではアップロードされた画像はpublic/uploads/user/avatarに追加されていくが、本番環境のみRialsチュートリアル第6版13章で作成したAWS S3にアップロードするように設定していく。 Gemfileにfog-awsを追加 gem 'fog-aws' $ bundle install avatar_uploader.rbの設定を変更 今回は画像のみ受け付けたいのでdef extension_allowlistも記述したが、iPhoneで撮影した写真は拡張子がheicであることも多いため、jpgなどに変換する機能を検討中である。 app/uploaders/avatar_uploader.rb . . . if Rails.env.development? storage :file elsif Rails.env.test? storage :file else storage :fog end . . . def extension_allowlist %w(jpg jpeg png) end end チュートリアルにおいて、下記の環境変数をHerokuに設定済みなので、そちらを利用していく。 もしまだなら、 $ heroku config:add で追加していく。 config/initializers配下に、carrierwave.rbを作成し、設定を記述していく。 config/initializers/carrierwave.rb require 'carrierwave/storage/abstract' require 'carrierwave/storage/file' require 'carrierwave/storage/fog' if Rails.env.production? CarrierWave.configure do |config| config.fog_provider = 'fog/aws' config.fog_directory = ENV['AWS_BUCKET'] config.fog_credentials = { provider: 'AWS', region: ENV['AWS_REGION'], aws_access_key_id: ENV['AWS_ACCESS_KEY'], aws_secret_access_key: ENV['AWS_SECRET_KEY'], path_style: true } end end AWSを開き、Amazon S3→バケット→アクセス許可と進み、ブロックパブリックアクセス (バケット設定)を以下のように設定する。 バケットポリシーに以下のように記述 { "Version": "2012-10-17", "Statement": [ { "Sid": "statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::(AWSアカウントID):user/(IAMユーザー名)" }, "Action": "*", "Resource": "arn:aws:s3:::(バケット名)" } ] }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】早期リターンと倒置if

早期リターン 式の値を戻り値としてメソッドの実行を終了します。式が省略された場合には nil を戻り値とします。 引用:https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#return def greeting(flag) 'good evening' return 'good night' if flag false end greeting(true) # => "good evening" greeting(false) # => false 倒置if 条件文がfalseのときnilを返す def greeting(flag) 'good evening' 'good night' if flag end greeting(true) # => "good evening" greeting(false) # => nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Ajaxでデータを渡す際のCSRFエラー【jQuery】

やりたいこと $.ajaxでデータをRails側に渡して保存したい。 function hoge(){ $.ajax({ type: 'POST', // リクエストのタイプ url: '/hoge', // リクエストを送信するURL data: { hoge: { hoge: hogehoge } }, // サーバーに送信するデータ dataType: 'json' // サーバーから返却される型 }) } エラー内容 422 (Unprocessable Entity) Can't verify CSRF token authenticity. 原因 RailsにはCSRFという脆弱性の対策が備わっている。 Railsが提供するformなどのヘルパーを使うと自動でセキュリティトークンが設定されるが$.ajaxでは自分で設定しなければならない。 解決策 CSRF対策を無効にするという方法もあるが、通信の際にセキュリティトークンを設定すればよい。 function set_csrftoken() { $.ajaxPrefilter(function (options, originalOptions, jqXHR) { if (!options.crossDomain) { const token = $('meta[name="csrf-token"]').attr('content'); if (token) { return jqXHR.setRequestHeader('X-CSRF-Token', token); } } }); } function hoge(){ // ajax通信条件にCSRFトークンを入れる set_csrftoken() $.ajax({ type: 'POST', // リクエストのタイプ url: '/hoge', // リクエストを送信するURL data: { hoge: { hoge: hogehoge } }, // サーバーに送信するデータ dataType: 'json' // サーバーから返却される型 }) } 参考 RailsのCSRF対策について - Qiita 【Rails6】jQueryのAjax通信を行う方法(CSRF対策付き) | サシミスタジオ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails ロールバックの手順

はじめに 自分の学びのアウトプットのため、こちらの記事を参考にさせていただきながら書きました。 【Rails】$rails db:rollbackしたい時の間違えない手順 手順 1.マイグレートのバージョンを確認 コンソール $ rails db:version Current version: 20211119053634 2.ロールバックを実行 コンソール #一つ前のバージョンに戻す $ rails db:rollback コンソール #指定したバージョンまでなかったことにする $ rails db:rollback STEP=◯◯○◯◯○◯◯○ 3.migrateされる前の状態に戻っているか確認 コンソール $ rails db:abort_if_pending_migrations You have 1 pending migrations: 20211119020307 AddAutoScalingSupportedToWebServers 20211119053634 AddWebServersComment 4.マイグレーションファイルを修正 5.migrateを実行
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

投稿モデル単体テストコードの記述

前回に続いて、サンプルアプリの投稿機能、Eatモデルの単体テストコードを実装します! ①.テストコードを記述するファイルを作成 まず、Eatモデル単体テストコードを記述するための、ファイルを生成します! 以下のコマンドを実行します! ターミナル % rails g rspec:model eat 以下のように、テストコードを記述するためのファイルeat_spec.rbと、FactoryBotを記述するためのファイルeats.rbが生成されます! spec L factories L eats.rb L users.rb models L eat_spec.rb L user_spec.rb ②.FactoryBotを準備 続いて、Eatモデルのインスタンスを生成するFactoryBotを設定します! eats.rbに、以下のように記述します! spec/factories/eats.rb FactoryBot.define do factory :eat do text {Faker::Lorem.sentence} image {Faker::Lorem.sentence} association :user end end 5行目にassociation :userという記述があります! これはusers.rbのFactoryBotとアソシエーションがあることを意味しています! つまり、Eatのインスタンスが生成したと同時に、関連するUserのインスタンスも生成されます! Eatに対しては、必ずUserが紐付いている必要があるため、このように記述する必要があります! (UserはEatを必ず持っているわけではないため、users.rbには記述しません。) ③.exampleを整理 Eatモデルで検討すべきexampleを整理します! まず、投稿機能の仕様が、テキストと画像を投稿する機能であることを把握します! 次に、どのようなときに新規投稿できないのかを、Eatモデルのバリデーションを参考にして考えます! Eatモデルのバリデーションは、以下のようになっています! app/models/eat.rb class Eat < ApplicationRecord validates :text, presence: true belongs_to :user has_many :comments def self.search(search) if search!="" Eat.where('text LIKE(?)', "%#{search}%") else Eat.all end end end textにpresenceのバリデーションが設置されています! さらに、アソシエーションを示すbelongs_to :userには、「EatはUserに属している必要がある」制約が含まれています! 画像にはpresenceのバリデーションが設置されていないため、任意であることが分かります! まとめると、以下のようなexampleを列挙できます! 画像とテキストを投稿できる! テキストのみで投稿できる! テキストが空では投稿できない! ユーザーが紐付いていなければ投稿できない! これらのexampleをテストコードに落とし込みます! バリデーションの記述を見るのは、あくまでexampleの「参考」にするためです! ここから全てを読み取るわけではありません! 異常系テストのexampleはバリデーションから分かることが多いですが、正常系テストのexampleはアプリケーションの仕様も含めて考える必要があります! ④.テストコードを記述 eat_spec.rbを以下のように編集しましょう。 spec/models/eat_spec.rb require 'rails_helper' RSpec.describe Tweet, type: :model do before do eat = FactoryBot.build(:eat) end describe 'イートの保存' do context 'イートが投稿できる場合' do it '画像とテキストを投稿できる' do end it 'テキストのみで投稿できる' do end end context 'イートが投稿できない場合' do it 'テキストが空では投稿できない' do end it 'ユーザーが紐付いていなければ投稿できない' do end end end end イートを投稿できる場合の記述 正常系テストを実装します! 正常系においては、be_validマッチャを用いて、生成したインスタンスが保存できるものであることを確かめます! spec/models/eat_spec.rb require 'rails_helper' RSpec.describe Tweet, type: :model do before do eat = FactoryBot.build(:eat) end describe 'イートの保存' do context 'イートが投稿できる場合' do it '画像とテキストを投稿できる' do expect(@eat).to be_valid end it 'テキストのみで投稿できる' do @eat.image = '' expect(@eat).to be_valid end end context 'イートが投稿できない場合' do it 'テキストが空では投稿できない' do end it 'ユーザーが紐付いていなければ投稿できない' do end end end end 以上のようになります! イートを投稿できない場合の記述 異常系テストを実装します! 異常系においては、バリデーションによって生成されるエラーメッセージが、想定通りの内容になっているかどうかを確かめます! spec/models/eat_spec.rb require 'rails_helper' RSpec.describe Tweet, type: :model do before do eat = FactoryBot.build(:eat) end describe 'イートの保存' do context 'イートが投稿できる場合' do it '画像とテキストを投稿できる' do expect(@eat).to be_valid end it 'テキストのみで投稿できる' do @eat.image = '' expect(@eat).to be_valid end end context 'イートが投稿できない場合' do it 'テキストが空では投稿できない' do @eat.text = '' @eat.valid? expect(@eat.errors.full_messages).to include("Text can't be blank") end it 'ユーザーが紐付いていなければ投稿できない' do @eat.user = nil @eat.valid? expect(@eat.errors.full_messages).to include('User must exist') end end end end 以上のようになります! 最後にテストコードを実行します! 以下のコマンドを実行して確認します! ターミナル % bundle exec rspec spec/models/eat_spec.rb 実行結果が緑色で表示されていればテスト成功です! ⑤.まとめ 内容は前回のUserモデル単体テストとほぼ一緒です! exampleをきちんと整理して行うことが大事だと思いました! 何か説明で間違っていたらご指導お願い致します(_ _)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「ロジックをモデルに移す」よりシンプルなファットコントローラの解消法【個人開発で学んだこと】

記事を読むのにかかる時間 約10分 結論 コントローラの記述が膨れ上がってしまう、いわゆる「ファットコントローラ」について、 最もオーソドックスな解消法として挙げられる「ロジックをモデルに移す」よりシンプルなのは 「テーブル設計を工夫し、そもそもコントローラに何も書かなくて良い状態を作る」 ことだと思います。 今回、個人開発でその体験ができたので共有します。 この記事から得られること 実装の前段階にあたるテーブル設計を工夫することで コードをシンプルにする方法を一つ知ることができる。 必要なRailsの予備知識 ・一般的なCRUDのアクション ・多対多の関連付け(has_many :throughとかbelongs_to)の概要 ・enum 目次 結論 開発したサービス テーブル設計で工夫したこと 詳しく まとめ 最後に 開発したサービス 11/25からAmazonPrimeVideoで配信される「バチェラー・ジャパン シーズン4」の優勝予想ゲームです。 ポイント制のゲームで、「途中で予想を変えると減点だが、予想した人が脱落するともっと減点される」 という減点法を採用しています。 バチェラー・ジャパンとは アメリカの人気恋愛リアリティ番組「The Bachelor(原題)」の日本版。バチェラーとは独身男性の意で、番組では1人の幸運なバチェラーが、一般応募で集まった15人の女性とのグループ・パーティーや2人きりのロマンチックなデートを経て、最終的に自分に最もふさわしい女性1人を選び出す「婚活サバイバルゲーム」。 今回は全4話で、各放送回で徐々に候補者(=番組の女性メンバー)が減っていくイメージです。 ポイント ここでポイントなのは、 以下のユーザーに対して減点が行われる必要があるということです。 ①BETする候補者を変更したユーザー ②管理者が候補者を脱落させたとき、その候補者にBETしていたユーザー テーブル設計で工夫したこと 今回のテーブル設計で唯一意識したのは、ユーザーの得点が 「操作によって変動する」のではなく、「履歴によって算出される」設計にする ということだけです。 以降で詳細の説明をしていきます。 詳しく 「操作によって変動する」とは 今回の処理を普通にやろうとすると、テーブル設計は以下のような感じになるかと思います。 各モデルの関連付けとenumの定義 # 本サービスのユーザー class User < ApplicationRecord belongs_to :candidate end # バチェラーに参加する女性候補者 class Candidate < ApplicationRecord has_many :users # 候補者の生き残り情報 enum :status { active: 0, dropout: 1 } end 一人の候補者が多数のユーザーにBETされるのでこのような関連付けになると思います。 そしてコントローラのロジックは一般的に以下のような感じになると思います。 ①BETする候補者を変更したユーザーへの減点(-10点) users_controller.rb class UsersController < ApplicationController # (中略) def update @user = User.find(params[:id]) @user.assign_attributes(user_params) if @user.save @user.points -= 10 # 減点処理 redirect_to ... else render ... end end private def user_params require(:user).permit(..., :candidate_id) end end ②管理者が候補者を脱落させたとき、その候補者にBETしていたユーザーへの減点(-20点) admin/candidates_controller.rb class Admin::CandidatesController < ApplicationController # (中略) def update @candidate = Candidate.find(params[:id]) @candidate.dropout! @candidate.users.each {|user| user.points -= 20 } # 減点処理 redirect_to ... end # (中略) end 上記の①はユーザーの「操作」、②は管理者の「操作」によって Userのpointsカラムの値が「変動」する設計であることが分かると思います。 これが「操作によって変動する」設計です。 減点処理は一行で記述できており、シンプルなCRUDの記述に近いといえば近いのですが、  ・一つのアクションに二つの関心事がある   (①はuser.candidateとuser.points、②はcandidate.statusとuser.points)  ・②はCandidateのコントローラなのにUserに関する処理が記述されている という点で、可読性に改善の余地があると考えられます。 どんなにロジックをモデルに移したとしても、最低一行はコントローラを肥やすことになるのです。 「履歴によって算出される」とは 一方、今回採用したテーブル設計はこんな感じです。 各モデルの関連付け class User < ApplicationRecord has_many :bettings has_many :candidates, through: :bettings end # UserとCandidateの中間モデル class Betting < ApplicationRecord belongs_to :user belongs_to :candidate end class Candidate < ApplicationRecord has_many :bettings has_many :users, through: :bettings has_and_belongs_to_many :episodes end # 番組の放送回(今回は全4回なので、全部で4つのインスタンスが存在) class Episodes < ApplicationRecord has_and_belongs_to_many :candidates end ※has_and_belongs_to_manyを使ったことがない人は、 ここではhas_many :throughと同じ多対多の簡易版と理解していればOKです。 さっきと違う大きなポイントが三つあります。 一つ目はCandidateのstatusカラムを廃止し、Episodeテーブルを採用している点です。 これは、active or dropoutだけでなく、いつまでactiveだったのか?の 「履歴」まで残すためです。 候補者は自身が出演する放送回を所有し、 逆に放送回は各回に出演する候補者を所有するイメージです。 (例えばある候補者が2つのEpisodeインスタンスを所有している場合、その候補者は 2話までは出演していたがそこで脱落し、3話には出られなかったということを表します) 二つ目はUserとCandidateを一対多から多対多に変更している点です。 これは単純にどのユーザーがどの候補者にBETしているかだけでなく、その履歴まで明確に残すためです。 詳しくは後述します。 三つ目はUserモデルのpointsカラムを廃止したことです。 ここも詳しくは後述しますが、結論からいうとpointsはカラムではなく モデルのインスタンスメソッドとしました。 こうすると、コントローラは以下のようになります。 bettings_controller.rb class BettingsController < ApplicationController # (中略) def create @betting = Betting.new(betting_params) if @betting.save redirect_to ... else render ... end end # (中略) end admin/episodes_controller.rb class Admin::EpisodesController < ApplicationController # (中略) def update @episode = Episode.find(params[:id]) @episode.assign_attributes(episode_params) if @episode.save redirect to ... else render ... end end private def episode_params params.require(:episode).permit(..., candidate_ids: []) end end 完全にCRUDに関する記述だけになったと思います。 BETについては、先ほどはupdateだったのが今回はcreateに変わっている点に注目してください。 BETは変更するのではなく履歴を累積するという考え方にシフトしています。 Admin::EpisodesControllerにおいても、「各放送回にどの候補者が出演したか」を履歴として残しているだけです。 管理画面はこんな感じで、次の出演が決まった候補者にチェックを入れてUpdateするだけでOKです。 つまりコントローラで行われているのはあくまで履歴の累積だけ、ということになります。 では、肝心の減点処理はどこで行うのか? これは先ほど少し触れたUserモデルのインスタンスメソッドで行っています。 user.rb class User < ApplicationRecord # (中略) def points points = 100 # BETの変更(2回目以降のBET)に対する減点 points -= (bettings.count - 1) * 10 #脱落した候補者にBETしていることに対する減点(1話時点) points -= 20 if current_candidate.episodes.count < 2 #脱落した候補者にBETしていることに対する減点(2話時点) points -= 20 if current_candidate.episodes.count < 3 #脱落した候補者にBETしていることに対する減点(3話時点) points -= 20 if current_candidate.episodes.count < 4 return points end private def current_candidate # 最後にBETした候補者 bettings.order(created_at: :desc).first.candidate end end こうすることで、まるでpointsカラムを呼び出すかのようにuser.pointsでユーザーの得点が取得できます。 また、ここでは得点が変動している訳ではなく、 あくまで履歴に応じて算出されているだけ、ということが分かると思います。 これが「履歴によって算出される」設計です。 まとめると、  ・コントローラはあくまで「起こった事実の履歴(ユーザーのBETと候補者の脱落)」を残すだけ。  ・あとはUserモデルのpointsメソッドが、その履歴を参照して得点を算出する。 という設計になっています。 「履歴によって算出される」設計のメリット これは単純にコントローラがすっきりするだけではないと思っています。 例えば、BETした候補者が脱落したときに発生する減点を -20点から-30点に変更したくなったとき、 この設計であればpointsメソッドを数行書き換えるだけで済みます。 (逆に「操作によって変動する」設計だとかなり面倒になることは容易に想像できます) 管理者が候補者の出演/脱落を間違えてインプットしてしまったときも同様です。 先ほどお見せしたように、Episodeインスタンスは管理画面で何度でも修正ができます。 ユーザーのBET履歴が残っているので、各ユーザーが 「いつ何が原因で減点されたか」を把握するのも簡単です。 「履歴によって算出される」設計は、可読性だけでなく 保守性やトレーサビリティにも寄与しているといえるでしょう。 まとめ ・ファットコントローラを解消する手段として、一般的にはコントローラのロジックをモデルに  移行することが最適とされているが、必要なエンティティとその履歴があれば、  そもそもコントローラには何も書かなくていい場合もある。 ・何らかの変動するデータを取り扱いたいとき、  ユーザーや管理者の「操作」によってそのデータを「変動」させるのではなく、  累積された「履歴」を参照しながら都度データを「算出」する設計にすることで、  可読性や保守性、トレーサビリティが向上する。 最後に 記事の分かりにくい箇所や過不足、誤りなどあればコメントいただけると幸いです。 記事のコードは分かりやすさ重視のため、実際のコードとは異なる部分があります。 本サービスのコードを詳しく知りたい場合はGitHubをご覧ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む