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

KaigiOnRails2024

 KaigiOnRails2024

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

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