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

PRIMARY KEYS AND IDS

197

 

In the Active Record class that wraps this table, we’ll use the serialize( )

 

declaration to tell Active Record to marshal objects into and out of this

 

column.

File 8

class Purchase < ActiveRecord::Base

 

serialize :last_five

 

# ...

 

end

 

When we create new Purchase objects, we can assign any Ruby object to

 

the last_five column. In this case, we set it to an array of strings.

File 8

purchase = Purchase.new

 

purchase.name = "Dave Thomas"

 

purchase.last_five = [ 'shoes', 'shirt', 'socks', 'ski mask', 'shorts' ]

 

purchase.save

 

When we later read it in, the attribute is set back to an array.

File 8

purchase = Purchase.find_by_name("Dave Thomas")

 

pp purchase.last_five

 

pp purchase.last_five[3]

 

This code outputs

 

["shoes", "shirt", "socks", "ski mask", "shorts"]

 

"ski mask"

 

Although powerful and convenient, this approach is problematic if you

 

ever need to be able to use the information in the serialized columns out-

 

side a Ruby application. Unless that application understands the YAML

 

format, the column contents will be opaque to it. In particular, it will be

 

difficult to use the structure inside these columns in SQL queries. You

 

might instead want to consider using object aggregation, described in Sec-

 

tion 15.2, Aggregation, on page 247, to achieve a similar effect.

14.3 Primary Keys and IDs

You may have noticed that our sample database tables all define an integer column called id as their primary key. This is an Active Record convention.

“But wait!” you cry. “Shouldn’t the primary key of my orders table be the order number or some other meaningful column? Why use an artificial primary key such as id?”

The reason is largely a practical one—the format of external data may change over time. For example, you might think that the ISBN of a book would make a good primary key in a table of books. After all, ISBNs are

Prepared exclusively for Rida Al Barazi

Report erratum

PRIMARY KEYS AND IDS 198

unique. But as this particular book is being written, the publishing industry in the US is gearing up for a major change as additional digits are added to all ISBNs.

If we’d used the ISBN as the primary key in a table of books, we’d have to go through and update each row to reflect this change. But then we’d have another problem. There’ll be other tables in the database that reference rows in the books table via the primary key. We can’t change the key in the books table unless we first go through and update all of these references. And that will involve dropping foreign key constraints, updating tables, updating the books table, and finally reestablishing the constraints. All in all, something of a pain.

If we use our own internal value as a primary key, things work out a lot better. No third party can come along and arbitrarily tell us to change things—we control our own keyspace. And if something such as the ISBN does need to change, it can change without affecting any of the existing relationships in the database. In effect, we’ve decoupled the knitting together of rows from the external representation of data in those rows.

Now there’s nothing to say that we can’t expose the id value to our end users. In the orders table, we could externally call it an order id and print it on all the paperwork. But be careful doing this—at any time some regulator may come along and mandate that order ids must follow an externally imposed format, and you’d be back where you started.

If you’re creating a new schema for a Rails application, you’ll probably want to go with the flow and give all of your tables an id column as their primary key.5 If you need to work with an existing schema, Active Record gives you a simple way of overriding the default name of the primary key for a table.

class BadBook < ActiveRecord::Base set_primary_key "isbn"

end

Normally, Active Record takes care of creating new primary key values for records that you create and add to the database—they’ll be ascending integers (possibly with some gaps in the sequence). However, if you override the primary key column’s name, you also take on the responsibility of setting the primary key to a unique value before you save a new row. Perhaps surprisingly, you still set an attribute called id to do this. As far as

5As we’ll see later, join tables are not included in this advice—they should not have an id column.

Prepared exclusively for Rida Al Barazi

Report erratum

CONNECTING TO THE DATABASE 199

Active Record is concerned, the primary key attribute is always set using an attribute called id. The set_primary_key declaration sets the name of the column to use in the table. In the following code, we use an attribute called id even though the primary key in the database is isbn.

book = BadBook.new book.id = "0-12345-6789"

book.title = "My Great American Novel" book.save

# ...

book = BadBook.find("0-12345-6789")

puts book.title # => "My Great American Novel" p book.attributes #=> {"isbn" =>"0-12345-6789",

"title"=>"My Great American Novel"}

Just to make things more confusing, the attributes of the model object have the column names isbn and titleid doesn’t appear. When you need to set the primary key, use id. At all other times, use the actual column name.

14.4 Connecting to the Database

Active Record abstracts the concept of a database connection, relieving the application of dealing with the low-level details of working with specific databases. Instead, Active Record applications use generic calls, delegating the details to a set of database-specific adapters. (This abstraction breaks down slightly when code starts to make SQL-based queries, as we’ll see later.)

One way of specifying the connection is to use the establish_connection( ) class method.6 For example, the following call creates a connection to a MySQL database called railsdb on the server dbserver.com using the given user name and password. It will be the default connection used by all model classes.

ActiveRecord::Base.establish_connection(

:adapter

=> "mysql",

:host

=> "dbserver.com",

:database

=> "railsdb",

:username

=> "railsuser",

:password => "railspw"

)

Active Record comes with support for the DB2, MySQL, Oracle, Postgres, SQLServer, and SQLite databases (and this list will grow). Each adapter

database connection

6In full-blown Rails applications, there’s another way of specifying connections. We describe it on page 178.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

 

 

 

CONNECTING TO THE DATABASE

200

 

 

 

 

 

 

 

 

:adapter

db2

mysql

oci

postgresql

sqlite sqlserver

 

 

:database

required

required

 

required

required

 

 

 

 

 

 

 

 

 

 

 

:host

 

localhost

required

localhost

localhost

 

 

:username

 

 

root

required

 

sa

 

 

:password

 

 

 

 

 

 

 

 

 

 

required

 

 

 

 

:port

 

 

3306

 

5432

 

 

 

:sslcert

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

:sslcapath

 

 

 

 

 

 

 

 

:sslcipher

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

:socket

 

 

/tmp/mysql.sock

 

 

 

 

:sslkey

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

:schema_order

 

 

 

 

 

 

 

:dbfile

 

 

 

 

 

required

 

 

Figure 14.2: Connection Parameters

takes a slightly different set of connection parameters, as shown in Figure 14.2 . Note that the Oracle adapter is called oci.

Connections are associated with model classes. Each class inherits the connection of its parent. As ActiveRecord::Base is the base class of all the Active Record classes, setting a connection for it sets the default connection for all the Active Record classes you define. However, you can override this when you need to.

In the following example, most of our application’s tables are in a MySQL database called online. For historical reasons (are there any other?), the customers table is in the backend database.

ActiveRecord::Base.establish_connection(

:adapter

=> "mysql",

:host

=> "dbserver.com",

:database

=> "online",

:username

=> "groucho",

:password

=> "swordfish")

class LineItem < ActiveRecord::Base

# ...

end

class Order < ActiveRecord::Base

# ...

end

class Product < ActiveRecord::Base

# ...

end

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 201

class Customer < ActiveRecord::Base

# ...

end

Customer.establish_connection(

:adapter

=> "mysql",

:host

=> "dbserver.com",

:database

=> "backend",

:username

=> "chicho",

:password

=> "piano")

When we wrote the Depot application earlier in this book, we didn’t use the establish_connection( ) method. Instead, we specified the connection parameters inside the file config/database.yml. For most Rails applications this is the preferred way of working. Not only does it keep all connection information out of the code, it also works better with the Rails testing and deployment philosophies. All of the parameters in Figure 14.2 can also be used in the YAML file. See Section 13.3, Configuring Database Connections, on page 178 for details.

Finally, you can combine the two approaches. If you pass a symbol to establish_connection( ), Rails looks for a section in database.yml with that name and bases the connection on the parameters found there. This way you can keep all connection details out of your code.

14.5 CRUD—Create, Read, Update, Delete

Active Record makes it easy to implement the four basic operations on database tables: create, read, update, and delete.

In this section we’ll be working with our orders table in a MySQL database. The following examples assume we have a basic Active Record model for this table.

class Order < ActiveRecord::Base end

Creating New Rows

In the object-relational paradigm, tables are represented as classes, and rows in the table correspond to objects of that class. It seems reasonable that we create rows in a table by creating new objects of the appropriate class. We can create new objects representing rows in our orders table by calling Order.new( ). We can then fill in the values of the attributes (corresponding to columns in the database). Finally, we call the object’s save( ) method to store the order back into the database. Without this call, the order would exist only in our local memory.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

 

CRUD—CREATE, READ, UPDATE, DELETE

202

File 11

an_order = Order.new

 

 

an_order.name

= "Dave Thomas"

 

 

an_order.email

= "dave@pragprog.com"

 

 

an_order.address

= "123 Main St"

 

 

an_order.pay_type = "check"

 

 

an_order.save

 

 

 

Active Record constructors take an optional block. If present, the block is

 

 

invoked with the newly created order as a parameter. This might be useful

 

 

if you wanted to create and save away an order without creating a new

 

 

local variable.

 

 

File 11

Order.new do |o|

 

 

 

o.name

= "Dave Thomas"

 

 

# . . .

 

 

 

 

o.save

 

 

 

 

end

 

 

 

 

Finally, Active Record constructors accept a hash of attribute values as an

 

 

optional parameter. Each entry in this hash corresponds to the name and

 

 

value of an attribute to be set. As we’ll see later in the book, this is useful

 

 

when storing values from HTML forms into database rows.

 

File 11

an_order = Order.new(

 

 

:name

=> "Dave Thomas",

 

 

:email

=> "dave@pragprog.com",

 

 

:address

=> "123 Main St",

 

 

:pay_type => "check")

 

 

an_order.save

 

 

 

Note that in all of these examples we did not set the id attribute of the new

 

 

row. Because we used the Active Record default of an integer column for

 

 

the primary key, Active Record automatically creates a unique value and

 

 

sets the id attribute just before saving the row. We can subsequently find

 

 

this value by querying the attribute.

 

File 11

an_order = Order.new

 

 

an_order.name = "Dave Thomas"

 

 

# ...

 

 

 

 

an_order.save

 

 

 

puts "The ID of this order is #{an_order.id}"

 

 

The new( ) constructor creates a new Order object in memory; we have to

 

 

remember to save it to the database at some point. Active Record has a

 

 

convenience method, create( ), that both instantiates the model object and

 

 

stores it into the database.

 

File 11

an_order = Order.create(

 

 

:name

=> "Dave Thomas",

 

 

:email

=> "dave@pragprog.com",

 

 

:address

=> "123 Main St",

 

:pay_type => "check")

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 203

You can pass create( ) an array of attribute hashes; it’ll create multiple rows in the database and return an array of the corresponding model objects.

File 11

orders = Order.create(

 

[ { :name

=> "Dave Thomas",

 

:email

=> "dave@pragprog.com",

 

:address

=> "123 Main St",

 

:pay_type => "check"

 

},

 

 

{ :name

=> "Andy Hunt",

 

:email

=> "andy@pragprog.com",

 

:address

=> "456 Gentle Drive",

 

:pay_type => "po"

 

} ] )

 

 

The real reason that new( ) and create( ) take a hash of values is that you

 

can construct model objects directly from form parameters.

File 11

order = Order.create(params)

Reading Existing Rows

Reading from a database involves first specifying which particular rows of data you are interested in—you’ll give Active Record some kind of criteria, and it will return objects containing data from the row(s) matching the criteria.

The simplest way of finding a row in a table is by specifying its primary key. Every model class supports the find( ) method, which takes one or more primary key values. If given just one primary key, it returns an object containing data for the corresponding row (or throws a RecordNotFound exception). If given multiple primary key values, find( ) returns an array of the corresponding objects. Note that in this case a RecordNotFound exception is returned if any of the ids cannot be found (so if the method returns without raising an error, the length of the resulting array will be equal to the number of ids passed as parameters).

an_order = Order.find(27) # find the order with id == 27

#Get a list of order ids from a form, then

#sum the total value

order_list = params[:order_ids] orders = Order.find(order_list) count = orders.size

Often, though, you need to read in rows based on criteria other than their primary key value. Active Record provides a range of options for performing these queries. We’ll start by looking at the low-level find( ) method and later move on to higher-level dynamic finders.

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 204

So far we’ve just scratched the surface of find( ), using it to return one or more rows based on ids that we pass in as parameters. However, find( ) has something of a split personality. If you pass in one of the symbols :first or :all as the first parameter, humble old find( ) blossoms into a remarkably powerful searching machine.

The :first variant of find( ) returns the first row that matches a set of criteria, while the :all form returns an array of matching rows. Both of these forms take a set of keyword parameters that control what they do. But before we look at these, we need to spend a page or two explaining how Active Record handles SQL.

SQL and Active Record

To illustrate how Active Record works with SQL, let’s look at the :conditions parameter of the find(:all, :conditions =>...) method call. This :conditions parameter determines which rows are returned by the find( ); it corresponds to an SQL where clause. For example, to return a list of all orders for Dave with a payment type of “po,” you could use

pos = Order.find(:all,

:conditions => "name = 'dave' and pay_type = 'po'")

The result will be an array of all the matching rows, each neatly wrapped in an Order object. If no orders match the criteria, the array will be empty.

That’s fine if your condition is predefined, but how do you handle the situation where the name of the customer is set externally (perhaps coming from a web form)? One way is to substitute the value of that variable into the condition string.

#get the limit amount from the form name = params[:name]

#DON'T DO THIS!!!

pos = Order.find(:all,

:conditions => "name = '#{name}' and pay_type = 'po'")

As the comment suggests, this really isn’t a good idea. Why? Because

it leaves your database wide open to something called an SQL injection SQL injection attack, which we describe in more detail in Chapter 21, Securing Your Rails Application, on page 427. For now, take it as a given that substituting a

string from an external source into an SQL statement is effectively the same as publishing your entire database to the whole online world.

Instead, the safe way to generate dynamic SQL is to let Active Record handle it. Wherever you can pass in a string containing SQL, you can also pass in an array. The first element of this array is a string containing

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 205

SQL. Within this SQL you can embed placeholders, which will be replaced at runtime by the values in the rest of the array.

One way of specifying placeholders is to insert one or more question marks in the SQL. The first question mark is replaced by the second element of the array, the next question mark by the third, and so on. For example, we could rewrite the previous query as

name = params[:name] pos = Order.find(:all,

:conditions => ["name = ? and pay_type = 'po'", name])

You can also use named placeholders. Each placeholder is of the form :name, and the corresponding values are supplied as a hash, where the keys correspond to the names in the query.

name

=

params[:name]

 

pay_type =

params[:pay_type]

 

pos = Order.find(:all,

 

 

 

:conditions => ["name = :name and pay_type =

:pay_type",

 

 

{:pay_type => pay_type, :name

=> name}])

You can take this a step further. Because params is effectively a hash, you can simply pass the whole thing in to the condition.

pos = Order.find(:all,

:conditions => ["name = :name and pay_type = :pay_type", params])

Regardless of which form of placeholder you use, Active Record takes great care to quote and escape the values being substituted into the SQL. Use these forms of dynamic SQL, and Active Record will keep you safe from injection attacks.

Power find()

Now that we know how to specify conditions, let’s turn our attention to the various options supported by find(:first, ...) and find(:all, ...).

First, it’s important to understand that find(:first, ...) generates an identical SQL query to doing find(:all, ...) with the same conditions, except that the result set is limited to a single row. We’ll describe the parameters for both methods in one place and illustrate them using find(:all, ...). We’ll call find( ) with a first parameter of :first or :all the finder method.

With no extra parameters, the finder effectively executes a select from...

statement. The :all form returns all rows from the table, and :first returns one. The order is not guaranteed (so Order.find(:first) will not necessarily return the first order created by your application).

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 206

David Says. . .

But Isn’t SQL Dirty?

Ever since programmers started to layer object-oriented systems on top of relational databases, they’ve struggled with the question of how deep to run the abstraction. Some object-relational mappers seek to eradicate the use of SQL entirely, striving for object-oriented purity by forcing all queries through another OO layer.

Active Record does not. It was built on the notion that SQL is neither dirty nor bad, just verbose in the trivial cases. The focus is on removing the need to deal with the verbosity in those trivial cases (writing a 10-attribute insert by hand will leave any programmer tired) but keeping the expressiveness around for the hard queries—the type SQL was created to deal with elegantly.

Therefore, you shouldn’t feel guilty when you use find_by_sql( ) to handle either performance bottlenecks or hard queries. Start out using the object-oriented interface for productivity and pleasure, and then dip beneath the surface for a close-to-the-metal experience when you need to.

The :conditions parameter lets you specify the condition passed to the SQL :conditions where clause used by the find( ) method. This condition can be either a

string containing SQL or an array containing SQL and substitution values, as described in the previous section. (From now on we won’t mention this explicitly—whenever we talk about an SQL parameter, assume the method can accept either an array or a string.)

daves_orders = Order.find(:all, :conditions => "name = 'Dave'")

name = params[:name]

other_orders = Order.find(:all, :conditions => ["name = ?", name])

yet_more = Order.find(:all,

:conditions => ["name = :name and pay_type = :pay_type", params])

SQL doesn’t guarantee that rows will be returned in any particular order unless you explicitly add an order by clause to the query. The :order param- :order eter lets you specify the criteria you’d normally add after the order by keywords. For example, the following query would return all of Dave’s orders, sorted first by payment type and then by shipping date (the latter

in descending order).

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

CRUD—CREATE, READ, UPDATE, DELETE

207

orders = Order.find(:all,

 

 

 

 

:conditions => "name = 'Dave'",

 

 

 

:order

=> "pay_type, shipped_at DESC")

 

 

You can limit the number of rows returned by find(:all, ...) with the :limit

:limit

 

parameter. If you use the limit parameter, you’ll probably also want to

 

 

specify the sort order to ensure consistent results. For example, the fol-

 

 

lowing returns the first 10 matching orders only.

 

 

orders = Order.find(:all,

 

 

 

 

:conditions => "name = 'Dave'",

 

 

 

:order

=> "pay_type, shipped_at DESC",

 

 

 

:limit

=> 10)

 

 

The :offset parameter goes hand in hand with the :limit parameter. It allows

:offset

 

you to specify the offset of the first row in the result set that will be

 

 

returned by find( ).

 

 

 

# The view wants to display orders grouped into pages,

 

 

# where each page shows page_size orders at a time.

 

 

# This method returns the orders on page page_num (starting

 

 

# at zero).

 

 

 

 

def Order.find_on_page(page_num, page_size)

 

 

find(:all,

 

 

 

 

:order => "id",

 

 

 

:limit => page_size,

 

 

 

:offset => page_num*page_size)

 

 

end

 

 

 

 

The :joins parameter to the finder method lets you specify a list of additional

:joins

 

tables to be joined to the default table. This parameter is inserted into

 

 

the SQL immediately after the name of the model’s table and before any

 

 

conditions specified by the first parameter. The join syntax is database-

 

 

specific. The following code returns a list of all line items for the book

 

 

called Programming Ruby.

 

 

 

LineItem.find(:all,

 

 

 

:conditions => "pr.title = 'Programming Ruby'",

 

 

:joins

=> "as li inner join products as pr on li.product_id = pr.id")

 

As we’ll see in Section 14.6, Relationships between Tables, on page 216, you probably won’t use the :joins parameter of find( ) very much—Active Record handles most of the common intertable joins for you.

There’s one additional parameter, :include, that kicks in only if you have :include associations defined. We’ll talk about it starting on page 234.

The find(:all, ...). method returns an array of model objects. If instead you want just one object returned, use find(:first, ...). This takes the same parameters as the :all form, but the :limit parameter is forced to the value 1, so only one row will be returned.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

CRUD—CREATE, READ, UPDATE, DELETE

208

File 10

# return an arbitrary order

 

 

 

 

order = Order.find(:first)

 

 

 

 

# return an order for Dave

 

 

 

 

order = Order.find(:first, :conditions => "name = 'Dave Thomas'")

 

 

 

# return the latest order for Dave

 

 

 

order = Order.find(:first,

 

 

 

 

:conditions => "name = 'Dave Thomas'",

 

 

 

:order

=> "id DESC")

 

 

 

If the criteria given to find(:first, ...) result in multiple rows being selected

 

 

 

from the table, the first of these is returned. If no rows are selected, nil is

 

 

 

returned.

 

 

 

 

The find( ) method constructs the full SQL query string for you. The method

 

 

 

find_by_sql( ) allows your application to take full control. It accepts a single

 

 

 

parameter containing an SQL select statement and returns a (potentially

 

 

 

empty) array of model objects from the result set. The attributes in these

 

 

 

models will be set from the columns returned by the query. You’d nor-

 

 

 

mally use the select * form to return all columns for a table, but this isn’t

 

 

 

required.7

 

 

 

File 10

orders = LineItem.find_by_sql("select line_items.* from line_items, orders " +

 

 

 

" where order_id = orders.id

" +

 

 

 

" and orders.name = 'Dave Thomas'

")

 

 

Only those attributes returned by a query will be available in the result-

 

 

 

ing model objects. You can determine the attributes available in a model

 

 

 

object using the attributes( ), attribute_names( ), and attribute_present?( ) meth-

 

 

 

ods. The first returns a hash of attribute name/value pairs, the second an

 

 

 

array of names, and the third returns true if a named attribute is available

 

 

 

in this model object.

 

 

 

File 10

orders = Order.find_by_sql("select name, pay_type from orders")

 

 

first = orders[0]

pfirst.attributes

pfirst.attribute_names

pfirst.attribute_present?("address")

This code produces

{"name"=>"Dave Thomas", "pay_type"=>"check"} ["name", "pay_type"]

false

find_by_sql( ) can also be used to create model objects containing derived column data. If you use the as xxx SQL syntax to give derived columns a name in the result set, this name will be used as the name of the attribute.

7But if you fail to fetch the primary key column in your query, you won’t be able to write updated data from the model back into the database. See Section 15.7, The Case of the Missing ID, on page 276.

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 209

File 10

items = LineItem.find_by_sql("select

*,

" +

 

"

quantity*unit_price as total_price,

" +

 

"

products.title as title

" +

 

" from

line_items, products

" +

 

" where

line_items.product_id = products.id

")

li = items[0]

puts "#{li.title}: #{li.quantity}x#{li.unit_price} => #{li.total_price}"

As with conditions, you can also pass an array to find_by_sql( ), where the first element is a string containing placeholders. The rest of the array can be either a hash or a list of values to be substituted.

Order.find_by_sql(["select * from orders where amount > ?", params[:amount]])

Counting Rows

Active Record defines two class methods that return the counts of rows matching criteria. The method count( ) returns the number of rows that match the given criteria (or all rows if no criteria are given). The method count_by_sql( ) returns a single number generated by an SQL statement (that statement will normally be a select count(*) from...).

File 10

c1

= Order.count

 

 

c2

=

Order.count(["name = ?", "Dave Thomas"])

 

 

c3

=

LineItem.count_by_sql("select count(*)

" +

"

from

line_items, orders

" +

" where

line_items.order_id = orders.id " +

"

and

orders.name = 'Dave Thomas'

")

puts "Dave has #{c3} line items in

#{c2} orders (#{c1} orders in all)"

Dynamic Finders

Probably the most common search performed on databases is to return the row or rows where a column matches a given value. A query might be return all the orders for Dave, or get all the blog postings where the subject is “Rails.” In many other languages and frameworks, you’d construct SQL queries to perform these searches. Active Record uses Ruby’s dynamic power to do this for you.

For example, our Order model has attributes such as name, email, and address. We can use these names in finder methods to return rows where the corresponding columns match some value.

File 10

order

= Order.find_by_name("Dave Thomas")

 

orders

= Order.find_all_by_name("Dave Thomas")

 

order

= Order.find_all_by_email(params['email'])

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 210

David Says. . .

To Raise, or Not to Raise?

When you use a finder driven by primary keys, you’re looking for a particular record. You expect it to exist. A call to Person.find(5) is based on our knowledge of the person table. We want the row with an id of 5. If this call is unsuccessful—if the record with the id of 5 has been destroyed—we’re in an exceptional situation. This mandates the raising of an exception, so Rails raises RecordNotFound.

On the other hand, finders that use criteria to search are looking for a match. So, Person.find(:first,:condition=>"name=’Dave’") is the equivalent of telling the database (as a black box), “Give me the first person row that has the name Dave.” This exhibits a distinctly different approach to retrieval; we’re not certain up front that we’ll get a result. It’s entirely possible the result set may be empty. Thus, returning nil in the case of finders that search for one row and an empty array for finders that search for many rows is the natural, nonexceptional response.

If you invoke a model’s class method with a name starting find_by_ or find_all_by_, Active Record converts it to a finder, using the rest of the method’s name to determine the column to be checked. Thus the call to

order = Order.find_by_name("Dave Thomas", other args...)

is (effectively) converted by Active Record into

order = Order.find(:first,

:conditions => ["name = ?", "Dave Thomas"], other_args...)

Similarly, calls to find_all_by_xxx are converted into matching find(:all, ...) calls.

The magic doesn’t stop there. Active Record will also create finders that search on multiple columns. For example, you could write

user = User.find_by_name_and_password(name, pw)

This is equivalent to

user = User.find(:first,

:conditions => ["name = ? and password = ?", name, pw])

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 211

To determine the names of the columns to check, Active Record simply splits the name that follows the find_by_ or find_all_by_ around the string _and_. This is good enough most of the time but breaks down if you ever have a column name such as tax_and_shipping. In these cases, you’ll have to use conventional finder methods.

And, no, there isn’t a find_by_ form that lets you use _or_ rather than _and_ between column names.

Reloading Data

In an application where the database is potentially being accessed by multiple processes (or by multiple applications), there’s always the possibility that a fetched model object has become stale—someone may have written a more recent copy to the database.

To some extent, this issue is addressed by transactional support (which we describe on page 237). However, there’ll still be times where you need to refresh a model object manually. Active Record makes this easy—simply call its reload( ) method, and the object’s attributes will be refreshed from the database.

stock = Market.find_by_ticker("RUBY") loop do

puts "Price = #{stock.price}" sleep 60

stock.reload end

In practice, reload( ) is rarely used outside the context of unit tests.

Updating Existing Rows

After such a long discussion of finder methods, you’ll be pleased to know that there’s not much to say about updating records with Active Record.

If you have an Active Record object (perhaps representing a row from our orders table), you can write it to the database by calling its save( ) method. If this object had previously been read from the database, this save will update the existing row; otherwise, the save will insert a new row.

If an existing row is updated, Active Record will use its primary key column to match it with the in-memory object. The attributes contained in the Active Record object determine the columns that will be updated—a column will be updated in the database even if its value has not changed. In the following example, all the values in the row for order 123 will be updated in the database table.

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 212

order = Order.find(123) order.name = "Fred" order.save

However, in this next example the Active Record object contains just the attributes id, name, and paytype—only these columns will be updated when the object is saved. (Note that you have to include the id column if you intend to save a row fetched using find_by_sql( )).

orders = Order.find_by_sql("select id, name, pay_type from orders where id=123") first = orders[0]

first.name = "Wilma" first.save

In addition to the save( ) method, Active Record lets you change the values of attributes and save a model object in a single call to update_attribute( ).

order = Order.find(123) order.update_attribute(:name, "Barney")

order = Order.find(321) order.update_attributes(:name => "Barney",

:email => "barney@bedrock.com")

Finally, we can combine the functions of reading a row and updating it using the class methods update( ) and update_all( ). The update( ) method takes an id parameter and a set of attributes. It fetches the corresponding row, updates the given attributes, saves the result back to the database, and returns the model object.

order = Order.update(12, :name => "Barney", :email => "barney@bedrock.com")

You can pass update( ) an array of ids and an array of attribute value hashes, and it will update all the corresponding rows in the database, returning an array of model objects.

Finally, the update_all( ) class method allows you to specify the set and where clauses of the SQL update statement. For example, the following increases the prices of all products with Java in their title by 10%.

result = Product.update_all("price = 1.1*price", "title like '%Java%'")

The return value of update_all( ) depends on the database adapter; most (but not Oracle) return the number of rows that were changed in the database.

save() and save!()

It turns out that there are two versions of the save method.

Plain old save( ) returns true if the model object is valid and can be saved.

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 213

if order.save

#all OK else

#validation failed

end

It’s up to you to check on each call to save( ) that it did what you expected. The reason Active Record is so lenient is that it assumes that save( ) is called in the context of a controller’s action method and that the view code will be presenting any errors back to the end user. And for many applications, that’s the case.

However, if you need to save a model object in a context where you want to make sure that all errors are handled programmatically, you should use save!( ). This method raises a RecordInvalid exception if the object could not be saved.

begin order.save!

rescue RecordInvalid => error

# validation failed end

Optimistic Locking

In an application where multiple processes access the same database, it’s possible for the data held by one process to become stale if another process updates the underlying database row.

For example, two processes may fetch the row corresponding to a particular account. Over the space of several seconds, both go to update that balance. Each loads an Active Record model object with the initial row contents. At different times they each use their local copy of the model to update the underlying row. The result is a race condition in which the last person to update the row wins and the first person’s change is lost. This is shown in Figure 14.3, on the next page.

One solution to the problem is to lock the tables or rows being updated. By preventing others from accessing or updating them, locking overcomes concurrency issues, but it’s a fairly brute-force solution. It assumes that things will go wrong and locks just in case. For this reason, the approach is often called pessimistic locking. Pessimistic locking is unworkable for web applications if you need to ensure consistency across multiple user requests, as it is very hard to manage the locks in such a way that the database doesn’t grind to a halt.

race condition

pessimistic locking

Optimistic locking doesn’t take explicit locks. Instead, just before writing

Optimistic locking

updated data back to a row, it checks to make sure that no one else has

 

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 214

Application Database

id

123

 

 

123

 

123

name

Dave

 

Fred

Dave

pay_type

check

 

check

po

etc...

...

 

 

...

 

...

process 1

 

 

 

 

 

 

 

 

 

o = Order.find(123)

 

 

o.name= 'Fred'

 

 

process 2

 

 

 

 

 

o.save

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

o = Order.find(123)

 

 

o.pay_type = 'po'

 

 

 

 

 

 

 

 

o.save

 

 

 

 

 

 

 

 

 

 

 

Figure 14.3: Race Condition: Second Update Overwrites First

already changed that row. In the Rails implementation, each row contains a version number. Whenever a row is updated, the version number is incremented. When you come to do an update from within your application, Active Record checks the version number of the row in the table against the version number of the model doing the updating. If the two don’t match, it abandons the update and throws an exception.

Optimistic locking is enabled by default on any table that contains an integer column called lock_version. You should arrange for this column to be initialized to zero for new rows, but otherwise you should leave it alone—Active Record manages the details for you.

Let’s see optimistic locking in action. We’ll create a table called counters containing a simple count field along with the lock_version column.

File 6

create table counters (

 

 

id

int

not null auto_increment,

 

count

int

default 0,

 

lock_version

int

default 0,

 

primary key (id)

 

 

 

);

 

 

 

Then we’ll create a row in the table, read that row into two separate model

 

objects, and try to update it from each.

File 13

class Counter < ActiveRecord::Base

 

end

Prepared exclusively for Rida Al Barazi

Report erratum

CRUD—CREATE, READ, UPDATE, DELETE 215

Counter.delete_all

Counter.create(:count => 0)

count1 = Counter.find(:first) count2 = Counter.find(:first)

count1.count += 3 count1.save

count2.count += 4 count2.save

When we run this, we see an exception. Rails aborted the update of count2 because the values it held were stale.

/use/lib/ruby/gems/1.8/gems/activerecord-1.9.0/lib/active_record/locking.rb:42: in ‘update_without_timestamps':

Attempted to update a stale object (ActiveRecord::StaleObjectError)

If you use optimistic locking, you’ll need to catch these exceptions in your application.

You can disable optimistic locking with

ActiveRecord::Base.lock_optimistically = false

Deleting Rows

Active Record supports two styles of row deletion. First, it has two classlevel methods, delete( ) and delete_all( ), that operate at the database level. The delete( ) method takes a single id or an array of ids and deletes the corresponding row(s) in the underlying table. delete_all( ) deletes rows matching a given condition (or all rows if no condition is specified). The return values from both calls depend on the adapter but are typically the number of rows affected. An exception is not thrown if the row doesn’t exist prior to the call.

Order.delete(123)

User.delete([2,3,4,5])

Product.delete_all(["price > ?", @expensive_price])

The various destroy methods are the second form of row deletion provided by Active Record. These methods all work via Active Record model objects.

The destroy( ) instance method deletes from the database the row corresponding to a particular model object. It then freezes the contents of that object, preventing future changes to the attributes.

order = Order.find_by_name("Dave") order.destroy

# ... order is now frozen

There are two class-level destruction methods, destroy( ) (which takes an id or an array of ids) and destroy_all( ) (which takes a condition). Both read the corresponding rows in the database table into model objects and call

Prepared exclusively for Rida Al Barazi

Report erratum