1
1

More than 3 years have passed since last update.

多対多の関係性にフォームオブジェクトなどが絡んで訳わからなくなった話

Posted at

アプリの仕様

  • キャンプの記録をするアプリです
  • ユーザーはキャンプのタイトルとキャンプスタイルという名のタグをつけて投稿します。
  • キャンプタイトルとタグを同時に保存するためにフォームオブジェクトを使用しています。
  • ユーザーは自分の持っているアイテムを登録できます。
  • キャンプには複数のアイテムを持っていける。アイテムは何度でもキャンプに持っていけることからキャンプとアイテムは多対多の関係です。
  • キャンプ記録を作成する際持って行ったアイテムの登録を実現させたい

発生したエラー

undefined method `id' for #<CampTags:0x00007ff6085ac980>
Request
Parameters:

{"authenticity_token"=>"6h75fWC/+zhPx96ZoRyir+kyevpfqLqawP75tlOKrGHOXHlfKtMlG0qvpQMLgrjc5Ld3x1bvduvrtllnJPZagw==",
 "camp_tags"=>{"title"=>"bboobb", "style"=>"無骨キャンプ", "item_ids"=>["1", "2"]},
 "commit"=>"Send"}

状況

  • @item_idsをeachにかけることでitemのidを一つずつ取り出すのは成功しています。
  • campのデータについてはtagと同時に作成されるためFormオブジェクトで保存
  • itemに関しては保存してあったものをselectによって選択する
  • campとtagの情報については登録保存は成功している
  • @camp_id = 3と直球にidを指定してやるとitemとcampの関連付けはできた
  • @campを取り出せれば解決する
  • @campを指定すると:camp_tagsからidを取り出そうとするため@camp.idが無いとエラーになる

@campを確認するとidは保存されているようです。

>> @camp
=> #<CampTags:0x00007ff6085ac980 @errors=#<ActiveModel::Errors:0x00007ff6085ac250 @base=#<CampTags:0x00007ff6085ac980 ...>, @messages={}, @details={}>, @title="bboobb", @style="無骨キャンプ", @item_ids=["1", "2"], @user_id=1, @validation_context=nil, @camp=#<Camp id: 36, user_id: 1, title: "bboobb", created_at: "2021-03-17 06:28:53", updated_at: "2021-03-17 06:28:53">, @tag_id=#<Tag id: 4, style: "無骨キャンプ", created_at: "2021-03-16 09:04:46", updated_at: "2021-03-16 09:04:46">>
>>  

該当するコード

コントローラー

camps_controller.rb
class CampsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_item, only: [:index, :new, :show, :create]
  def index
    @tags = Tag.all
  end

  def new
    @camp = CampTags.new
  end

  def create
    @camp = CampTags.new(camp_params)

    if @camp.valid?
      @tag_list = camp_params[:style].split(/[[:blank:]]+/).select(&:present?)
      @camp.save(@tag_list)
      @camp_id = @camp.id
      @item_ids = @camp.item_ids
      @item_ids.each do |item_id|
        CampItemRelation.create(camp_id: @camp_id, item_id: item_id)
      end
      return redirect_to root_path
    else
      render "new"
    end
  end

  private

  def camp_params
    params.require(:camp_tags).permit(:title, :style, item_ids: []).merge(user_id: current_user.id)
  end

  def set_item
    if user_signed_in?
      user = User.find(current_user.id)
      @items = user.items
    end
  end

end

formオブジェクト

camp.tags.rb
class CampTags

  include ActiveModel::Model
  attr_accessor :title, :style, :user_id, :item_ids

  with_options presence: true do
    validates :title
    validates :style
  end

  def save(tag_list)
    @camp = Camp.create(user_id: user_id, title: title)
    tag_list.each do |tag|
      unless Tag.find_by(style: tag)
        @tag = Tag.create(style: tag)
        CampTagRelation.create(camp_id: @camp.id, tag_id: @tag.id)
      else
        @tag_id = Tag.find_by(style: tag)
        CampTagRelation.create(camp_id: @camp.id, tag_id: @tag_id.id)
      end
    end
  end

end

入力フォーム

new.html.erb
<div class="wrapper">
  <div class="side-ber">
    <%= render "side_ber" %>
  </div>
  <%= form_with model: @camp, url: camps_path, class:'form-wrap', local: true do |f| %>
    <div class='form'>
      <div class="title-field">
        <%= f.label :title,  "キャンプタイトル" %>
        <%= f.text_area :title, class:"input-title" %>
      </div>
      <div class="tag-field", id='tag-field'>
        <%= f.label :style, "キャンプスタイル" %>
        <%= f.text_field :style, class:"input-tag" %>
      </div>

      <p>使用アイテムを選択 </p>
      <select name="camp_tags[item_ids][]" multiple>
        <% @items.each do |item| %>
          <option value=<%= item.id %>><%= item.name %></option>
        <% end %>
      </select>


    </div>
    <div class="submit-post">
      <%= f.submit "Send", class: "submit-btn" %>
    </div>
  <% end %>
</div>

ルーティング

routes.rb
Rails.application.routes.draw do
  root to: "camps#index"
  resources :camps, only: [:new, :create, :show]
  resources :items, only: [:new, :create, :show]
  devise_for :users
end

テーブル同士のアソシエーション

item.rb
class Item < ApplicationRecord
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to :genre
  belongs_to :user
  has_many :camp_item_relations
  has_many :camps, through: :camp_item_relations


  with_options presence: true do
    validates :genre_id, numericality: { other_than: 1, message: 'Select'}
    validates :name
    validates :feature
    validates :price, numericality: { with: /\A[0-9]+\z/, message: 'Half-width number' }
  end

end
camp.rb
class Camp < ApplicationRecord
  belongs_to :user
  has_many :camp_tag_relations
  has_many :tags, through: :camp_tag_relations
  has_many :camp_item_relations
  has_many :items, through: :camp_item_relations

  validates :title, presence: true

end

itemとcampの中間テーブル

camp.item.relations.rb
class CampItemRelation < ApplicationRecord
  belongs_to :camp
  belongs_to :item
end

試したこと

コントローラーではitem_idsを扱える
フォームオブジェクトではcamp.idを扱えることから
CampItemRelation.createでのcampとitemのidの保存を別々で行ってみた。
最初にフォームオブジェクトでCampItemRelation.create(camp_id: @camp.id)として
次にコントローラーでCampItemRelation.create(item_id: item_id)
こうすることでデータを扱えるうちに保存できると考えましたが、データはどうやら同時に保存しないといけないらしくこれらの記述は読み飛ばされたようでした。

camps.controller.rb
class CampsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_item, only: [:index, :new, :show, :create]
  def index
    @tags = Tag.all
  end

  def new
    @camp = CampTags.new
  end

  def create
    @camp = CampTags.new(camp_params)

    if @camp.valid?
      @tag_list = camp_params[:style].split(/[[:blank:]]+/).select(&:present?)
      @camp.save(@tag_list)

      @item_ids = @camp.item_ids
      @item_ids.each do |item_id|
        CampItemRelation.create(item_id: item_id)
      end
      return redirect_to root_path
    else
      render "new"
    end
  end

  private

  def camp_params
    params.require(:camp_tags).permit(:title, :style, item_ids: []).merge(user_id: current_user.id)
  end

  def set_item
    if user_signed_in?
      user = User.find(current_user.id)
      @items = user.items
    end
  end

end
camp.tags.rb
class CampTags

  include ActiveModel::Model
  attr_accessor :title, :style, :user_id, :item_ids

  with_options presence: true do
    validates :title
    validates :style
  end

  def save(tag_list)
    @camp = Camp.create(user_id: user_id, title: title)
    CampItemRelation.create(camp_id: @camp.id)
    tag_list.each do |tag|
      unless Tag.find_by(style: tag)
        @tag = Tag.create(style: tag)
        CampTagRelation.create(camp_id: @camp.id, tag_id: @tag.id)
      else
        @tag_id = Tag.find_by(style: tag)
        CampTagRelation.create(camp_id: @camp.id, tag_id: @tag_id.id)
      end
    end
  end

end

試したこと2

save処理後すぐに保存された最新のレコードを取り出す

camps.controller.rb
  def create
    @camp = CampTags.new(camp_params)

    if @camp.valid?
      @tag_list = camp_params[:style].split(/[[:blank:]]+/).select(&:present?)
      @camp.save(@tag_list)
      camp = Camp.order(updated_at: :desc).limit(1)
      @camp_id = camp.ids
      @item_ids = @camp.item_ids
      binding.pry
      @item_ids.each do |item_id|
        CampItemRelation.create(camp_id: @camp_id, item_id: item_id)
      end
      return redirect_to root_path
    else
      render "new"
    end
  end

camp = Camp.order(updated_at: :desc).limit(1)
とすることで最新のレコードを一個取り出しています。
答えまで後少し!!!

たどり着いた回答

camps.controller.rb
  def create
    @camp = CampTags.new(camp_params)

    if @camp.valid?
      @tag_list = camp_params[:style].split(/[[:blank:]]+/).select(&:present?)
      @camp.save(@tag_list)
      camp = Camp.order(updated_at: :desc).limit(1)
      @camp_id = camp.ids
      @item_ids = @camp.item_ids
      @item_ids.each do |item_id|
        CampItemRelation.create(camp_id: @camp_id[0], item_id: item_id)
      end
      return redirect_to root_path
    else
      render "new"
    end
  end

saveメソッド後すぐに最新のレコードを取り出してそれを変数に入れる
orderで最新のレコードを一つしか取り出していないがidの取り出しにはidsとしなければならないようです。
保存の際に@camp_id[0]とすることで配列の一つ目のデータを指定することが叶っています。

以上

1
1
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
1
1