13
3

More than 3 years have passed since last update.

【Rails】1対1のアソシエーションにおけるネストされたリソース設計について

Last updated at Posted at 2021-01-22

はじめに

リソース設計を考えるときに悩んだ結果をまとめました。
コード例はRailsです。

忙しい人のための要約

PUT questions/:id/answer

でupsertする。

DB構造

Untitled Diagram.png

questionsテーブルとanswerテーブルが1対1の関係です。
answerがquestionに紐づくので、外部キーはanswersに持たせます。

モデル

question.rb
class Question < ApplicationRecord
  has_one :answer, dependent: :destroy
end
answer.rb
class Answer < ApplicationRecord
  belongs_to :question
end

特に言うことのないシンプルな形です。

リソースどうしよう?

さて、「ある質問に対する回答を追加する場合」を考えました。
オーソドックスな1対多の場合はこんな感じになるはずです。

POST questions/:id/answers

では、「更新」は?

PATCH questions/:question_id/answers/:id

しかし、これを1対1で想定すると違和感があります。

  • 「ある質問に対する回答」は一つしかないのに、question_idanswer_idをリクエストで送るのが冗長
  • 一度回答を作成したら、同じ質問に対して回答を新規作成することはない

「ある質問の回答」を追加することと更新することは同等の意味を持つはずです。

HTTP の PUT リクエストメソッドは、新しいリソースを作成するか、指定したリソースの表現をリクエストのペイロードで置き換えます。
PUT - HTTP | MDN

ということなので、Railsのupdateメソッドに相当します。

PUT questions/:id/answer

というリクエストで回答の値を更新することにします。

既にanswerが存在しているときに、新たなanswerをnewとかcreateすること自体がおかしいです。
question1のanswerは一つしかないので、newとedit、createとupdateは等価といえます。
なので、単一リソースのときはedit/updateで作成または更新の行動をしたい。
POST, PATCHを使い分けるためには、リクエスト(URL)がanswerの有無を知る必要があるので、違和感があります。URLがそこまで責任を持つべきではありません。

URLが
:no_good:「question1のanswerを新しくこの値で作成してくれ」とか「question1のanswerがあるはずだから、この値に書き換えてくれ」
:ok_woman:「answerがあるかどうかは知らんけど、question1のanswerの値をこうしてくれ」
ってお願いして、それに答えるのが自然です。

ルーティング

routes.rb
resources :questions do
  resource :answer, only: %i[edit update show]
end

コントローラ

#find_or_initialize_byという便利なメソッドがあるので、それを使うだけで他は普通のCRUDの場合と変わりません。

answers_controller.rb
class AnswersController < ApplicationController
  before_action :set_question

  def show
    @answer = @question.answer
  end

  def edit
    @answer = Answer.find_or_initialize_by(question_id: @question.id)
  end

  def update
    @answer = Answer.find_or_initialize_by(question_id: @question.id)
    if @answer.update(answer_params)
      redirect_to question_answer_path(@question, @answer)
    else
      render :edit
    end
  end

  private

  def set_question
    @question = Question.find(params[:question_id])
  end

  def answer_params
    params.require(:answer).permit(:text)
  end
end

終わりに

という考えでこういうリソース設計にしたのですが、補足やオススメの書籍などありましたらコメントをいただけるとありがたいです。

13
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
3