Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KaigiOnRails2024

 KaigiOnRails2024

Railsの仕組みを理解してモデルを上手に育てる
- モデルを見つける、モデルを分割する良いタイミング -

Avatar for Kuniaki IGARASHI

Kuniaki IGARASHI

October 24, 2024
Tweet

More Decks by Kuniaki IGARASHI

Other Decks in Technology

Transcript

  1. 自己紹介 五十嵐邦明(igaiga) ガーネットテック373株式会社 代表取締役 フリーランスのRailsエンジニア プログラミングスクール「フィヨルドブートキャンプ」顧問 著書 ゼロからわかる Ruby超入門 Railsの教科書

    パーフェクトRuby on Rails[増補改訂版] RubyとRailsの学習ガイド Railsの練習帳 ほか 謝辞 応援してくれている妻、子(1歳)、家族に感謝します
  2. ファミレスメニューでのテーブル設計例 「メインとなるテーブル名」 を名詞で出す メニューは「注文するための道具」なので 「注 文(orders)」 をメインとなるテーブルにします 「誰が・何が」 を考える 「誰が注文するか」と考えて

    「顧客 (customers)」 テーブルを作ります 「誰を・何を」 を考える 「何を注文するか」と考えて 「商品(items)」 テーブルを作ります 見つけるコツが必要なテーブルの1つ 「イベント型テ ーブル(モデル)」 をこのあと説明します
  3. 例: 入荷処理でのイベント型モデル 入荷処理は以下の仕様だとします 在庫(Stockモデル)を増やす 支払った代金を銀行口座(BankAccountモデル)から減らす StockモデルとBankAccountモデルしか見つけられていないとき どちらのモデルに入荷処理を書くか迷う イベント型モデル 「Arrival(入荷)モデル」 をつくる

    Arrivalモデルにreceivedメソッドをつくる 「在庫を増やす」 「支払った代金を銀行口座から減らす」 をする 複数モデルで実装場所を迷った処理を、イベント型モデルに書けて嬉しい 実装に適切な責務のモデルとして、イベント型モデルがみつけられることがある
  4. イベント型モデルに関する参考資料 諸橋さん Kaigi on Rails 2023講演 「Simplicity on Rails --

    RDB, REST and Ruby」 https://speakerdeck.com/moro/simplicity-on-rails-rdb-rest-and-ruby yasaichiさん、t-wadaさん podcast 「texta.fm」 https://open.spotify.com/show/2BdZHve9cIU6c8OFyz7LeB 次はPOROをつかってモデルを育てていく方法を話します
  5. PORO(Plain Old Ruby Object)をつくる PORO(Plain Old Ruby Object): 何も継承していないただのクラスのこと ActiveRecordを継承せず、テーブルとも結びつかない

    POROをつくると嬉しいときの例 責務がしっくりくるモデルがつくれないとき イベント型モデルをがんばって探しておくことも大切 テーブルに保存しなくても良い、モデルぽいオブジェクトを発見したとき 例: 別アプリのAPIに問い合わせて取得したオブジェクト 集計して得られた結果のオブジェクト Redisやsessionなどに一時保存しておけば良いオブジェクト POROファイルの置き場所はモデルと同じくapp/models以下に置くのがお勧め ActiveRecordを継承しないが、ビジネスロジック置き場なのでモデルの仲間
  6. フォームオブジェクトのつくりかた Railsが提供する ActiveModel::Model, ActiveModel::Attributes をつかう ActiveModel::Attributes 型(cast type)を指定したattributesをつくれる ActiveModel::Model form_withに渡せたり、validationできたり、newメソッドでattributesと一緒に

    初期化できたり、など、モデルのように振る舞える Rails7.0以降ではかわりに ActiveModel::API をつかうこともできます 違い: ActiveModel::Model = ActiveModel::API + ActiveModel::Access ActiveModel::Accessにはsliceメソッドとvalues_atメソッドがあります どちらをつかうのが良いのかご存知の方、教えてください サンプルコード: https://github.com/igaiga/rails_form_object_sample_app 別案としてはYAAF Gemでつくる方法も https://github.com/rootstrap/yaaf
  7. ActiveModel::Modelモジュール モデルのように振る舞う機能各種をつかえるようになる バリデーションを設定して実行できる機能 form_withとやりとりする機能 newメソッドでattributesと一緒に初期化する機能 ほか FooFormObject.new(name: "iga", email: "[email protected]",

    terms_of_service: true) class FooFormObject include ActiveModel::Model include ActiveModel::Attributes attribute :name, :string attribute :email, :string attribute :terms_of_service, :boolean validates :name, presence: true validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } # URI::MailTo::EMAIL_REGEXPはRubyに定義されてるemail検証正規表現 validates :terms_of_service, acceptance: { allow_nil: false } # acceptanceはチェックボックス確認用 https://railsguides.jp/active_record_validations.html#acceptance
  8. Userモデルのattributesとバリデーション app/models/user.rb # DB schema # create_table "users" do |t|

    # t.string "name", null: false # t.string "email" # class User < ApplicationRecord validates :name, presence: true validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true end nameは必須 emailはblank可で、フォーマットの確認をする URI::MailTo::EMAIL_REGEXP はRubyに定義されてるemail検証正規表現
  9. フォームオブジェクトをつくる その1 基礎工事 名前は UserNameForm 、置くフォルダは app/forms とします app/forms/user_name_form.rb nameは必須、全ひらがな(=ひらがなだけ)で入力する仕様

    attributesとして attribute :name, :string を持ちます validates :name, format: { with: /\A\p{Hiragana}+\z/ }, presence: true /\A\p{Hiragana}+\z/ は全ひらがなかどうか判定する正規表現 app/forms/user_name_form.rb class UserNameForm include ActiveModel::Model include ActiveModel::Attributes attribute :name, :string validates :name, format: { with: /\A\p{Hiragana}+\z/ }, presence: true end
  10. その2 Userモデルをフォームオブジェクトへ渡す Userモデルに仕事を委譲するために attr_accessor :user で設定取得可能に DB保存機能を委譲するためにUserモデルを設定可能に URLヘルパーで xxx_path(user) のようにつかいたいので取得も可能に

    transfer_attributesメソッドでフォームオブジェクトからモデルへattributesセット app/forms/user_name_form.rb class UserNameForm # ...(略)... attr_accessor :user # フォームオブジェクトからモデルへattributesをセット def transfer_attributes user.name = name end end
  11. その3 saveメソッドをフォームオブジェクトに実装 DB保存機能をsaveメソッドとして実装 モデルと似た書き味を目指します app/forms/user_name_form.rb def save(...) # ... は全引数を引き渡す記法

    transfer_attributes # フォームオブジェクトからモデルへattributesをセット if valid? # フォームオブジェクトのバリデーション実行 user.save(...) # モデルのsaveメソッドへ委譲 else false # これがないとvalid?失敗時にnilが返る end # valid? && user.save(...) # 短く書いても良い end
  12. その4 initializeメソッドをフォームオブジェクトに実装 UserNameForm.new(name_params) フォームからのparamsで初期化する想定 追加実装しなくてもnewメソッドでattributesを設定可能 UserNameForm.new(name: "いがいが") 今回はnewメソッドへattributesのほかにUserモデルも渡したい initializeメソッドをオーバーライドして機能追加 app/forms/user_name_form.rb

    def initialize(model: nil, **attrs) # `**`はキーワード引数をHashで受け取る文法 attrs.symbolize_keys! # StringとSymbolの両対応 if model @user = model attrs = {name: @user.name}.merge(attrs) # attrsがあれば優先 end super(**attrs) # もともとのinitializeメソッドを呼び出し # `**`はHashをキーワード引数で渡す文法 end
  13. その5-2 コントローラの変更 new & createアクション @user_name_form 変数へUserNameFormオブジェクトを代入 リダイレクト先のURL取得を変更 app/controllers/names_controller.rb class

    NamesController < ApplicationController def new @user_name_form = UserNameForm.new(model: User.new) end def create @user_name_form = UserNameForm.new(model: User.new, **name_params) if @user_name_form.save redirect_to user_url(@user_name_form.user), notice: "User was successfully created." else render :new, status: :unprocessable_entity end end end
  14. その5-3 コントローラの変更 edit & updateアクション @user_name_form 変数へUserNameFormオブジェクトを代入 リダイレクト先のURL取得を変更 app/controllers/names_controller.rb class

    NamesController < ApplicationController def edit @user_name_form = UserNameForm.new(model: User.find(params[:id])) end def update @user_name_form = UserNameForm.new(model: User.find(params[:id]), **name_params) if @user_name_form.save redirect_to user_url(@user_name_form.user), notice: "User was successfully updated." else render :edit, status: :unprocessable_entity end end end
  15. その6-1 form_withでフォームオブジェクトをつかう form_withのmodelオプションにはフォームオブジェクトを渡すことにします 変数user_name_formにフォームオブジェクトが入っています app/views/names/_form.html.erb <%= form_with(model: user_name_form) do |form|

    %> 次のエラーが出ます undefined method `user_name_forms_path' for an instance of #<Class:0x...> フォームのリクエスト先パスがわからない旨のエラー 今回はform_withへurl, methodオプションでリクエスト先を指定する方法で対応 他にはidメソッドとpersisted?メソッドとroutesを実装する方法もあります
  16. その6-2 form_withへurl, methodオプションを指定 form_withのurl, methodオプションでリクエスト先を指定 モデルが未保存のときはcreateアクションへ モデルが保存済みのときはupdateアクションへ app/views/names/_form.html.erb <%# 実際は改行なし

    %> <% form_with_options = user_name_form.user.persisted? ? { url: name_path(user_name_form.user), method: :patch } : { url: names_path, method: :post } %> <%= form_with(model: user_name_form, **form_with_options) do |form| %> model.persisted? メソッドはDB保存済みかどうかを判定 **form_with_options の ** はHashをキーワード引数で渡す文法 ビューに書くと読みづらいのでフォームオブジェクトへ移動します
  17. その6-3 form_withのオプションをフォームオブジェクトから取得 app/forms/user_name_form.rb def form_with_options if user.persisted? # update用 {

    url: Rails.application.routes.url_helpers.name_path(user), method: :patch } else # create用 { url: Rails.application.routes.url_helpers.names_path, method: :post } end end Rails.application.routes.url_helpers.names_path ビュー以外でURLヘルパーメソッド(names_pathなど)をつかう方法 app/views/names/_form.html.erb <%= form_with(model: user_name_form, **user_name_form.form_with_options) do |form| %> これで完成です!
  18. フォームオブジェクト最終形 GitHub: https://github.com/igaiga/rails_form_object_sample_app class UserNameForm include ActiveModel::Model # バリデーション機能、form_withに渡せる機能、 #

    new(name: "xxx", ...)のようにattributesとあわせて初期化する機能などを足す include ActiveModel::Attributes # 型を持つattributesをかんたんに定義できるようにする attribute :name, :string # このフォームオブジェクトのバリデーション validates :name, format: { with: /\A\p{Hiragana}+\z/ }, presence: true # DB保存などの機能を委譲するためにUserモデルをセット可能に # redirect_to @user のときなどUserモデルを取りたいので取得もできるようにする attr_accessor :user def initialize(model: nil, **attrs) # `**`はキーワード引数をHashで受け取る文法 attrs.symbolize_keys! # StringとSymbolの両対応 if model @user = model attrs = {name: @user.name}.merge(attrs) # attrsがあれば優先 end super(**attrs) # もともとのinitializeメソッドを呼び出し # `**`はHashをキーワード引数で渡す文法 end def save(...) # ... は全引数を引き渡す記法 transfer_attributes # フォームオブジェクトからモデルへattributesをセット if valid? # フォームオブジェクトのバリデーション実行 user.save(...) # モデルのsaveメソッドへ委譲 else false # これがないとvalid?失敗時にnilが返る end # valid? && user.save(...) # 短く書いても良い end def form_with_options if user.persisted? # update用 { url: Rails.application.routes.url_helpers.name_path(user), method: :patch } else # create用 { url: Rails.application.routes.url_helpers.names_path, method: :post } end end private # フォームオブジェクトからモデルへattributesをセット def transfer_attributes user.name = name end end
  19. 参考資料 諸橋さん 「Simplicity on Rails -- RDB, REST and Ruby」

    https://speakerdeck.com/moro/simplicity-on-rails-rdb-rest-and-ruby yasaichiさん、t-wadaさん 「texta.fm」 https://open.spotify.com/show/2BdZHve9cIU6c8OFyz7LeB yasaichiさん「Ruby on Railsの正体と向き合い方」 https://speakerdeck.com/yasaichi/what-is-ruby-on-rails-and-how-to-deal-with-it 「パーフェクト Ruby on Rails 増補改訂版」      https://gihyo.jp/book/2020/978-4-297-11462-6 「Railsの練習帳」 DBモデリング基礎講座 https://zenn.dev/igaiga/books/rails-practice- note/viewer/rails_db_modeling_workshop フォームオブジェクト https://zenn.dev/igaiga/books/rails-practice- note/viewer/ar_form_object
  20. お仕事募集中! Railsの業務委託の仕事を月1〜6日程リモートで承っています。育成が得意分野です。 著書 パーフェクトRails や Railsの練習帳 などをつかった対話会、読書会、講義 ペアプロ屋、二人三脚での開発技術相談、実装 入社後の研修育成体制を構築して、採用できるエンジニア範囲を増やす コードの健康診断とレポート

    レガシーコード改善実装、RubyとRailsのバージョンアップ実装 今日話した感じで、Railsのいろいろな話題を勉強会やペアプロでお話しします。 ご相談は会場や弊社問い合わせページにて気軽にお声かけください! 仕事内容詳細ページ: https://garnettech373.com/services 問い合わせページ: https://garnettech373.com/contacts