- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
Chapter 8
Task C: Cart Creation
Now that we have the ability to display a catalog containing all our wonderful products, it would be nice to be able to sell them. Our customer agrees, so we’ve jointly decided to implement the shopping cart functionality next. This is going to involve a number of new concepts, including sessions and parent-child relationships between database tables, so let’s get started.
8.1 Sessions
Before we launch into our next wildly successful iteration, we need to spend just a little while looking at sessions, web applications, and Rails.
As a user browses our online catalog, she’ll (we hope) select products to buy. The convention is that each item selected will be added to a virtual shopping cart, held in our store. At some point, our buyer will have everything she needs, and she’ll proceed to our site’s checkout, where she’ll pay for the stuff in the cart.
This means that our application will need to keep track of all the items added to the cart by the buyer. This sounds simple, except for one minor detail. The protocol used to talk between browsers and application programs is stateless—there’s no memory built-in to it. Each time your application receives a request from the browser is like the first time they’ve talked to each other. That’s cool for romantics but not so good when you’re trying to remember what products your user has already selected.
The most popular solution to this problem is to fake out the idea of stateful transactions on top of HTTP, which is stateless. A layer within the application tries to match up an incoming request to a locally held piece
Prepared exclusively for Rida Al Barazi
SESSIONS 75
of session data. If a particular piece of session data can be matched to all the requests that come from a particular browser, we can keep track of all the stuff done by the user of that browser using that session data.
The underlying mechanisms for doing this session tracking are varied. Sometimes an application encodes the session information in the form data on each page. Sometimes the encoded session identifier is added to the end of each URL (the so-called URL Rewriting option). And sometimes the application uses cookies. Rails uses the cookie-based approach.
A cookie is simply a chunk of named data that a web application passes to a web browser. The browser stores the cookie locally on the user’s computer. Subsequently, when the browser sends a request to the application, the cookie data tags along. The application uses information in the cookie to match the request with session information stored in the server. It’s an ugly solution to a messy problem. Fortunately, as a Rails programmer you don’t have to worry about all these low-level details. (In fact, the only reason to go into them at all is to explain why users of Rails applications must have cookies enabled in their browsers.)
Rather than have developers worry about protocols and cookies, Rails provides a simple abstraction. Within the controller, Rails maintains a special hash-like collection called session. Any key/value pairs you store into this hash during the processing of a request will be available during subsequent requests from the same browser.
In the Depot application we want to use the session facility to store the information about what’s in each buyer’s cart. But we have to be slightly careful here—the issue is deeper than it might appear. There are problems of resilience and scalability.
By default, Rails stores session information in a file on the server. If you have a single Rails server running, there’s no problem with this. But imagine that your store application gets so wildly popular that you run out of capacity on a single server machine and need to run multiple boxes. The first request from a particular user might be routed to one backend machine, but the second request might go to another. The session data stored on the first server isn’t available on the second; the user will get very confused as items appear and disappear in their cart across requests.
So, it’s a good idea to make sure that session information is stored somewhere external to the application where it can be shared between multiple application processes if needed. And if this external store is persistent, we can even bounce a server and not lose any session information. We talk
cookie
hash
→ page 474
Prepared exclusively for Rida Al Barazi
Report erratum
|
MORE TABLES, MORE MODELS |
76 |
|
|
all about setting up session information in Chapter 22, Deployment and |
|
|
|
Scaling, on page 440. For now, let’s assume that Rails handles all this. |
|
|
|
So, having just plowed through all that theory, where does that leave us |
|
|
|
in practice? We need to be able to assign a new cart object to a session the |
|
|
|
first time it’s needed and find that cart object again every time it’s needed |
|
|
|
in the same session. We can achieve that by creating a helper method, |
|
|
|
find_cart( ), in the store controller. The implementation is as follows. |
||= |
|
File 75 |
private |
→ page 479 |
|
|
|
||
|
def find_cart |
|
|
|
session[:cart] ||= Cart.new |
|
|
|
end |
|
|
|
This method is fairly tricky. It uses Ruby’s conditional assignment opera- |
|
|
|
tor, ||=. If the session hash has a value corresponding to the key :cart, that |
|
|
|
value is returned immediately. Otherwise a new cart object is created and |
|
|
|
assigned to the session. This new cart is then returned. |
|
|
|
We make the find_cart( ) method private. This prevents Rails from making |
|
|
|
it available as an action on the controller. |
|
|
8.2 More Tables, More Models
So, we know we need to create some kind of cart that holds the things that our customers have selected for purchase, and we know that we’ll keep that cart around between requests by associating it with a session. The cart will hold line items, where a line item is basically a combination of a product and a quantity. If, for example, the end user buys a unit testing book, the cart will hold a line item with a quantity of 1 referencing the unit testing product in the database. We chatted with our customer, who reminded us that if a product’s price changes, existing orders should honor the old price, so we’ll also keep a unit price field in the line item.
Based on what we know, we can go ahead and create our line_items table by adding a new table definition to our create.sql script. Notice the foreign key reference that links the line item to the appropriate product.
File 80 |
drop table if exists line_items; |
||
|
create table line_items ( |
|
|
|
id |
int |
not null auto_increment, |
|
product_id |
int |
not null, |
|
quantity |
int |
not null default 0, |
|
unit_price |
decimal(10,2) |
not null, |
|
constraint fk_items_product |
foreign key (product_id) references products(id), |
|
|
primary key (id) |
|
|
|
); |
|
|
Prepared exclusively for Rida Al Barazi
Report erratum
MORE TABLES, MORE MODELS |
77 |
|
If you’re following along at home, remember to load this new definition into |
|
your MySQL schema. |
|
depot> mysql depot_development <db/create.sql |
|
We need to remember to create a Rails model for this new table. Note how |
|
the name mapping works here: a class name of LineItem will be mapped |
|
to the underlying table line_items. Class names are mixed case (each word |
|
starts with a capital letter, and there are no breaks). Table names (and, as |
|
we’ll see later, variable names and symbols) are lowercase, with an under- |
|
score between words. (Although that’s all we have to say about naming |
|
for now, you might want to look at Section 14.1, Tables and Classes, on |
|
page 191, for more information on how class and table names are related.) |
|
depot> ruby script/generate model LineItem |
|
Finally, we have to tell Rails about the reference between the line items |
|
and the product tables. You might think that Rails could poke around |
|
in the database schema definition to discover these relationships, but not |
|
all database engines support foreign keys.1 Instead, we have to explicitly |
|
tell Rails what relationships exist between tables. In this particular case, |
|
we represent the relationship between a line item and a product by telling |
|
Rails that the line item belongs_to( ) a product. We specify that directly in |
|
the line item model class, defined in app/models/line_item.rb. |
File 77 |
class LineItem < ActiveRecord::Base |
|
belongs_to :product |
|
end |
|
Rails uses a naming convention that lets it make assumptions about how |
|
the foreign keys work in the underlying database. Let’s look at the logical |
|
model, schema, and resulting Rails classes shown in Figure 8.1, on the |
|
next page. If the model called Child belongs to the model Parent, Rails |
|
assumes that the table children has a column parent_id referencing the col- |
|
umn id in the parents table.2 In our line item model, the belongs to rela- |
|
tionship means that a line item has a column product_id that references |
|
the id column in the products table. As with most things in Rails, if the |
|
assumptions it makes about column names don’t work for your particular |
|
schema, you can always override it. |
1For example, the popular MySQL database does not implement foreign keys internally unless you specify a particular underlying implementation for the corresponding tables.
2Yes, Rails is smart enough to know that a model called Child should map to a table
named children.
Prepared exclusively for Rida Al Barazi
Report erratum