Railsのきれいなコードのお題案 · Issue #1 · clean-rails-ja/conversation からの転載。
例
ブログにカテゴリーを複数つけて投稿する機能。
はてなブログ の投稿画面がイメージに近いです。
おそらく、モデルは下記のように複数のモデルになると思います。
# app/models/article.rb
class Article < ApplicationRecord
has_many :categories
end
# app/models/category.rb
class Category < ApplicationRecord
belongs_to :article
end
実装方法の例
下記のような方法があると思いますが、それぞれメリット・デメリットがあると思う。
私が知らないだけで、他にも良い実装方法はあるかも。
「いいね!」 1
[WIP]Model と画面上の form が1対1で一致しない場合、どのように実装するのが綺麗なのか? · Issue #2 · clean-rails-ja/conversation からの転載
この例、厳密に考えるとArticleとCategoryは多対多な気がするけど、面倒なのでいったん1対多で考えますね。
accepts_nested_attributes_for の例
class Article < ApplicationRecord
has_many :categories
accepts_nested_attributes_for :categories, allow_destroy: true, reject_if: :all_blank
end
class Category < ApplicationRecord
belongs_to :article
end
class ArticlesController < ApplicationController
# ...
def create
# ...
end
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to article_path(@article), notice: '更新しました'
else
render :edit
end
end
def article_params
params.require(:article).permit(:title, :body, categories_attributes: [:id, :name, :_destroy])
end
end
<%= form_with @article do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body %>
<%= f.field_for :categories |g| %>
<%= g.text_field :name %>
削除する <%= g.check_box :_destroy %>
<% end %>
<% f.submit %>
<% end %>
メリット
デメリット
覚えづらい、ハマりやすいインターフェース
reject_if
と_destroy
accepts_nested_attributes_for
を定義するとfields_forの挙動が変わる
name属性がnested attributes仕様に変わる
モデルの定義とビューの定義が一体化される
レールから外れたいときに大変
保存時にbelongs_toのバリデーションが走ってN+1になってしまう、みたいなケースがあった気がする
nested attributes嫌いな人の意見も聞いてみたいですね。僕はわかって使う分にはいいんじゃないのか派。
↓こういうバッドノウハウができてしまうのはちょっと微妙だなという気はするけども。
nested attributes なレコードを、特定の属性が空の時に削除する - おもしろwebサービス開発日記
ちょっとトピックから外れますが、上記のような場合は_destroy: 1を設定するのではなく、mark_for_destructionを使うのが好みであったりはします(起きる現象は同じですが_desrtroy: 1は隠し属性的でよろしくないなーと)
oakbow
2018 年 4 月 15 日午後 6:28
6
基本方針は@elcondorさんと同じかなあ。
ただ、フォームオブジェクトを使う場合、私はサービスと併用したりしています。
フォームオブジェクトにはあくまでユーザ入力の取り扱いのみ(具体的にはヴァリデーションとかその辺)をお願いし、その後のビジネスロジックはサービスで実装する、という形を取っていたり。
いつもそうするというわけではありませんし、フォームオブジェクトで通常のモデルでやるように振る舞いまで実装してしまっても構わないかなとは思います。
同様の処理をフォームオブジェクトを経由せずに行う処理(例えばバッチ処理やAPI経由の処理)にしたい場合、フォームオブジェクトを共有するのは違和感があるので、ビジネスロジック(サービス)とビューに依存する処理(フォームオブジェクト)に分けてる感じです。
この辺みんなどうしてるのかな?
tkawa
2018 年 4 月 16 日午前 4:12
7
単純な複数選択の場合には、こういうのも好きです。
# class Article
def category_names
categories.map(&:name)
end
def category_names=(names)
self.categories = names.select(&:present?).map { |name| Category.find_or_initialize_by(name: name) }
end
ビューはselect2みたいな感じで。category_names[]
を複数送ればOK。
accepts_nested_attributesは滅びるべきAPIだと思ってるので、自分なら絶対使いません。
大きな理由は2点かな。
あれはActiveRecordのAPIでPOROに使えないので、ARのカラム構造と現実が合わなくなった時や付随する処理でPOROが必要になった時に調整で苦労する可能性が高い
ReactやVueの様なJSON構造と表示のbindingと相性が悪い(名前付けのルールとかdestroyフラグとか)
この手の複数選択系のフォームはまともなUIで作るならJSでの項目の増減や個別の編集フォーム等はほぼ必須で、今時そういうUIをRailsのform helperをベースに作ることはほぼ無いので、記述削減のメリットが無い。
基本はFormオブジェクトで、シンプルに一要素だけでマッピング可能であるならtkawaさんのスタイルも使いそうです。
「いいね!」 1
本当にこういうAPIが出てきたら面白そうだなぁと思っています
「いいね!」 1
accepts_nested_attributes_for
この辺のコメントを読む限り新規に開発する部分ではあまり使いたくないですね(バージョン上げる際の負債になりそうなので)
dhh on 15 Nov 2016 Owner
I’d actually like to kill accepts_nested_attributes_for in due time. Don’t think we should promote it for this new API. Rather, let’s just show how to do it by hand in the controller.
https://github.com/rails/rails/pull/26976#discussion_r87855694
「いいね!」 3
Formオブジェクトにするのは僕も賛成なのですが、お仕事だとFormオブジェクトの作り方を伝授する暇がなかったり、POROのFormオブジェクトだと記述量がそれなりに増えてしまうという理由でaccepts_nested_attributes_for
を許容する機会がわりとあります。
Formオブジェクトで実装する場合、POROで実装しますか?それともなんらかのライブラリを使います?
同じく ActiveModel ですね…最近はこんなクラスを用意して使ってます。
class ApplicationModel
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations
end
「いいね!」 1
同じく ActiveModel で、
class ApplicationModel
これを個人的に「ApplicationModel パターン」と呼んでいますw
$ bin/rails g model HogeForm --parent ApplicationModel
ってするとモデルが生成できるので便利(Qiita か何かに書こうかな…)
あと、ActiveModel にすることで AR::Base なクラスとインターフェイスが揃うのが良いですね
「いいね!」 3