ProblemYou've got a shopping application that adds items to a cart and then removes those items from inventory. These two steps are part of a single operation. Both the number of items in the cart and the amount remaining in inventory are stored in separate tables in a database. You recognize that it's possible that when a specific number of items are added to the cart that there could be insufficient inventory to fill the order. You could try to get around this by checking for available inventory prior to adding items to the cart, but it's still possible for another user to deplete the inventory in between your check for availability and the cart quantity update. You want to ensure that if there isn't enough of an item in inventory, the amount added to the cart is rolled back to its original state. In other words, you want both operations to complete successfully, or neither to make any changes. SolutionUse Active Record transactions. First, create a very simple database to store items in the cart and those remaining in inventory. Populate the inventory table with 50 laptops. Use the following migration to set this up: db/migrate/001_build_db.rb: class BuildDb < ActiveRecord::Migration def self.up create_table :cart_items do |t| t.column :user_id, :integer t.column :name, :string t.column :quantity, :integer, { :default => 0 } end create_table :inventory_items do |t| t.column :name, :string t.column :on_hand, :integer end InventoryItem.create :name => "Laptop", :on_hand => 50 end def self.down drop_table :cart_items drop_table :inventory_items end end Create a model for inventory that subtracts items from the quantity on hand. Add a validation method that ensures that the amount of an item in inventory cannot be negative. app/models/inventory_item.rb: class InventoryItem < ActiveRecord::Base def subtract_from_inventory(total) self.on_hand -= total self.save! return self.on_hand end protected def validate errors.add("on_hand", "can't be negative") unless on_hand >= 0 end end Next, create a cart model with an accessor method for adding items: app/models/cart_item.rb: class CartItem < ActiveRecord::Base def add_to_cart(qty) self.quantity += qty self.save! return self.quantity end end In the Cart Controller, create a method that adds five laptops to a shopping cart. Pass a block containing the related operations into an Active Record transaction method. Further surround this transaction with exception handling. app/controllers/cart_controller.rb: class CartController < ApplicationController def add_items item = params[:item] || "Laptop" quantity = params[:quantity].to_i || 5 @new_item = CartItem.find_or_create_by_name(item) @inv_item = InventoryItem.find_by_name(@new_item.name) begin CartItem.transaction(@new_item, @inv_item) do @new_item.add_to_cart(quantity) @inv_item.subtract_from_inventory(quantity) end rescue flash[:error] = "Sorry, we don't have #{quantity} of that item left!" render :action => "add_items" return end end end Finally, create a view that displays the number of items in the cart with the number left in inventory, as well as a form for adding more items: app/views/cart/add_items.rhtml: <h1>Simple Cart</h1> <% if flash[:error] %> <p style="color: red; font-weight: bold;"><%= flash[:error] %></p> <% end %> <p>Items in cart: <b><%= @new_item.quantity %></b> <%= @new_item.name.pluralize %><p> <p>Items remaining in inventory: <b><%= @inv_item.on_hand %></b> <%= @inv_item.name.pluralize %><p> <form action="add_items" method="post"> <input type="text" name="quantity" value="1" size="2"> <select name="item"> <option value="Laptop">Laptop</option> </select> <input type="submit" value="Add to cart"> </form> DiscussionThe solution uses Active Record's transaction facility to guarantee that all operations within the transaction block are performed successfully or that none of them are. In the solution, the model definitions take care of incrementing and decrementing the quantities involved. The save! method that Active Record provides will commit the changed object to the database. save! differs from save because it raises a RecordInvalid exception if the save fails, instead of returning false. The rescue block in the Cart Controller catches the error, should one occur; this block defines the error message to be sent to the user. Passing a block of code to the transaction method takes care of rolling back partial database changes made by that code, upon error. To return the objects involved in the transaction to their original state, you have to pass them as arguments to the transaction call as well as the code block. Figure 3-6 shows the cart from the solution after a successful "add to cart" attempt. Figure 3-6. A laptop successfully added to the cart and removed from inventoryAt the mysql prompt, you can confirm that the quantities change as expected. More importantly, you can confirm that the transaction actually rolls back any database changes upon error. mysql> select quantity, on_hand from cart_items ci, inventory_items ii where ci.name = ii.name; +----------+---------+ | quantity | on_hand | +----------+---------+ | 32 | 18 | +----------+---------+ 1 row in set (0.01 sec) Figure 3-7 shows the results of trying to add more than what's left in inventory. Figure 3-7. The results of a failed transactionSure enough, the update of quantity was rolled back because the decrement of on_hand failed its validation check: mysql> select quantity, on_hand from cart_items ci, inventory_items ii where ci.name = ii.name; +----------+---------+ | quantity | on_hand | +----------+---------+ | 50 | 0 | +----------+---------+ 1 row in set (0.01 sec) For Active Record transactions to work, your database needs to have transaction support. The default database engine for MySQL is MyISAM, which does not support transactions. The solution specifies that MySQL use the InnoDB storage engine in the table creation statements. InnoDB has transaction support. If you're using PostgreSQL, you have transaction support by default. See Also
|