4.15. Validierung
(Validation)
Einer der häufigsten Fehlerquellen in Programmen sind nicht valide
Datensätze. ActiveRecord stellt mit validates
eine
schnelle und einfache Möglichkeit zur Validierung von Datensätzen zur
Verfügung. So können Sie sicher sein, dass auch wirklich nur sinnvolle
Datensätze den Weg in Ihre Datenbank finden.
Zu jedem Model gibt es im Verzeichnis
app/models/
eine passende Datei. In dieser Datei
können wir nicht nur Datenbankabhängigkeiten definieren, sondern auch
sämtliche Validations realisieren. Der Vorteil ist: alles an einer Stelle.
Convention over Configuration!
Ohne jegliche Validierung können wir problemlos einen leeren
Datensatz in beiden Modellen anlegen:
SW:shop stefan$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > Product.create
(0.1ms) begin transaction
SQL (8.9ms) INSERT INTO "products" ("created_at", "expiration_date", "in_stock", "name", "price", "updated_at", "weight") VALUES (?, ?, ?, ?, ?, ?, ?) [["created_at", Mon, 10 Sep 2012 14:45:33 UTC +00:00], ["expiration_date", nil], ["in_stock", nil], ["name", nil], ["price", nil], ["updated_at", Mon, 10 Sep 2012 14:45:33 UTC +00:00], ["weight", nil]]
(0.9ms) commit transaction
=> #<Product id: 1, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: "2012-09-10 14:45:33", updated_at: "2012-09-10 14:45:33">
1.9.3p194 :002 > exit
SW:shop stefan$
Dieser Datensatz macht aber in der Realität keinen Sinn. Ein
Product
braucht einen name
und
einen price
. Deshalb kann man in ActiveRecord
Validierungen definieren. Damit können Sie als Programmierer
sicherstellen, dass nur für Sie valide Datensätze in Ihrer Datenbank
abgespeichert werden.
Ich greife zur Veranschaulichung des Mechanismus den
presence
Helper vorweg. Bitte füllen Sie Ihre
app/model/product.rb
mit folgendem
Inhalt:
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true
validates :price,
:presence => true
end
Jetzt versuchen wir noch mal in der Console einen leeren Datensatz
anzulegen:
SW:shop stefan$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > product = Product.create
(0.0ms) begin transaction
(0.1ms) rollback transaction
=> #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :002 > product.valid?
=> false
1.9.3p194 :003 >
Obwohl wir mit der Methode
create
(siehe
„create“) einen neuen Datensatz anlegen
wollten, ist dies nicht geschehen. Der Validierungs-Mechanismus hat vor
dem Abspeichern des Datensatzes eingegriffen. Es wird also erst validiert
und dann gespeichert.
Haben wir Zugriff auf die Fehler? Ja, mit der Methode
errors
bzw. mit
errors.messages
können wir uns die aufgetretenen
Fehler anschauen:
1.9.3p194 :003 > product.errors
=> #<ActiveModel::Errors:0x007ff88537fae8 @base=#<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:name=>["can't be blank"], :price=>["can't be blank"]}>
1.9.3p194 :004 > product.errors.messages
=> {:name=>["can't be blank"], :price=>["can't be blank"]}
1.9.3p194 :005 >
Diese Fehlermeldung ist für einen menschlichen und
englischsprachigen Benutzer definiert (mehr dazu und wie die Fehler ins
Deutsche übersetzt werden können in
Kapitel 10, Internationalisierung).
Erst wenn wir den Attributen
name
und
price
einen Wert zuweisen, können wir das Objekt
abspeichern:
1.9.3p194 :005 > product.name = 'Milch (1 Liter)'
=> "Milch (1 Liter)"
1.9.3p194 :006 > product.price = 0.45
=> 0.45
1.9.3p194 :007 > product.save
(0.1ms) begin transaction
SQL (6.4ms) INSERT INTO "products" ("created_at", "expiration_date", "in_stock", "name", "price", "updated_at", "weight") VALUES (?, ?, ?, ?, ?, ?, ?) [["created_at", Mon, 10 Sep 2012 14:52:44 UTC +00:00], ["expiration_date", nil], ["in_stock", nil], ["name", "Milch (1 Liter)"], ["price", #<BigDecimal:7ff8852ea5d8,'0.45E0',9(45)>], ["updated_at", Mon, 10 Sep 2012 14:52:44 UTC +00:00], ["weight", nil]]
(0.8ms) commit transaction
=> true
1.9.3p194 :008 >
Die Methode
valid?
gibt als Boolean aus,
ob ein Objekt valide ist. So kann man schon vor dem eigentlichen
Abspeichern eine entsprechende Kontrolle machen:
1.9.3p194 :008 > product = Product.new
=> #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :009 > product.valid?
=> false
1.9.3p194 :010 >
save( :validate
=> false )
Wie so oft im Leben kann man auch hier alles umgehen. Wenn man der
Methode
save
als Parameter
:validate
=> false
mitgibt, dann wird der Datensatz von
Validation
abgespeichert:
1.9.3p194 :010 > product = Product.new
=> #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :011 > product.save
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> false
1.9.3p194 :012 > product.valid?
=> false
1.9.3p194 :013 > product.save(:validate => false)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "products" ("created_at", "expiration_date", "in_stock", "name", "price", "updated_at", "weight") VALUES (?, ?, ?, ?, ?, ?, ?) [["created_at", Tue, 08 May 2012 15:20:44 UTC +00:00], ["expiration_date", nil], ["in_stock", nil], ["name", nil], ["price", nil], ["updated_at", Tue, 08 May 2012 15:20:44 UTC +00:00], ["weight", nil]]
(3.5ms) commit transaction
=> true
1.9.3p194 :014 > exit
MacBook:shop xyz$
Warnung
Ich gehe davon aus, dass Sie die hier auftauchende Problematik
verstehen. Diese Möglichkeit bitte nur einsetzen, wenn es einen guten
Grund gibt. Sonst könnte man sich die ganze Validierung ja auch direkt
sparen.
In unserem Model product
gibt es ein paar Felder,
die auf jeden Fall ausgefüllt werden müssen. Das erreichen wir mit
presence
.
app/models/product.rb
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true
validates :price,
:presence => true
end
Wenn wir damit einen leeren User-Datensatz anlegen wollen, bekommen
wir sehr viele Validierungsfehler:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > product = Product.create
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :002 > product.errors
=> #<ActiveModel::Errors:0x007ffe9189e0d0 @base=#<Product id: nil, name: nil, price: nil, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:name=>["can't be blank"], :price=>["can't be blank"]}>
1.9.3p194 :003 >
Erst wenn wir alle Angaben gemacht haben, kann der Datensatz
gespeichert werden:
1.9.3p194 :003 > product.name = 'Milch (1 Liter)'
=> "Milch (1 Liter)"
1.9.3p194 :004 > product.price = 0.45
=> 0.45
1.9.3p194 :005 > product.save
(0.1ms) begin transaction
SQL (5.2ms) INSERT INTO "products" ("created_at", "expiration_date", "in_stock", "name", "price", "updated_at", "weight") VALUES (?, ?, ?, ?, ?, ?, ?) [["created_at", Tue, 08 May 2012 15:25:22 UTC +00:00], ["expiration_date", nil], ["in_stock", nil], ["name", "Milch (1 Liter)"], ["price", #<BigDecimal:7ffe91a2d270,'0.45E0',9(45)>], ["updated_at", Tue, 08 May 2012 15:25:22 UTC +00:00], ["weight", nil]]
(3.9ms) commit transaction
=> true
1.9.3p194 :006 > exit
MacBook:shop xyz$
Mit length
können Sie die Länge eines
bestimmten Attributes eingrenzen. Erklärt sich am Beispiel am
einfachsten.
app/models/product.rb
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true,
:length => { :in => 2..20 }
validates :price,
:presence => true
end
Wenn wir jetzt einen
Product
mit einem
name
aus einem Buchstaben abspeichern wollen,
dann bekommen wir einen Fehler:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > product = Product.create(:name => 'M', :price => 0.45)
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Product id: nil, name: "M", price: #<BigDecimal:7fd6ad8cec28,'0.45E0',9(45)>, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :002 > product.errors
=> #<ActiveModel::Errors:0x007fd6ad88fe38 @base=#<Product id: nil, name: "M", price: #<BigDecimal:7fd6ad8d19c8,'0.45E0',9(45)>, weight: nil, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:name=>["is too short (minimum is 2 characters)"]}>
1.9.3p194 :003 > exit
MacBook:shop xyz$
length
kann mit folgenden Optionen
aufgerufen werden:
Mit numericality
können Sie überprüfen, ob
ein Attribut eine Zahl ist. Erklärt sich am Beispiel am
einfachsten.
app/models/product.rb
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true,
:length => { :in => 2..20 }
validates :price,
:presence => true
validates :weight,
:numericality => true
end
Wenn wir das Gewicht (
weight
) fehlerhaft mit oder
ganz aus Buchstaben definieren, dann bekommen wir einen
Validierungsfehler:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > product = Product.create(:name => 'Milch (1 Liter)', :price => 0.45, :weight => 'abc')
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Product id: nil, name: "Milch (1 Liter)", price: #<BigDecimal:7f8594def848,'0.45E0',9(45)>, weight: 0, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :002 > product.errors
=> #<ActiveModel::Errors:0x007f8594ba3878 @base=#<Product id: nil, name: "Milch (1 Liter)", price: #<BigDecimal:7f8594df3588,'0.45E0',9(45)>, weight: 0, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:weight=>["is not a number"]}>
1.9.3p194 :003 > exit
MacBook:shop xyz$
Tipp
Auch wenn ein Attribute in der Datenbank als String abgelegt wird,
kann man mit numericality
den Inhalt auf eine
Zahl festlegen.
numericality
kann mit folgenden Optionen
aufgerufen werden:
Anmerkung
Die folgenden Validationen können als Wert auch ein Proc oder
ein Symbol, das mit einer Methode korrespondiert, enthalten:
:greater_than
,
:greater_than_or_equal_to
,
:equal_to
,
:less_than
,
:less_than_or_equal_to.
Beispiel dazu aus
ri
validates_numericality_of:
validates_numericality_of :width, :less_than => Proc.new { |person| person.height }
Mit uniqueness
können Sie definieren, dass
der Wert dieses Attributes einzigartig in der Datenbank ist. Wenn ein
Produkt in der Datenbank einen eindeutigen und nicht doppelt vorhandenen
Namen haben soll, dann geht das mit dieser Validierung:
app/models/product.rb
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true,
:uniqueness => true
end
Wenn wir jetzt ein neues
Product
mit einem
schon existierenden
name
einrichten wollen,
bekommen wir einen Fehler ausgegeben:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > Product.last
Product Load (0.2ms) SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT 1
=> #<Product id: 4, name: "Milch (1 Liter)", price: #<BigDecimal:7fc6e9695160,'0.45E0',9(45)>, weight: nil, in_stock: nil, expiration_date: nil, created_at: "2012-05-08 15:25:22", updated_at: "2012-05-08 15:25:22">
1.9.3p194 :002 > product = Product.create(:name => 'Milch (1 Liter)', :price => 0.45, :weight => 1000)
(0.1ms) begin transaction
Product Exists (0.2ms) SELECT 1 FROM "products" WHERE "products"."name" = 'Milch (1 Liter)' LIMIT 1
(0.1ms) rollback transaction
=> #<Product id: nil, name: "Milch (1 Liter)", price: #<BigDecimal:7fc6e9f7cb70,'0.45E0',9(45)>, weight: 1000, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :003 > product.errors
=> #<ActiveModel::Errors:0x007fc6e9432300 @base=#<Product id: nil, name: "Milch (1 Liter)", price: #<BigDecimal:7fc6e9b09250,'0.45E0',9(45)>, weight: 1000, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:name=>["has already been taken"]}>
1.9.3p194 :004 > exit
MacBook:shop xyz$
Warnung
Die Validierung mit
uniqueness
ist kein
absoluter Garant für eine echte Einzigartigkeit des Attributes in der
Datenbank. Es kann dabei zu einer Race-Condition kommen (siehe
http://de.wikipedia.org/wiki/Race_Condition
).
Die detaillierte Diskussion dieses Effektes übersteigt allerdings die
Tiefe eines Einsteiger-Buches (es handelt sich um ein extrem seltenes
Phänomen).
uniqueness
kann mit folgenden Optionen
aufgerufen werden:
:scope
Definiert einen Geltungsbereich für die Einzigartigkeit.
Wenn wir eine anders strukturierte Telefonnummerndatenbank hätten
(mit nur einem Feld für die Telefonnummer), dann könnten wir damit
definieren, dass eine Telefonnummer pro User nur einmal
gespeichert werden darf. Das sähe dann so aus:
validates :name,
:presence => true,
:uniqueness => { :scope => :user_id }
:case_sensitive
Überprüft die Einzigartigkeit auch auf Groß- und
Kleinschreibung. Default: False. Beispiel:
validates :name,
:presence => true,
:uniqueness => { :case_sensitive => true }
Mit inclusion
können Sie definieren, aus
welchen Werten der Inhalt dieses Attributes erstellt werden kann. Bei
unserem Beispiel können wir das am Attribute
in_stock
veranschaulishen.
app/models/product.rb
class Product < ActiveRecord::Base
attr_accessible :expiration_date, :in_stock, :name, :price, :weight
validates :name,
:presence => true,
:length => { :in => 2..20 }
validates :price,
:presence => true
validates :in_stock,
:inclusion => { :in => [true, false] }
end
In unserem Datenmodel muss ein
Product
als
in_stock
entweder
true
oder
false
sein (es darf also kein nil geben). Wenn wir einen
anderen Wert als
true
oder
false
eingeben, wird
ein Validation-Fehler gemeldet:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.8)
1.9.3p194 :001 > product = Product.create(:name => 'Milch fettarm (1 Liter)', :price => 0.45, :weight => 1000)
(0.1ms) begin transaction
Product Exists (0.2ms) SELECT 1 FROM "products" WHERE "products"."name" = 'Milch fettarm (1 Liter)' LIMIT 1
(0.1ms) rollback transaction
=> #<Product id: nil, name: "Milch fettarm (1 Liter)", price: #<BigDecimal:7fd125470f38,'0.45E0',9(45)>, weight: 1000, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>
1.9.3p194 :002 > product.errors
=> #<ActiveModel::Errors:0x007fd12513a648 @base=#<Product id: nil, name: "Milch fettarm (1 Liter)", price: #<BigDecimal:7fd125475bf0,'0.45E0',9(45)>, weight: 1000, in_stock: nil, expiration_date: nil, created_at: nil, updated_at: nil>, @messages={:in_stock=>["is not included in the list"]}>
1.9.3p194 :003 > exit
MacBook:shop xyz$
Tipp
Denken Sie immer an die Macht von Ruby! Sie können so z. B. das
Enumerable-Objekt immer live aus einer anderen Datenbank generieren. Das
heißt, die Validation ist nicht statisch definiert.
inclusion
kann mit folgenden Optionen
aufgerufen werden:
:message
Um eine eigene Fehlermeldung auszugeben. Default: "is not
included in the list". Beispiel:
validates :in_stock,
:inclusion => { :in => [true, false],
:message => 'this one is not allowed' }