Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
28
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

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