4.7. Editing a Record

Adding data is quite nice, but often you want to edit a record. To show how that's done I use the album database from Section 4.6, “Searching and Finding with Queries”.

Simple Editing

Simple editing of a record takes place in the following steps:
  1. Finding the record and creating a corresponding instance
  2. Changing the attribute
  3. Saving the record via the method save
We are now searching for the album The Beatles and changing its name to A Test:
$ rails console
Loading development environment (Rails 4.0.0)
>> beatles_album = Album.where(name: 'The Beatles').first
  Album Load (0.3ms)  SELECT "albums".* FROM "albums" WHERE "albums"."name" = 'The Beatles' ORDER BY "albums"."id" ASC LIMIT 1
=> #<Album id: 10, name: "The Beatles", release_year: 1968, created_at: "2013-07-15 18:59:50", updated_at: "2013-07-15 18:59:50">
>> beatles_album.name
=> "The Beatles"
>> beatles_album.name = 'A Test'
=> "A Test"
>> beatles_album.save
   (0.2ms)  begin transaction
  SQL (2.1ms)  UPDATE "albums" SET "name" = ?, "updated_at" = ? WHERE "albums"."id" = 10  [["name", "A Test"], ["updated_at", Tue, 16 Jul 2013 08:08:00 UTC +00:00]]
   (2.6ms)  commit transaction
=> true
>> exit
$

changed?

If you are not sure if a record has been changed and not yet saved, you can check via the method changed?:
$ rails console
Loading development environment (Rails 4.0.0)
>> beatles_album = Album.where(id: 10).first
  Album Load (0.1ms)  SELECT "albums".* FROM "albums" WHERE "albums"."id" = 10 ORDER BY "albums"."id" ASC LIMIT 1
=> #<Album id: 10, name: "A Test", release_year: 1968, created_at: "2013-07-15 18:59:50", updated_at: "2013-07-16 08:08:00">
>> beatles_album.changed?
=> false
>> beatles_album.name = 'The Beatles'
=> "The Beatles"
>> beatles_album.changed?
=> true
>> beatles_album.save
   (0.2ms)  begin transaction
  SQL (2.3ms)  UPDATE "albums" SET "name" = ?, "updated_at" = ? WHERE "albums"."id" = 10  [["name", "The Beatles"], ["updated_at", Tue, 16 Jul 2013 08:23:52 UTC +00:00]]
   (2.9ms)  commit transaction
=> true
>> beatles_album.changed?
=> false
>> exit
$

update_attributes

With the method update_attributes you can change several attributes of an object in one go and then immediately save them automatically.
Let's use this method within the example used in the section called “Simple Editing”:
$ rails console
Loading development environment (Rails 4.0.0)
>> first_album = Album.first
  Album Load (0.1ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" ASC LIMIT 1
=> #<Album id: 1, name: "Sgt. Pepper's Lonely Hearts Club Band", release_year: 1967, created_at: "2013-07-15 18:59:50", updated_at: "2013-07-15 18:59:50">
>> first_album.changed?
=> false
>> first_album.update_attributes(name: 'Another Test')
   (0.2ms)  begin transaction
  SQL (2.2ms)  UPDATE "albums" SET "name" = ?, "updated_at" = ? WHERE "albums"."id" = 1  [["name", "Another Test"], ["updated_at", Tue, 16 Jul 2013 08:25:24 UTC +00:00]]
   (3.0ms)  commit transaction
=> true
>> first_album.changed?
=> false
>> Album.first
  Album Load (0.5ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" ASC LIMIT 1
=> #<Album id: 1, name: "Another Test", release_year: 1967, created_at: "2013-07-15 18:59:50", updated_at: "2013-07-16 08:25:24">
>> 
This kind of update can also be chained with a where method:
>> Album.where(name: 'Another Test').first.update_attributes(name: "Sgt. Pepper's Lonely Hearts Club Band")
  Album Load (0.2ms)  SELECT "albums".* FROM "albums" WHERE "albums"."name" = 'Another Test' ORDER BY "albums"."id" ASC LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.9ms)  UPDATE "albums" SET "name" = ?, "updated_at" = ? WHERE "albums"."id" = 1  [["name", "Sgt. Pepper's Lonely Hearts Club Band"], ["updated_at", Tue, 16 Jul 2013 08:27:25 UTC +00:00]]
   (3.0ms)  commit transaction
=> true
>> exit
$

Locking

There are many ways of locking a database. By default, Rails uses optimistic locking of records. To activate locking you need to have an attribute with the name lock_version which has to be an integer. To show how it works I'll create a new Rails project with a Product model. Than I'll try to change the price of the first Product on two different instances. The second change will raise an ActiveRecord::StaleObjectError.
Example setup:
$ rails new shop
  [...]
$ cd shop
$ rails generate model Product name 'price:decimal{8,2}' lock_version:integer 
  [...]
$ rake db:migrate
  [...]
$
Raising an ActiveRecord::StaleObjectError:
$ rails console
Loading development environment (Rails 4.0.0)
>> Product.create(name: 'Orange', price: 0.5)
   (0.1ms)  begin transaction
  SQL (4.7ms)  INSERT INTO "products" ("created_at", "lock_version", "name", "price", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 16 Jul 2013 11:24:56 UTC +00:00], ["lock_version", 0], ["name", "Orange"], ["price", #<BigDecimal:7f958e0f5de0,'0.5E0',9(45)>], ["updated_at", Tue, 16 Jul 2013 11:24:56 UTC +00:00]]
   (3.2ms)  commit transaction
=> #<Product id: 1, name: "Orange", price: #<BigDecimal:7f958e0f5de0,'0.5E0',9(45)>, lock_version: 0, created_at: "2013-07-16 11:24:56", updated_at: "2013-07-16 11:24:56">
>> a = Product.first
  Product Load (0.3ms)  SELECT "products".* FROM "products" ORDER BY "products"."id" ASC LIMIT 1
=> #<Product id: 1, name: "Orange", price: #<BigDecimal:7f958d098768,'0.5E0',9(45)>, lock_version: 0, created_at: "2013-07-16 11:24:56", updated_at: "2013-07-16 11:24:56">
>> b = Product.first
  Product Load (0.4ms)  SELECT "products".* FROM "products" ORDER BY "products"."id" ASC LIMIT 1
=> #<Product id: 1, name: "Orange", price: #<BigDecimal:7f958e1268a0,'0.5E0',9(45)>, lock_version: 0, created_at: "2013-07-16 11:24:56", updated_at: "2013-07-16 11:24:56">
>> a.price = 0.6
=> 0.6
>> a.save
   (0.2ms)  begin transaction
   (0.5ms)  UPDATE "products" SET "price" = 0.6, "updated_at" = '2013-07-16 11:25:41.931401', "lock_version" = 1 WHERE ("products"."id" = 1 AND "products"."lock_version" = 0)
   (2.0ms)  commit transaction
=> true
>> b.price = 0.7
=> 0.7
>> b.save
   (0.1ms)  begin transaction
   (0.2ms)  UPDATE "products" SET "price" = 0.7, "updated_at" = '2013-07-16 11:25:49.170722', "lock_version" = 1 WHERE ("products"."id" = 1 AND "products"."lock_version" = 0)
   (0.1ms)  rollback transaction
ActiveRecord::StaleObjectError: Attempted to update a stale object: Product
[...]
>> exit
$
You have to deal with the conflict by rescuing the exception and fix the conflict depending on your business logic. Please make sure to add a lock_version hidden field in your forms while using this mechanism with a WebGUI.