Tim Feeley
Tim Feeley

Reputation: 115

Populating "has_many" associations to an un-saved object (Order has_many LineItems)

I'm working on a sample project that allows Orders to be created, comprised of many Products, associated via LineItems (see my recent question at Should I include other fields in a HABTM table?).

Once my view for a new object gets rendered, I have access to the @orders object, and it seems like I should be collecting @line_items as I go. The problem is, the @orders object hasn't been saved yet.

The best example I could find on the web so far was at a Railscasts writeup that seemed to encourage storing object parameters in session:

def new
   session[:order_params] ||= {}
   @order = Order.new(session[:order_params])
   @order.current_step = session[:order_step]
 end

I just wasn't sure how to best pay this out when I'm dealing with storing multiple Products per Order (via LineItem). One thing that came to mind was to create and save the object in the database but to only mark it as "real" once the user actually saves it (vs just adding items to the order) - but given abandonment rates and such, it seemed like I might end up with too much garbage.

Is there a widely accepted convention to taking a new @order that's unsaved and "building" a has_many list of products "correctly?" For the record, I'm trying to replicate a project I built in PHP using RoR, if that helps provide context of my end game.

My schema (intended to support ordering gift cards for multiple Properties) looks a little something like:

# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended that you check this file into your version control system.    

ActiveRecord::Schema.define(version: 3) do    

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"    

  create_table "line_items", id: false, force: true do |t|
    t.integer "order_id"
    t.integer "product_id"
    t.decimal "pay_cost",      precision: 8, scale: 2,               null: false
    t.decimal "pay_discount",  precision: 8, scale: 2, default: 0.0
    t.decimal "pay_linetotal", precision: 8, scale: 2,               null: false
    t.text    "note"
  end    

  add_index "line_items", ["order_id", "product_id"], name: "index_line_items_on_order_id_and_product_id", unique: true, using: :btree    

  create_table "orders", force: true do |t|
    t.string   "order_id",                                              null: false
    t.integer  "property_id"
    t.string   "order_status",                          default: "new"
    t.string   "email_address"
    t.string   "bill_name"
    t.string   "bill_address1"
    t.string   "bill_address2"
    t.string   "bill_city"
    t.string   "bill_state"
    t.string   "bill_zip"
    t.string   "ship_name"
    t.string   "ship_address1"
    t.string   "ship_address2"
    t.string   "ship_city"
    t.string   "ship_state"
    t.string   "ship_zip"
    t.string   "pay_cardtype"
    t.string   "pay_pastfour"
    t.text     "order_summary"
    t.boolean  "is_gift",                               default: false
    t.text     "order_message"
    t.boolean  "pay_live",                              default: false
    t.boolean  "pay_paid",                              default: false
    t.boolean  "pay_refunded",                          default: false
    t.decimal  "pay_total",     precision: 8, scale: 2,                 null: false
    t.decimal  "pay_discount",  precision: 8, scale: 2, default: 0.0
    t.integer  "stripe_fee"
    t.string   "stripe_token"
    t.datetime "created_at"
    t.datetime "updated_at"
  end    

  add_index "orders", ["order_id"], name: "index_orders_on_order_id", unique: true, using: :btree
  add_index "orders", ["order_status"], name: "index_orders_on_order_status", using: :btree    

  create_table "products", force: true do |t|
    t.string  "name",                                          null: false
    t.decimal "price",  precision: 8, scale: 2,                null: false
    t.boolean "active",                         default: true
  end    

  create_table "products_properties", id: false, force: true do |t|
    t.integer "product_id"
    t.integer "property_id"
  end    

  add_index "products_properties", ["product_id", "property_id"], name: "index_products_properties_on_product_id_and_property_id", unique: true, using: :btree    

  create_table "properties", force: true do |t|
    t.string  "name",                   null: false
    t.string  "slug",                   null: false
    t.string  "prefix",                 null: false
    t.string  "phone"
    t.text    "address"
    t.string  "email"
    t.boolean "visible", default: true
  end    

  add_index "properties", ["prefix"], name: "index_properties_on_prefix", unique: true, using: :btree
  add_index "properties", ["slug"], name: "index_properties_on_slug", unique: true, using: :btree    

  create_table "properties_users", id: false, force: true do |t|
    t.integer "user_id"
    t.integer "property_id"
  end    

  add_index "properties_users", ["user_id", "property_id"], name: "index_properties_users_on_user_id_and_property_id", unique: true, using: :btree    

  create_table "users", force: true do |t|
    t.string   "first_name"
    t.string   "last_name"
    t.string   "email",                              null: false
    t.string   "encrypted_password", default: "",    null: false
    t.datetime "locked_at"
    t.boolean  "active",             default: true,  null: false
    t.boolean  "superuser",          default: false, null: false
  end    

  add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree    

end

Upvotes: 0

Views: 100

Answers (2)

Arctodus
Arctodus

Reputation: 5847

To persist the shopping cart between requests you can either store the necessary details in session or use actual database records and mark the order as temporary/unconfirmed. In simple scenarios using the database might be overkill but you definitely want to avoid having complex data in session. If you change things in the future it might break old sessions.

Im working on a simple web shop right now, so I'll show how Im using the session to store and retrieve the working order. I store a hash in session[:cart] with the keys being product ids and the values being the quantity

Add to cart action

  def add_to_cart
    product = Product.find(params[:product_id])
    session[:cart][product.id] ||= 0
    session[:cart][product.id] += 1
    redirect_to :back
  end

A before_action method restores @order between requests

class ApplicationController < ActionController::Base
  before_action :restore_order

  private

  def restore_order
    session[:cart] ||= {}
    @order = Order.build_from_session(session[:cart])
  end
end

The Order model

class Order < ActiveRecord::Base
  has_many :line_items

  def self.build_from_session(session_data)
    order = self.new
    session_data.each do |product_id, quantity|
      product = Product.find_by_id(product_id)
      next unless product && quantity > 0
      order.line_items << LineItem.new(product: product, quantity: quantity)
    end
    return order
  end
end

Upvotes: 1

James Mason
James Mason

Reputation: 4306

Storing it in the session should be fine. If you're configured to use CookieStore for sessions, try to keep the data as small as possible (just product IDs, ideally). You might also want to look at something like wicked if you've got a multi-step checkout process.

I would encourage you to think about storing orders directly in your database, though. That lets you do some cool stuff like show a user their abandoned cart when they come back to your site, or send out reminder emails/special offers to people who left in the middle of the process.

You would also want a scheduled cleanup process that purges incomplete orders older than a few weeks/months.

Upvotes: 1

Related Questions