Agile Web Development with Rails phần 5 pptx

55 394 0
Agile Web Development with Rails phần 5 pptx

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

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 mul- tiple 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 col- umn 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. 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,andpaytype—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. 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 partic- ular 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 race condition person to update the row wins and the first person’s change is lost. This isshowninFigure14.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 pessimistic locking 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. 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 Report erratum CRUD—CREATE,READ,UPDATE,DELETE 214 id name pay_type etc 123 Dave check process 1 process 2 o.name= 'Fred' o.save o.pay_type = 'po' o.save 123 Fred check 123 Dave po DatabaseApplication o = Order.find(123) o = Order.find(123) Figure 14.3: Race Condition: Second Update Overwrites First already changed that row. In the Rails implementation, each row con- tains a version number. Whenever a row is updated, the version number is incremented. When you come to do an update from within your appli- cation, 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 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 counter2 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 class- level 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 cor- responding row(s) in the underlying table. delete_all()deletesrowsmatch- ing 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 corre- sponding 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)anddestroy_all( ) (which takes a condition). Both read the corresponding rows in the database table into model objects and call Report erratum RELATIONSHIPS BETWEEN TABLES 216 the instance level destroy( ) method of that object. Neither method returns anything meaningful. 30.days.ago → page 185 Order.destroy_all(["shipped_at < ?", 30.days.ago]) Why do we need both the delete and the destroy class methods? The delete methods bypass the various Active Record callback and validation func- tions, while the destroy methods ensure that they are all invoked. (We talk about callbacks starting on page 264.) In general it is better to use the destroy methods if you want to ensure that your database is consistent according to the business rules defined in your model classes. 14.6 Relationships between Tables Most applications work with multiple tables in the database, and normally there’ll be relationships between some of these tables. Orders will have multiple line items. A line item will reference a particular product. A prod- uct may belong to many different product categories, and the categories may each have a number of different products. Within the database schema, these relationships are expressed by linking tables based on primary key values. 8 If a line item references a product, the line_items table will include a column that holds the primary key value of the corresponding row in the products table. In database parlance, the line_items table is said to have a foreign key reference to the products table. But that’s all pretty low level. In our application, we want to deal with model objects and their relationships, not database rows and key columns. If an order has a number of line items, we’d like some way of iterating over them. If a line item refers to a product, we’d like to be able to say something simple, such as price = line_item.product.price rather than product_id = line_item.product_id product = Product.find(product_id) price = product.price Active Record to the rescue. Part of its ORM magic is that it converts the low-level foreign key relationships in the database into high-level interob- ject mappings. It handles the three basic cases. 8 There’s another style of relationship between model objects in which one model is a subclass of another. We discuss this in Section 15.3, Single Table Inheritance,onpage253. Report erratum RELATIONSHIPS BETWEEN TABLES 217 • One row in table A is associated with zero or one rows in table B. • One row in table A is associated with an arbitrary number of rows in table B. • An arbitrary number of rows in table A are associated with an arbi- trary number of rows in table B. We have to give Active Record a little help when it comes to intertable relationships. This isn’t really Active Record’s fault—it isn’t possible to deduce from the schema what kind of intertable relationships the devel- oper intended. However, the amount of help we have to supply is minimal. Creating Foreign Keys As we discussed earlier, two tables are related when one table contains a foreign key reference to the primary key of another. In the following DDL, the table line_items contains a foreign key reference to the products and orders tables. create table products ( id int not null auto_increment, title varchar(100) not null, /* */ primary key (id) ); create table orders ( id int not null auto_increment, name varchar(100) not null, /* */ primary key (id) ); create table line_items ( id int not null auto_increment, product_id int not null, order_id int not null, quantity int not null default 0, unit_price float(10,2) not null, constraint fk_items_product foreign key (product_id) references products(id), constraint fk_items_order foreign key (order_id) references orders(id), primary key (id) ); It’sworthnotingthatitisn’ttheforeign key constraints that set up the relationships. These are just hints to the database that it should check that the values in the columns reference known keys in the target tables. The DBMS is free to ignore these constraints (and some versions of MySQL do). The intertable relationships are set up simply because the developer chooses to populate the columns product_id and order_id with key values from the products and orders table. Report erratum RELATIONSHIPS BETWEEN TABLES 218 Looking at this DDL, we can see why it’s hard for Active Record to divine the relationships between tables automatically. The orders and products foreign key references in the line_items table look identical. However, the product_id column is used to associate a line item with exactly one product. The order_id column is used to associate multiple line items with a single order. The line item is part of the order but references the product. This example also shows the standard Active Record naming convention. The foreign key column should be named after the class of the target table, converted to lowercase, with _id appended. Note that between the plural- ization and _id appending conventions, the assumed foreign key name will be consistently different from the name of the referenced table. If you have an Active Record model called Person, it will map to the database table peo- ple . A foreign key reference from some other table to the people table will have the column name person_id. The other type of relationship is where some number of one thing is related to some number of another thing (such as products belonging to multiple categories, and categories that contain multiple products). The SQL con- vention for handling this uses a third table, called a join table. The join join table table contains a foreign key for each of the tables it’s linking, so each row in the join table represents a linkage between the two other tables. create table products ( id int not null auto_increment, title varchar(100) not null, /* */ primary key (id) ); create table categories ( id int not null auto_increment, name varchar(100) not null, /* */ primary key (id) ); create table categories_products ( product_id int not null, category_id int not null, constraint fk_cp_product foreign key (product_id) references products(id), constraint fk_cp_category foreign key (category_id) references categories(id) ); Depending on the schema, you might want to put additional informa- tion into the join table, perhaps describing the nature of the relationship between the rows being joined. Rails assumes that a join table is named after the two tables it joins (with the names in alphabetical order). Rails will automatically find the join table categories_products linking categories and products. If you used some other name, you’ll need to add a declaration so Rails can find it. Report erratum RELATIONSHIPS BETWEEN TABLES 219 Specifying Relationships Active Record supports three types of relationship between tables: one- to-one, one-to-many, and many-to-many. You indicate these relatonships by adding declarations to your models: has_one, has_many, belongs_to,and has_and_belongs_to_many. A one-to-one relationship might exist between orders and invoices: for each one-to-one order there’s at most one invoice. We declare this in Rails by saying class Order < ActiveRecord::Base has_one :invoice class Invoice < ActiveRecord::Base belongs_to :order Orders and line items have a one-to-many relationship: there can be any one-to-many number of line items associated with a particular order. In Rails, we’d code this as class Order < ActiveRecord::Base has_many :line_items class LineItem < ActiveRecord::Base belongs_to :order We might categorize our products. A product can belong to many cat- egories, and each category may contain multiple products. This is an example of a many-to-many relationship, expressed in Rails as many-to-many class Product < ActiveRecord::Base has_and_belongs_to_many :categories class Category < ActiveRecord::Base has_and_belongs_to_many :products The various linkage declarations do more than specify the relationships between tables. They each add a number of methods to the model to help navigate between linked objects. Let’s look at these in more detail in the context of the three different kinds of intertable linkage. We’ll also look at the methods each injects into its host class. We summarize them all in Figure 14.5,onpage233. For more in-depth and up-to-date information, see the RDoc documentation for the corresponding methods. Report erratum RELATIONSHIPS BETWEEN TABLES 220 One-to-One Associations invoices id order_id . . . orders id name . . . 1 0 1 class Invoice < ActionRecord::Base belongs_to :order # . . . end class Order < ActionRecord::Base has_one :invoice # . . . end A one-to-one association (or, more accurately, a one-to-zero-or-one rela- tionship) is implemented using a foreign key in one row in one table to reference at most a single row in another table. The preceding figure illus- trates the one-to-one relationship between an order and an invoice: an order either has no invoice referencing it or has just one invoice referenc- ing it. In Active Record we signify this relationship by adding the declaration has_one :invoice to class Order and, at the same time, adding belongs_to :order to class Invoice. (Remember that the belongs_to line must appear in the model for the table that contains the foreign key.) You can associate an invoice with an order from either side of the rela- tionship: you can tell an order that it has an invoice associated with it, or you can tell the invoice that it’s associated with an order. The two are almost equivalent. The difference is in the way they save (or don’t save) objects to the database. If you assign an object to a has_one association in an existing object, that associated object will be automatically saved. an_invoice = Invoice.new( ) order.invoice = an_invoice # invoice gets saved If instead you assign a new object to a belongs_to association, it will never be automatically saved. order = Order.new( ) an_invoice.order = order # Order will not be saved There’s one more difference. If there is already an existing child object when you assign a new object to a has_one association, that existing object Report erratum [...]... => 100, :number => "123 45" ) paul = Account.create(:balance => 200, :number => "54 321") Report erratum 239 T RANSACTIONS File 16 begin Account.transaction do paul.deposit( 350 ) peter.withdraw( 350 ) end rescue puts "Transfer aborted" end puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}" What we see is a little surprising Transfer aborted Paul has 55 0.0 Peter has - 250 .0 Although the database... try to transfer $ 350 , we’ll run Peter into the red, which isn’t allowed by the validation rule Let’s try it File 16 peter = Account.create(:balance => 100, :number => "123 45" ) paul File 16 = Account.create(:balance => 200, :number => "54 321") Account.transaction do paul.deposit( 350 ) peter.withdraw( 350 ) end When we run this, we get an exception reported on the console validations.rb: 652 :in ‘save!': ActiveRecord::RecordInvalid... Account.create(:balance => 100, :number => "123 45" ) paul File 16 = Account.create(:balance => 200, :number => "54 321") Account.transaction do paul.deposit(10) peter.withdraw(10) end We check the database, and, sure enough, the money got transfered mysql> select * from accounts; + + + -+ | id | number | balance | + + + -+ | 5 | 123 45 | 90.00 | | 6 | 54 321 | 210.00 | + + + -+ Now let’s... listing them explicitly as parameters to the transaction( ) method File 16 peter = Account.create(:balance => 100, :number => "123 45" ) paul File 16 = Account.create(:balance => 200, :number => "54 321") begin Account.transaction(peter, paul) do paul.deposit( 350 ) peter.withdraw( 350 ) end rescue puts "Transfer aborted" end puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}" This time we see the... self.transfer(from, to, amount) transaction(from, to) do from.withdraw(amount) to.deposit(amount) end end end Report erratum 240 T RANSACTIONS With this method defined, our transfers are a lot tidier File 16 peter = Account.create(:balance => 100, :number => "123 45" ) paul File 16 = Account.create(:balance => 200, :number => "54 321") Account.transfer(peter, paul, 350 ) rescue puts "Transfer aborted" puts "Paul has... different databases in Rails? The current answer is that you can’t Rails doesn’t support distributed two-phase commits (which is the jargon term for the protocol that lets databases synchronize with each other) However, you can (almost) simulate the effect by nesting transactions Remember that transactions are associated with database connections, and connections are associated with models So, if the... to the table This column is a foreign key reference back Report erratum 2 45 Acts As categories id 1 1 parent_id null 2 6 8 3 1 3 7 2 8 9 5 7 1 6 9 4 3 2 6 3 1 4 5 6 Figure 15. 1: Representing a Tree Using Parent Links in a Table into the same table, linking child rows to their parent row This is illustrated in Figure 15. 1 To show how trees work, let’s create a simple category table, where... ActiveRecord::RecordInvalid from transactions.rb:36:in ‘adjust_balance_and_save' from transactions.rb: 25: in ‘withdraw' : : from transactions.rb:71 Looking in the database, we can see that the data remains unchanged mysql> select * from accounts; + + + -+ | id | number | balance | + + + -+ | 7 | 123 45 | 100.00 | | 8 | 54 321 | 200.00 | + + + -+ However, there’s a trap waiting for you here The transaction... account2.withdraw(100) However, we have to be careful What happens if the deposit succeeds but for some reason the withdrawal fails (perhaps the customer is overdrawn)? We’ll have added $100 to the balance in account1 without a corresponding deduction from account2 In effect we’ll have created $100 out of thin air Transactions to the rescue A transaction is something like the Three Musketeers with their... database, unless an exception is raised within the block, in which case all changes are rolled back and the database is left untouched Because transactions exist in the context of a database connection, we have to invoke them with an Active Record class as a receiver Thus we could write Account.transaction do account1.deposit(100) account2.withdraw(100) end Let’s experiment with transactions We’ll start by . another. We discuss this in Section 15. 3, Single Table Inheritance,onpage 253 . Report erratum RELATIONSHIPS BETWEEN TABLES 217 • One row in table A is associated with zero or one rows in table B. •. relationship between the rows being joined. Rails assumes that a join table is named after the two tables it joins (with the names in alphabetical order). Rails will automatically find the join table categories_products. an invoice with an order from either side of the rela- tionship: you can tell an order that it has an invoice associated with it, or you can tell the invoice that it’s associated with an order.

Ngày đăng: 07/08/2014, 00:22

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan