ruby-on-railsrubydatabasenomethoderror

undefined method `item_id' for @order.save


I'm creating a rails app with relational DB, when I try to save order appearing NoMethodError to string that I pass to order.new I'm new to rails and maybe don`t fully understand how need to proceed with the records. But debugger cant show me where to search for errors.

controller

class OrdersController

  def create
    #render plain: params
    a = {amount: params[:amount], user_id: current_user.id, id: 1}
    @order = Order.new a

    a = {quantity: params[:quantity], item_id: params[:item], order_id: 1}
    @order_description = OrderDescription.new a
    @order.save
    @order_description.save
  end

error

undefined method `item_id' for #<Order id: 1, amount: 2222, created_at: nil, updated_at: nil, user_id: 19>

db

  create_table "items", force: :cascade do |t|
    t.string "name"
    t.string "description"
    t.integer "price"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "order_descriptions", force: :cascade do |t|
    t.integer "quantity"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.bigint "item_id", null: false
    t.bigint "order_id", null: false
    t.index ["item_id"], name: "index_order_descriptions_on_item_id"
    t.index ["order_id"], name: "index_order_descriptions_on_order_id"
  end

  create_table "orders", force: :cascade do |t|
    t.integer "amount"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.bigint "user_id", null: false
    t.index ["user_id"], name: "index_orders_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.string "last_name"
    t.string "first_name"
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

  add_foreign_key "order_descriptions", "items"
  add_foreign_key "order_descriptions", "orders"
  add_foreign_key "orders", "users"

I tried to remove the relations in the ruby model but not help. This error happens regardless of the data I pass to it.

True Solve

In folder test/fixters I found a file named order.yml and in it was field item_id (God know how it appears there) and because of these field order.save doesn't pass the validation test.


Solution

  • It looks like you're trying to setup a has_many through: association. Where order_descriptions serves as the line items of an order.

    class Order < ApplicationRecord
      belongs_to :user
      has_many :order_descriptions
      has_many :items, through: :order_descriptions
    end
    
    # very vague name - you could do better
    class OrderDescription < ApplicationRecord
      belongs_to :item
      belongs_to :order
    end
    
    class Item < ApplicationRecord
      has_many :order_descriptions
      has_many :orders, through: :order_descriptions
    end
    

    If you want to create an classical order form where you create an order with multiple lines you can use accepts_nested_attributes:

    class Order < ApplicationRecord
      has_many :order_descriptions
      has_many :items, through: :order_descriptions
      accepts_nested_attributes_for :order_descriptions, reject_if: :all_blank?
    end
    

    This lets you pass an array of attributes (hashes) to create the parent and child records at the same time:

    Order.create(
      user: User.first,
      order_descriptions_attributes: [
        { item_id: 1, quantity: 10 },
        { item_id: 2, quantity: 20 }
      ]
    )
    

    You can handle this in a form with fields_for:

    <%= form_with(model: @order) do |form| %>
      <%= form.fields_for(:order_descriptions) do |ff| %>
        <div class="field">
          <%= ff.label :item_id %>
          <%= ff.collection_select :item_id, @items, :id, :name %>
        </div>
        <div class="field">
          <%= ff.label :quantity %>
          <%= ff.number_field :quantity, step: 1 %>
        </div>
      <% end %>
    <% end %>
    

    And in your controller with:

    class OrdersController
      before_action :set_order, only: [:show, :edit, :update, :destroy]
    
      def show
      end
    
      def new
        @order = Order.new
        @items = Item.all
        5.times { @order.order_descriptions.new } 
      end
    
      def create
        @order = Order.new(order_params) do |order|
          order.user = current_user
        end
        if @order.save
          redirect_to @order 
        else
          @items = Item.all
          5.times { @order.order_descriptions.new } 
          render :new
        end
      end
    
      private
    
      def set_order
         @order = Order.find(params[:id])
      end
      
      def order_params
        params.require(:order)
              .permit(
                order_descriptions_attributes: [:id, :item_id, :quantity]
              )
      end
    end
    

    order_descriptions_attributes: [:item_id, :quantity] permits an array of hashes with the keys :id, :item_id and :quantity.

    In terms of user experience its really far from ideal though - and for your typical web shop you'll want to create the order (or in other terms cart) upfront and then setup a nested route where users add line items to the order.