20211124のRubyに関する記事は8件です。

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の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で続きを読む

【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で続きを読む

日本における転職活動についてのユーザーインタビューに外国人ITエンジニア大募集中!

[日本語] ※English Below Jellyfishについて 東京に拠点を置くJellyfishは、技術系の人材紹介会社です。私たちのモットーは、"Expand Your Horizon "であり、海外のIT人材が日本で夢のある仕事を見つけられるようサポートしています。 会社概要 Jellyfishは、オムニチャネル・マーケティング手法を採用戦略に応用し、柔軟性をビジネスの中核に据えています。これまでの求職者の声を聞くと、転職活動の方法やプラットフォームが多様化しており、適切なタイミングで適切な仕事を持つエージェントにアプローチすることが難しくなっています。 これは海外のITエンジニアだけでなく、日本のエンジニアにも当てはまります。タッチポイントを明確に理解し、ITエンジニアがタイムリーに良い仕事を見つけられるようにするために、皆さんの洞察力、経験、提案をもっと知りたいと思っています。 インタビュー内容 ・インタビューは、日本語、英語、韓国語、中国語、ベトナム語のいずれかの言語で行われます。 ・面接時間は45分~1時間、時間帯は9:00~19:00(平日のみ)です。 ・日本在住の外国人ITエンジニアで、日本での勤務経験が2年以上ある方を対象としています。 面接の流れ 企画書の提出を受けて、こちらからご連絡いたします。なお、ボリュームの関係上、お寄せいただいたすべてのご提案にお応えできない場合がありますので、あらかじめご了承ください。 報酬額 Amazon Card 3,000円 ご興味のある方は、以下の情報を添えてh-thu@jellyfish-g.co.jpまでにご応募ください。 ・氏名 ・年齢 ・現在の居住地(日本または海外) ・最終学歴 ・日本語レベル ・職務経験 ・日本での実務経験 ・転職経験の有無 ・現在のポジション ・開発言語 その他、ご不明な点がございましたら、お気軽にお問い合わせください。チャットを楽しみにしています。 [ENGLISH] About us Located in Tokyo, Japan, Jellyfish is a tech recruiting agency. Our motto is "Expand Your Horizon", supporting foreign IT talents to find their dream jobs in Japan. Overview Applying the omnichannel marketing method to our recruiting strategy, Jellyfish embeds flexibility at the core of its business. As we have heard the voice from our previous candidates about how diverse the methods/platforms they are using to find new jobs, it’s difficult to reach out to agencies with the right job at the right time. This does not apply only to foreign IT engineers, but also to Japanese engineers. In order to clearly understand the touchpoint and help IT engineers find a good job timely, we would be appreciated to know more about your insights, experiences, and suggestions. Interview details ・The interview is available in one of the following languages Japanese, English, Korean, Chinese, Vietnamese ・The interview should last 45 minutes to 1 hour within the timeframe of 9:00 ~ 19:00 (JST) (weekdays only) ・We are looking for foreign IT engineers living in Japan, having worked in the country for at least 2 years. Interview flow We will contact you once we have received your proposal submission. Please understand that due to volume, we might not be able to respond to all proposals submitted. Compensation rate Amazon Gift Card 3,000 yen Should you be interested, please submit your proposal to h-thu@jellyfish-g.co.jp with the following information ・Name ・Age ・Current location (Japan or Overseas) ・Final education ・Japanese level ・Work Experience ・Work experience in Japan ・Number of times you have changed jobs ・Current Position ・Development language Should you have any other questions, please feel free to contact us. Looking forward to our chat!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyプログラミング問題にチャレンジ! suzuki_mar

この記事は、 @jnchitoさんのアドベントカレンダーのRubyプログラミング問題にチャレンジ!の12月5日の担当分です。 詳しいレギュレーションは、問題のGitHubのURLを参照してください。 PRのURL ロジックの解説 用語 名前 英語 説明 マス square 点字の中の一つの点 点字 Tenji マスが一つの組み合わせになったもの 点字の組み合わせで文字に対応している マーク mark 点字を組み合わせた単語(文字列) 点字のルール 点字は、縦3点、横2点の6点の組み合わせで作られています。そして、この単位をマスと言います。 下の図をみてください。この図の①②④の点を組み合わせて母音を表し③⑤⑥の点を組み合わせて子音を表します。 引用元 全視情協 クラス説明 TenjiMaker 今回の問題のmain関数である、TenjiMaker.to_tenji(text)を定義しています。 可読性を高くするために抽象度が高く理解容易性が高くなるようにしています。 自分は点字を一文字単位で扱っているため点字が複数ある、TenjisMakerとしたほうが自然だと思いますが、問題のルール上単数形のTenjiMakerになっています Tenji クラスの説明 点字を表しているクラスです。 このクラスは表示する文字列 "○" とかと関係性をもっていないので、 例えば出力をコンソール(CLI)ではなくて、ブラウザ(HTML)に変更する仕様追加が発生した場合も柔軟に対応できると思います。 や行について や行は特殊なルールがありますが、そのルールはや行だけなのでロジックを実装するとわかりづらくなったり、バグが発生する可能性があるためハードコーディングをしています。 MarkBuilder 渡された点字をもとに表示するマークを生成します。 このクラスはマークを生成することに特化しているクラスなので、状態を持たないイミュータブルなクラスにしています。 (Railsでいうサービスクラスです) テストのメソッド名について 自分は英語が苦手なのと、このコードを見る人の殆どは日本人だと思います。 そのため、テストのメソッド名については日本語で補足を書いています。 コードのアピールポイント 頑張ったところ マーク(コマンドで表示する文字列)を生成する部分が点字を複数組み合わせて行の文字列を生成するので、直感的な仕様ではないです そのため実装するのが少しだけ大変でした 苦労したところ MiniTestをはじめて扱うのでキャッチアップやRspecとの書き方の違いが少しだけわかりづらかったことが苦労しました 工夫したところ コードとこのドキュメントがあれば、点字について理解できるように意識しました 自慢したいところ 拡張性もあると思うのでもし今後、複雑な点字のルールを実装することになっても実装しやすいのではないかと思います 点字についてわからないエンジニアでもコードを理解しやすいのではないかと思います 点字のルールはTenjiクラスをみればわかると思います 表示する文字列(マーク)についてはMarkBuilderをみればわかると思います 伊藤さんにメッセージ 点字については全くの知識がなかったのですが、今回の実装した範囲内であれば説明できるようになったので、少しだけ点字が身近に感じました 著名な方にレビューをしてもらうのはじめてですので緊張していますw
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Ruby の Array.pack('p') で 'can't modify frozen String' が出たら、バージョンを上げるか、解凍が必要

Ruby で pack をしたら、 can't modify frozen String が出たので、記録する。 [47] pry(main)> str = 'abc' => "abc" [48] pry(main)> str.freeze => "abc" [49] pry(main)> [str].pack('p*') RuntimeError: can't modify frozen String from (pry):48:in `pack' pack は変更(modify)ではない、エラーが出るのが不思議だった。 そこで思いついたのは、「ポインタを渡すと、破壊できるから」という理由。 本当にそうなのかを調べようと思ったが、 2.6.3 まではエラーだが、 2.7.3 からはエラーにならなくなっていることに気づいた。 下記のコードを tmp.rb として保存し、手元にある version を切り替えながらテストした。 # tmp.rb # frozen_string_literal: true puts RUBY_VERSION ['abc'].pack('p*') $ rbenv shell 2.3.3 $ ruby tmp.rb 2.3.3 tmp.rb:5:in `pack': can't modify frozen String (RuntimeError) from tmp.rb:5:in `<main>' $ rbenv shell 2.5.0 $ ruby tmp.rb 2.5.0 Traceback (most recent call last): 1: from tmp.rb:5:in `<main>' tmp.rb:5:in `pack': can't modify frozen String (FrozenError) $ rbenv shell 2.6.3 $ ruby tmp.rb 2.6.3 Traceback (most recent call last): 1: from tmp.rb:5:in `<main>' tmp.rb:5:in `pack': can't modify frozen String (FrozenError) $ rbenv shell 2.7.3 $ ruby tmp.rb 2.7.3 $ rbenv shell 3.0.1 $ ruby tmp.rb 3.0.1 ポインタが関係なかったのは残念。 バージョンを上げれば解決する。 バージョンを上げずに対応する方法は、解凍すること。 [56] pry(main)> [str].map(&:+@).pack('p*') => "\xD8\xC6\b\x04\x00\x00\x00\x00"
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む