Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

4.7. Einen Datensatz verändern

Daten hinzufügen ist ja schon ganz nett, aber hin und wieder will man ja auch einen Datensatz editieren. Wir benutzen zur Beschreibung die Album-Datenbank aus Abschnitt 4.6, „Suchen und Finden mit Queries“.

Einfaches Editieren

Das einfache Editieren eines Datensatzes geschieht in folgenden Schritten:
  1. Suche des Datensatzes und Erstellen einer entsprechenden Instanz
  2. Verändern der Werte
  3. Abspeichern des Datensatzes mit der Methode save
Wir suchen uns jetzt das Album namens The Beatles und verändern den Namen in Ein Test:
MacBook:jukebox xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > album = Album.where(:name => 'The Beatles').first
  Album Load (0.6ms)  SELECT "albums".* FROM "albums" WHERE "albums"."name" = 'The Beatles' LIMIT 1
 => #<Album id: 10, name: "The Beatles", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-04-30 14:51:03"> 
1.9.3p194 :002 > album.name
 => "The Beatles" 
1.9.3p194 :003 > album.name = 'Ein Test'
 => "Ein Test" 
1.9.3p194 :004 > album.save
   (0.1ms)  begin transaction
   (0.5ms)  UPDATE "albums" SET "name" = 'Ein Test', "updated_at" = '2012-05-06 13:04:54.261940' WHERE "albums"."id" = 10
   (1.7ms)  commit transaction
 => true 
1.9.3p194 :005 > Album.last
  Album Load (0.3ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" DESC LIMIT 1
 => #<Album id: 10, name: "Ein Test", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-05-06 13:04:54"> 
1.9.3p194 :006 > exit
MacBook:jukebox xyz$  

changed?

Wenn man sich nicht sicher ist, ob ein Datensatz verändert und noch nicht abgespeichert wurde, dann kann man dies mit der changed?-Methode herausfinden:
MacBook:jukebox xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > album = Album.last
  Album Load (0.2ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" DESC LIMIT 1
 => #<Album id: 10, name: "Ein Test", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-05-06 13:04:54"> 
1.9.3p194 :002 > album.changed?
 => false 
1.9.3p194 :003 > album.name = 'The Beatles'
 => "The Beatles" 
1.9.3p194 :004 > album.changed?
 => true 
1.9.3p194 :005 > album.save
   (0.1ms)  begin transaction
   (0.5ms)  UPDATE "albums" SET "name" = 'The Beatles', "updated_at" = '2012-05-06 13:06:24.359737' WHERE "albums"."id" = 10
   (3.5ms)  commit transaction
 => true 
1.9.3p194 :006 > album.changed?
 => false 
1.9.3p194 :007 > exit
MacBook:jukebox xyz$

update_attributes

Mit der Methode update_attributes kann man verschiedene Attribute eines Objektes auf einmal ändern und danach automatisch direkt speichern.
Setzen wir damit das Beispiel von „Einfaches Editieren“ um:
MacBook:jukebox xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > album = Album.last
  Album Load (0.2ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" DESC LIMIT 1
 => #<Album id: 10, name: "The Beatles", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-05-06 13:06:24"> 
1.9.3p194 :002 > album.changed?
 => false 
1.9.3p194 :003 > album.update_attributes(:name => 'Noch ein Test')
   (0.1ms)  begin transaction
   (0.5ms)  UPDATE "albums" SET "name" = 'Noch ein Test', "updated_at" = '2012-05-06 13:08:21.064466' WHERE "albums"."id" = 10
   (3.8ms)  commit transaction
 => true 
1.9.3p194 :004 > album.changed?
 => false 
1.9.3p194 :005 > Album.last
  Album Load (0.3ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" DESC LIMIT 1
 => #<Album id: 10, name: "Noch ein Test", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-05-06 13:08:21"> 
1.9.3p194 :006 > exit
MacBook:jukebox xyz$
Ein solches Update lässt sich auch direkt an eine where-Methode anknüpfen und ist dann fast atomar:
MacBook:jukebox xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Album.where(:name => 'Noch ein Test').first.update_attributes(:name => 'The Beatles')
  Album Load (0.2ms)  SELECT "albums".* FROM "albums" WHERE "albums"."name" = 'Noch ein Test' LIMIT 1
   (0.1ms)  begin transaction
   (0.4ms)  UPDATE "albums" SET "name" = 'The Beatles', "updated_at" = '2012-05-06 13:13:11.870462' WHERE "albums"."id" = 10
   (3.8ms)  commit transaction
 => true 
1.9.3p194 :002 > Album.last
  Album Load (0.3ms)  SELECT "albums".* FROM "albums" ORDER BY "albums"."id" DESC LIMIT 1
 => #<Album id: 10, name: "The Beatles", release_year: 1968, created_at: "2012-04-30 14:51:03", updated_at: "2012-05-06 13:13:11"> 
1.9.3p194 :003 > exit
MacBook:jukebox xyz$

Locking

Per Default benutzt Rails sogenanntes optimistisches Locking (optimistic Locking) der Datensätze. Dies kann aber umgeschaltet werden. Da für die meisten Anwender optimistisches Locking optimal ist und viele vor diesem Absatz nicht mal über das Problem nachgedacht haben, werde ich nicht weiter darauf eingehen, sondern auf die ri-Hilfe verweisen. Wer pessimistisches Locking benötigt, der findet dort die notwendigen Parameter.
MacBook:jukebox xyz$ ri -T ActiveRecord::Locking::Optimistic
ActiveRecord::Locking::Optimistic

(from gem activerecord-3.2.3)
------------------------------------------------------------------------------
What is Optimistic Locking

Optimistic locking allows multiple users to access the same record for edits,
and assumes a minimum of conflicts with the data. It does this by checking
whether another process has made changes to a record since it was opened, an
ActiveRecord::StaleObjectError exception is thrown if that has occurred
and the update is ignored.

Check out ActiveRecord::Locking::Pessimistic for an alternative.

Usage

Active Records support optimistic locking if the field lock_version is
present. Each update to the record increments the lock_version column
and the locking facilities ensure that records instantiated twice will let the
last one saved raise a StaleObjectError if the first was also updated.
Example:

  p1 = Person.find(1)
  p2 = Person.find(1)

  p1.first_name = "Michael"
  p1.save

  p2.first_name = "should fail"
  p2.save # Raises a ActiveRecord::StaleObjectError

Optimistic locking will also check for stale data when objects are destroyed.
Example:

  p1 = Person.find(1)
  p2 = Person.find(1)

  p1.first_name = "Michael"
  p1.save

  p2.destroy # Raises a ActiveRecord::StaleObjectError

You're then responsible for dealing with the conflict by rescuing the
exception and either rolling back, merging, or otherwise apply the business
logic needed to resolve the conflict.

This locking mechanism will function inside a single Ruby process. To make it
work across all web requests, the recommended approach is to add
lock_version as a hidden field to your form.

You must ensure that your database schema defaults the lock_version
column to 0.

This behavior can be turned off by setting
ActiveRecord::Base.lock_optimistically = false. To override the name of
the lock_version column, invoke the set_locking_column method.
This method uses the same syntax as set_table_name
------------------------------------------------------------------------------
MacBook:jukebox xyz$