10 Datenbankzugriff mit ActiveRecord
ActiveRecord oder wie man Daten aus einer Datenbank wie Objekte behandelt.
Domain Specific Language
ActiveRecord ist ein Framework, das als domänenspezifische Programmiersprache (Domain Specific Language, DSL), d. h. als problemorientierte Programmiersprache, die nur einen bestimmten fachlichen Bereich unterstützt, den objektorientierten Zugriff auf relationale Datenbanksysteme wie MySQL, PostgreSQL, Oracle usw. steuert. Da es sich um ein eigenes Framework handelt, können Sie ActiveRecord auch außerhalb von Rails nutzen, aber dazu später mehr.
10.1 Einführung
Entwurfsmuster
Den Begriff ActiveRecord hat Martin Fowler in seinem Buch Patterns für Enterprise Application-Architekturen zur Benennung eines Entwurfsmusters (Desgin Pattern) zur Abbildung von objektorientierten Daten auf relationale Daten und umgekehrt verwendet.
Abbildung Model-View-Controller Entwurfsmuster
Martin Fowler hält das ActiveRecord-Framework in Rails für die beste Implementierung des Entwurfsmusters. Nach seiner Definition ist ActiveRecord Folgendes:
Ein Objekt, das eine Zeile in einer Datenbanktabelle oder in einer Sicht umhüllt, die Datenbankzugriffe kapselt und Domänenlogik zu diesen Daten hinzufügt. Das hört sich etwas kompliziert an, ist aber im Prinzip ganz einfach.
Models
Angenommen, wir haben eine Datenbank mit den Tabellen products und clients zur Verwaltung von Produkten und Kunden. Zu jeder Tabelle gibt es jeweils eine entsprechende ActiveRecord-Klasse, die Models. Die Models sind für die Datenbankoperationen zuständig. In unserem Fall heißen die beiden Models Product und Client . Nach einer Konvention in Rails werden Tabellennamen mit Kleinbuchstaben und im Plural benannt und die Klassennamen im Singular mit einem führenden Großbuchstaben.
Abbildung Jede Klasse entspricht einer Tabelle.
Jede Zeile in der Tabelle products wird durch ein Objekt bzw. eine Instanz der ActiveRecord-Klasse Product repräsentiert.
Abbildung Jedes Objekt entspricht einer Zeile.
Klassen-Methoden
Jede ActiveRecord-Klasse besitzt u. a. folgende (Klassen-)Methoden:
- new(...)
Erzeugt ein neues ActiveRecord-Objekt (wird nicht gespeichert).
- create(...)
Dient zum Erzeugen eines ActiveRecord-Objekts und anschließendem Speichern in der Datenbank.
- find(2)
Mit Angabe einer ID wird der Datensatz mit dieser ID als ActiveRecord-Objekt zurückgeliefert.
- find(:all, :conditions=>...)
Zum Suchen von Daten. Das Ergebnis ist ein Array von ActiveRecord-Objekten.
Es ist auch möglich, eigene Methoden hinzuzufügen, wie z. B. eine Methode best_offer, die das günstigste Produkt aus der Produkt-Tabelle zurückliefert.
Jedes ActiveRecord-Objekt besitzt mindestens folgende Operationen:
- save
zum Speichern eines Objekts in der Datenbank.
- update
zum Ändern von allen oder einzelnen Attributen des Objekts.
- destory, delete
zum Löschen eines Objekts.
Vor- und Nachteile
Interessante Funktion
ActiveRecord bietet folgende sehr interessante Funktionen an:
- Validierung
Zu jedem Attribut kann man Validierungs-Regeln angeben, die festlegen, welchen Kriterien das Attribut entsprechen muss. Zum Beispiel kann man vorgeben, welche Attribute einen Werte haben müssen oder dass nur positive Zahlen erlaubt sind. Erst wenn alle Validierungs-Regeln erfüllt sind, kann das Objekt in der Datenbank gespeichert werden.
- Before- und After-Filter
Filter erlauben dem Entwickler, bestimmte Befehle vor oder nach dem Speichern eines Objekts durchzuführen. Zum Beispiel kann vor dem Speichern ein Feld automatisch aktualisiert werden.
- Assoziation bzw. Relationen
Auf sehr einfache Weise können Relationen, wie z. B. 1:n-Relationen, n:m-Relationen usw. zwischen den Model-Klassen abgebildet werden.
- Migrations
Mit Migrations ist es möglich, die Tabellenstruktur in Ruby zu beschreiben und auch Änderungen vorzunehmen wie z. B. das Hinzufügen oder Löschen von Spalten.
- Automatische Attribute created_at und updated_at
Wenn sich in der Tabelle die Felder created_at (oder created_on ) und updated_at (oder updated_on ) vom Datentyp DateTime befinden, werden sie automatisch aktualisiert.
- Single Table Inheritance
ActiveRecord unterstützt auch das Vererben von Models mit der Technik Single Table Inheritance.
- Transaktionen
Eine Transaktion gewährleistet, dass eine Gruppe von Datenbankoperationen gemeinsam ausgeführt werden, oder eben nicht, falls eine Operation scheitert.
Solange man sich auf Schienen bewegt, d. h. die Konvention von ActiveRecord beachtet, ist der Einsatz von ActiveRecord relativ leicht. Abweichungen von dem Standard sind meist nicht leicht umzusetzen. Folgendes wird in ActiveRecord nicht direkt unterstützt bzw. ist nicht so leicht zu realisieren:
- Stored Procedures
- Views
- zusammengesetzte IDs
- Abweichung von der Namenskonvention
Unterstützte Datenbanksysteme
Relationale Datenbanksysteme
Rails unterstützt die folgenden relationalen Datenbanksysteme (Relational Database Management Systems):
- DB2
DB2 ist ein kommerzielles Datenbanksystem von IBM.
- Firebird
Firebird ist die Open-Source Variante des kommerziell von Borland vertriebenen Datenbanksystems InterBase.
- Frontbase
Frontbase ist ein kommerzielles Datenbanksystem für Mac OS X.
- MySQL
MySQL ist das meistverwendete Open-Source-Datenbanksystem.
- Openbase
Openbase ist ein kommerzielles Datenbanksystem für Mac OS X.
- Oracle
Das leistungsfähige, kommerzielle Datenbanksystem Oracle ist leider nicht für Mac OS X verfügbar.
- PostgreSQL
Das Open-Source-Datenbanksystem PostgreSQL kann auch für Geschäftsanwendungen eingesetzt werden.
- SQLite
SQLite kommt ohne Datenbankserver aus. Es ist nur für kleine Applikationen oder Applikationen mit mehr Lesezugriffen als Schreibzugriffen geeignet. Beim Generieren des Rails-Projekts wird automatisch die Datenbankdatei development.sqlite3 im Verzeichnis db erstellt.
- Sybase
Sybase ist ein kommerzielles Datenbanksystem von einem der größten unabhängigen Softwarehäusern der Welt.
Ein erstes Beispiel
Wir werden im Folgenden ein kleines Beispielprojekt erstellen, um die grundlegende Funktionsweise von ActiveRecord zu erklären. Dabei werden wir zunächst keine Generatoren verwenden, sondern alles manuell erstellen, damit Sie die Funktionsweise besser nachvollziehen können. In der Praxis sollten Sie jedoch Generatoren einsetzen.
MySQL oder SQLite
Als Datenbanksystem setzen wir MySQL ein. Sollte auf Ihrem System nicht MySQL installiert sein, so lassen Sie einfach die Option --database beim Generieren des Rails-Projekts weg. Standardmäßig wird dann SQLite verwendet.
Projekt generieren
Zunächst erstellen wir ein neues Beispielprojekt activerecord_bsp1 und wechseln in das Projektverzeichnis.
rails activerecord_bsp1 --database=mysql cd activerecord_beispiel
database.yml
Beim Generieren des Projekts wurde u. a. die Datenbank-Konfigurationsdatei config/database.yml automatisch erzeugt. Dieser Datei können Sie die Namen der erwarteten Datenbanken entnehmen.
... development: adapter: mysql encoding: utf8 database: activrecord_bsp1_development username: root password: socket: /tmp/mysql.sock test: adapter: mysql encoding: utf8 database: activrecord_bsp1_test username: root password: socket: /tmp/mysql.sock production: adapter: mysql encoding: utf8 database: activrecord_bsp1_production username: root password: socket: /tmp/mysql.sock
development
Für uns ist zunächst nur die Umgebung development relevant. Legen Sie gegebenenfalls einen Usernamen und ein Passwort an, falls Ihre Datenbank passwortgeschützt sein soll.
Datenbank erstellen
rake db:create
Anschließend erstellen wir eine Datenbank mit dem Namen, der in der Datei config/database.yml für die Umgebung development definiert ist. In Rails 2 muss die Datenbank nicht mehr manuell angelegt werden, sondern es steht ein Rake-Task zur Verfügung:
Listing Erstellen einer Datenbank
rake db:create
Die Datenbank muss für alle Datenbanksysteme außer SQLite erstellt werden. Für SQLite wird beim Erstellen des Projekts die SQLite-Datenbank-Datei, in der die gesamte Datenbank gespeichert wird, automatisch angelegt.
Model erstellen
Wir legen nun die ActiveRecord-Klasse Product an. Dazu erstellen wir die Datei product.rb im Verzeichnis app/models mit folgendem Inhalt:
class Product < ActiveRecord::Base end
Die Klasse Product scheint über keine Methoden zu verfügen, aber da sie von der Klasse Base innerhalb des Moduls ActiveRecord erbt, stehen ihr alle Methoden der Klasse Base zur Verfügung. ActiveRecord analysiert beim Laden der Klasse, welche Felder die Tabelle products enthält, und generiert dynamisch die Methoden, um auf die Felder zugreifen zu können. Zum Beispiel die Methode price, mit der der Preis eines Product-Objekts abgefragt werden kann.
Tabelle erstellen
Nachdem wir die Datenbank und die Model-Klasse erstellt haben, müssen wir nun die zugehörige Datenbanktabelle products anlegen.
Konvention
Wir haben nirgendwo definiert, dass sich die Klasse Product auf die Tabelle products bezieht. Müssen wir auch nicht. Rails folgt der Konvention, dass Datenbanktabellen nach den zugehörigen Klassen im Plural benannt werden.
Die Tabelle products soll folgende Felder besitzen:
- Id
Primärschlüssel der Tabelle vom Typ Integer - Name
Name des Produkts vom Typ Varchar(255) - Price
Preis des Produkts vom Typ Decimal mit 6 Stellen vor dem Komma und 2 Stellen nach dem Komma - Enabled
Dieses Feld gibt an, ob das Product freigegeben ist oder nicht, und ist deshalb vom Typ Boolean. Der Standardwert soll true sein. - Created_at
Erstellungsdatum des Datensatzes vom Typ Datetime - Updated_at
Änderungsdatum des Datensatzes vom Typ Datetime
Migration
Zum Anlegen der Tabelle werden wir eine sogenannte Migration-Datei einsetzen. Migration-Dateien enthalten Ruby-Code, in denen die Tabellenstruktur definiert wird. Für die Erstellung einer Migration stellt Rails den migration -Generator zur Verfügung. Wir werden die Migration-Datei jedoch von Hand erstellen, indem wir im Verzeichnis db das Unterverzeichnis migrate erzeugen und die Datei 001_create_products.rb darin erstellen.
Zusätzlich zur Tabellenstruktur werden wir in der Migration-Datei einige Beispieldatensätze definieren:
Listing 001_create_products.rb
class CreateProducts < ActiveRecord::Migration def self.up create_table :products do |t| t.string :name t.decimal :price, :precision => 8, :scale => 2 t.boolean :enabled, :default => true t.timestamps end Product.create(:name => "iPod nano 3G", :price => 149.00) Product.create(:name => "Mac Book", :price => 1100.00) end def self.down drop_table :products end end
Primärschlüssel
Das ID-Feld wird von ActiveRecord immer automatisch als Primärschlüssel vom Typ Integer angelegt, weshalb es nicht in der Migration definiert werden muss.
timestamps
Der Befehl timestamps erzeugt die Felder created_at und updated_at.
self.down
Wird eine Migration rückgängig gemacht, wird die Methode self.down ausgeführt. In unserem Beispiel wird dann die Tabelle products gelöscht.
Beispieldaten
Werden so wie in unserem Beispiel in der Migration-Datei auch Beispieldaten angelegt, wird bei Ausführung der Migration-Datei erst die Tabelle erstellt, und anschließend werden die Beispieldaten angelegt.
Die Migration wird mit folgendem Befehl aus dem Projektverzeichnis heraus ausgeführt:
Listing Migration ausführen
rake db:migrate
== 1 CreateProducts: migrating ===========================
-- create_table(:products)
-> 0.0046s
== 1 CreateProducts: migrated (0.0189s) ==================
Testen in der Konsole
Öffnen Sie zwei Terminalfenster, und wechseln Sie in das Projektverzeichnis. In einem Fenster starten Sie mit ruby script/server den lokalen Rails-Server. Hier können Sie später sehen, wie Rails arbeitet. In dem anderen Fenster starten Sie mit ruby script/console die Rails-Konsole.
count
In der Rails-Konsole können wir die neue ActiveRecord-Klasse nutzen, um z. B. die Anzahl der Produkte abzufragen:
>> Product.count => 2
In dem Fenster, in dem Sie den lokalen Rails-Server gestartet haben, können Sie u. a. sehen, welche SQL-Befehle Rails an die Datenbank sendet. Es wird u. a. Folgendes ausgegeben:
Product Columns (0.051914) SHOW FIELDS FROM products SQL (0.050556) SELECT count(*) AS count_all FROM products
Mit dem SQL-Befehl SHOW FIELDS FROM products erfragt Rails, aus welchen Feldern die Tabelle products besteht. Dies wird immer beim ersten Zugriff auf eine Tabelle durchgeführt.
Mit dem zweiten SQL-Befehl wird die Anzahl der Zeilen in der Tabelle bestimmt.
Gibt man in der Konsole nur den Klassennamen ein, so werden die Felder und deren Datentypen angezeigt.
>> Product
=> Product(id: integer, name: string, price: decimal,
created_at: datetime, updated_at: datetime)
Lesen und Ändern eines Datensatzes
find
Um ein Produkt aus der Datenbank auszulesen und zu verändern, können Sie folgende Befehle ausführen:
>> ipod = Product.find(1) => ... >> puts ipod.name => iPod nano 3G >> puts ipod.price => 149.0 >> ipod.price=139.0 =>... >> ipod.save => true
Ausgabe in dem Fenster, in dem der lokale Rails-Server läuft:
SELECT * FROM products WHERE (products.`id` = 1) BEGIN UPDATE products SET "created_at" = '2008-01-24 22:16:33', "name" = 'iPod nano 3G', "enabled" = 't', "price" = 139.0, "updated_at" = '2008-01-24 22:30:45' WHERE "id" = 1 COMMIT
BEGIN und COMMIT sind Transaktionsbefehle, die dafür sorgen, dass entweder alle Befehle ausgeführt werden oder kein Befehl (falls es zu einem Problem kommt).
Erstellen eines neuen Datensatzes
new
Ein neues Produkt legen Sie wie folgt an:
>> imac = Product.new => ... >> imac.name = 'iMac 24' => 'iMac 24' >> imac.price = 1998.0 => 1998.0 >> imac.save => true
create
Alternativ hätten wir auch die Kurzschreibweise zur Erzeugung eines neuen Produkts nutzen können:
Product.create(:name=>'iMac 24', :price=1998.0)
Die Befehle werden in folgenden SQL-Befehl umgewandelt:
BEGIN INSERT INTO products ("name", "updated_at", "price", "enabled", "created_at") VALUES('iMac 24', '2008-01-24 22:36:34', 1998.0, 't', '2008-01-24 22:36:34') COMMIT
Die Methode create erlaubt es sogar, mehrere Datensätze auf einmal zu erzeugen, indem ein Array von Hashes als Parameter übergeben wird:
Product.create( [:name => 'iMac 24', :price => 1998.0, :name => 'iPod nano 2G', :price => 139.0} ])
Suchen mit einem Kriterium
:conditions
Anschließend werden wir die billigsten Produkte suchen und ausgeben:
>> cheapest = Product.find(:all,:conditions=>"price < 1500") => ... >> cheapest.each {|prod| puts "Produkt #{prod.name} kostet #{prod.price} EUR" Produkt iPod nano 3G kostet 139.0 EUR Produkt Mac Book kostet 1100.0 EUR =>...
Folgender SQL-Befehl wurde generiert:
SELECT * FROM products WHERE (price < 1500)
Löschen eines Datensatzes
destroy
Abschließend wollen wir noch den letzten Datensatz löschen:
>> Product.count => 3 >> macbook = Product.find(2) => ... >> macbook.destroy => ... >> Product.count => 2
Ausgabe SQL:
DELETE FROM products WHERE `id` = 2
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.