Ruby meets Enterprise - Ruby on Rails-Entwicklung mit Oracle-Datenbanken
Datenbanken im Enterprise-Umfeld
Bei Zweitag wollen wir das Beste aus den Welten Startup und Enterprise verbinden. Gerade Startups profitieren von den schnellen Ergebnissen, die mit Frameworks wie Ruby on Rails erzielt werden können. Der Weg zu einem minimum viable product (MVP) oder einem ersten Prototypen ist in der Regel nicht weit und der Infrastruktur-Stack des Startups kann nach eigenen Wünschen gestaltet werden.
Im Enterprise-Umfeld dagegen ist die Zusammenstellung des Technologie-Stacks restriktiver. Bei der Wahl der Persistenzschicht einer Anwendung sind Open Source-Lösungen wie PostgreSQL oder MySQL eher Ausnahme als die Regel. Die Entscheider bauen im Enterprise-Umfeld meist auf große, etablierte Namen wie Oracle Database, Microsoft SQL Server oder DB2. Auch die Einführung oder gar der komplette Wechsel von Datenbanken ist aufgrund bestehender Backup-Strategien, der Verflechtung in bestehende Systeme und Oracle-Erfahrung von Mitarbeitern langwierig und teuer.
Mit Ruby on Rails können in Unternehmen flexible Systeme umgesetzt werden, die auf immer schneller wechselnde Rahmenbedingungen reagieren können. Die Datenbank ist dabei nur ein Faktor, der in diesem Kontext eine Rolle spielt. Dieser Blogeintrag soll durch eine kurze technische Beschreibung zeigen, dass das effektive Zusammenspiel von proprietärer Software mit Ruby on Rails erfolgreich funktionieren kann. Ergebnis soll ein Leitfaden zur Nutzung von Ruby oder Ruby on Rails mit Oracle Database sein.
Ruby und Oracle Database
Oracle Database ist gewöhnlich nicht die erste Wahl eines Ruby-Entwicklers. Wenn die Nutzung von Oracle Database durch externe Rahmenbedingungen vorgegeben ist, muss jedoch nicht auf Ruby verzichtet werden. Der erste Schritt ist eine lokale Installation der Oracle Database - hier empfiehlt sich die kostenlose Variante Oracle Database 11g Express Edition. Die Installation unter Mac OS ist nicht gerade straight-forward, daher empfiehlt sich die Installation in einer virtuellen Maschine. Eine Beispiel-Anleitung ist hier zu finden.
Für den Oracle-Zugriff aus Ruby bietet sich das Gem ruby-oci8 an. Dieses Interface kommuniziert über die von Oracle definierte C-Schnittstelle OCI (Oracle Call Interface). Das Gem ist über Rubygems verfügbar und durch Einbindung ins Gemfile im Ruby-Projekt verwendbar. Damit der Zugriff funktioniert, müssen die „Oracle Instant Client Packages“ installiert sein. Ein Anleitung hierfür ist in der Dokumentation des Gems zu finden.
Das folgende Code-Beispiel prüft die Funktionalität des Datenbank Interfaces:
Waren die Konfiguration der Datenbank und der Instant-Client-Pakete erfolgreich, wird mit dem kurzen Quellcode-Beispiel eine Datenbankverbindung aufgebaut und der Inhalt der Tabelle „dual” ausgelesen.
##Rails und Oracle Database - Zusammenspiel ActiveRecord & Oracle Database
In Ruby on Rails-Projekten wird durch den Objekt-relationalen Mapper ActiveRecord von SQL-Queries abstrahiert. Zusätzlich zum ruby-oci8 gem benötigt man in Rails-Projekten den Datenbank-Adapter activerecord-oracle_enhanced-adapter. Dieser kann nach Einbindung im Gemfile in der database.yml genutzt werden.
Das Zusammenspiel mit ActiveRecord funktioniert nach erfolgreicher Konfiguration in der Regel reibungslos. Im Vergleich mit anderen Datenbanken sind hier allerdings ein paar Feinheiten zu beachten.
DateTime und Date
Im Gegensatz zu Postgres und MySQL gibt es in Oracle-Datenbanken keinen Spaltentyp, der ein reines Datum ohne Uhrzeit persistiert. Dies kann zu Verwirrung führen, wenn in der Rails-Anwendung UTC nicht als Zeitzone konfiguriert wurde.
Im Code-Beispiel wird der Geburtstag eines Users persistiert. Ruby-seitig wird der Geburtstag korrekt persistiert und auch korrekt ausgegeben - allerdings als DateTime Objekt. Zudem ist der Datenbankwert - wie bei allen DateTimes - in UTC gespeichert, was in der Datenbank eine Abweichung des Tages zur Folge hat. Der Geburtstag in unserem Beispiel ist aber in allen Zeitzonen am 17. Juni. Der Oracle-Adapter bietet im Legacy Schema Support set_date_columns an. Wenn im User-Model „birthday“ als date_column definiert wird, ist der Rückgabe Wert ein Date Object und in der Datenbank wird auch in UTC das korrekte Datum persistiert.
Boolean Werte
Oracle Database bietet keine boolean-Spalten an. ActiveRecord legt boolean-Spalten als NUMBER(1,0) an. In der Datenbank sind demnach 0, 1, und 2 gültige Werte. Wenn NULL vermieden werden soll, bietet sich zusätzlich ein NOT NULL Constraint an, so dass false keine Doppelbelegung in der Datenbank hat.
Umgekehrt können NUMBER(1,0)-Spalten aus Legacy-Datenbanken Probleme verursachen, da diese von ActiveRecord als boolean interpretiert werden. Werte von 2-9 werden als_false_ interpretiert. Um das zu vermeiden kann die entsprechende Spalte im Model mit set_integer_columns als Integer definiert werden.
Größenbeschränkungen in Oracle Database
Bezeichnungen von Tabellen und Spalten sind in Oracle generell auf eine Länge von 30 Zeichen begrenzt. Längere Bezeichnungen lehnt Oracle mit dem Fehler “OCIError: ORA-00972: identifier is too long” ab. Verwendet man beispielsweise ein Model mit der Klassenbezeichnung „DoesYourRubyLookLikeJavaExample“, akzeptiert die Datenbank nicht den vom Adapter generierten Namen „does_your_ruby_look_like_java_examples“. Dieser überschreitet die 30-Zeichen-Obergrenze für Bezeichnungen. An dieser Stelle muss der Tabellenname mit “self.table_name” im Model manuell definiert und gekürzt werden. Ebenfalls nicht erlaubt sind Spaltennamen über 30 Zeichen
Datenbank-Abfragen die in eine “IN”-Bedingung enthalten, sind auf 1000 Objekte begrenzt. Enthält die „WHERE“-Abfrage mehr Einträge bricht Oracle mit der Fehlermeldung "ORA-01795: maximum number of expressions in a list" ab. Folgende ActiveRecordRelation
all_cities = City.where(zip:(1..1001).to_a)
kann nicht aufgelöst werden - dies muss Ruby-seitig abgefangen werden, damit diese Obergrenze eingehalten wird. Wenn man die Queries mit mehr als 1000 Einträgen in einer „IN“ Abfrage nicht vermeiden kann oder will, ist ein ActiveRecord Monkey Patch ein möglicher Ausweg.
Dieser Monkey Patch teilt Abfragen mit mehr als 1000 Einträgen auf und konkateniert diese mit einem logischen „Oder“. Dadurch wird allerdings der Rückgabewert von Arel::Nodes::In zu Arel::Nodes::Grouping verändert. Das führt im Einsatz führt dieses MonkeyPatches in der Regel nicht zu Problemen. Die ActiveRecord::Relation “all_cities = City.where(zip:(1..1001).to_a)” funktioniert mit diesem Patch.
dbconsole und Oracle Database
Rails bietet den Befehl, dbconsole um das jeweilige Command Line Interface (CLI) der genutzten Datenbank zu öffnen. Oracles CLI ist sqlplus, welches im ORACLE_HOME Verzeichnis liegt. Daher muss die Umgebungsvariable ORACLE_HOME gesetzt sein und im PATH liegen. Der Befehl “sqlplus system/abcd123@192.168.1.142:1521/xe” verbindet dann dementsprechend zur Datenbank. Verbindungen können in einer tnsnames.ora gespeichert werden.
Wenn diese tnsnames.ora vorhanden ist, reicht "sqlplus system@myDb" + Kennworteingabe um zur Datenbank zu verbinden.
Ist die database.yml mit host, username und password konfiguriert (erster Teil der Grafik), erscheint beim Ausführen der Fehler “ORA-12154: TNS: Angegebener Connect Identifier konnte nicht aufgelöst werden”. Um diesen Fehler zu vermeiden,muss die database.yml mit einem Connection String - wie in der zweiten Grafik dargestellt - konfiguriert sein.
Anschließend muss nur noch rails dbconsole -p ausgeführt werden und dann können Datenbankabfragen in der Entwicklungsdatenbank ausgeführt werden.
Integration von Rails-Anwendungen in Oracle Database-Systemlandschaften
Wenn bestimmte Unternehmensdaten in zentralen Datenbanken hinterlegt sind und man sich in der Situation befindet, diese Daten aus einer Rails-Anwendung nutzen zu müssen, bieten sich mehrere Möglichkeiten an. Bei allen Alternativen empfiehlt sich ein rein lesender Zugriff - solange eine andere Anwendung für die Pflege der Daten zuständig ist.
Eine Rails-Anwendung sollte in jedem Fall über ein eigenes Oracle Database Datenbank-Schema verfügen, in dem nur diese Anwendung Daten schreiben darf.
Abstraktion in einer eigenen Anwendung
Sollten die Daten in einer verteilten Umgebung in mehreren Anwendungen benötigt werden, bietet es sich an, die Daten über einen zentralen Service zu verteilen. Dazu wird bspw. eine zusätzliche Anwendung geschrieben, die exklusiven Zugriff auf die Daten hat und eine API anbietet, die von den anderen Anwendungen genutzt wird. Im Optimalfall bietet die Anwendung, die über die Datenhoheit verfügt, eine entsprechende API an.
Vorteile: Der Zugriff auf Legacy-Daten ist gekapselt und die Konsumenten benötigen keine aufwändige Konfiguration. Die Service App kann in einer beliebigen Programmiersprache umgesetzt werden.
Einbindung über Datenbank Links
Sollte es tatsächlich unumgänglich sein, dass auf die Legacy-Daten direkt in der Rails-Anwendung zugegriffen werden muss, können diese über Datenbank-Links von anderen Anwendungen bereitgestellt werden. Dabei empfiehlt es sich, die Berechtigungen so einzurichten, dass nur ein lesender Zugriff möglich ist.
Um die Tabelle nahtlos im eigenen Schema zugänglich zu machen, kann eine View erstellt werden.
Nun kann über ein Rails Model auf diese Tabelle zugegriffen werden. Da Write-Methoden von ActiveRecord auf diesem "Model" auf einen Fehler laufen, sollte das Model als schreibgeschütztes Model definiert werden.
Vorteil: Es ist nur eine Datenbank-Verbindung notwendig.
Nachteil: Ein Datenbank-Link ist in der Regel nur im produktiven System verfügbar. In einem lokalen Testsystem steht der Datenbank Link nicht zur Verfügung. Daher sind Rails-environment spezifische Migrationen notwendig um die Tabelle in der schema.rb und in Entwicklungs-Umgebungen verfügbar zu machen.
Einbindung mehrerer Datenbanken in der database.yml
Legacy-Datenbanken können als weitere Quellen in der Datenbank-Konfiguration angegeben werden. Die zweite Datenbank sollte ebenfalls read only eingebunden werden. Das lässt sich am einfachsten über die Berechtigungen des Datenbank-Users realisieren.
Um die Konfigurationen für alle ActiveRecord Objekte der Legacy-Datenbank zu vereinheitlichen, bietet sich eine Oberklasse an, in der die notwendigen Einstellungen vorgenommen werden. (ExternalDatabase::Base siehe Gist)
In dieser wird über establish_connection die zweite Datenbank genutzt. Wenn die Tabellen in der Legacy-Datenbank nicht den Rails-Konventionen entsprechen, können in dieser Klasse entsprechende Vorkehrungen getroffen werden. (Beispiel: self.primary_key = "Legacy_id")
Testen
Wenn Legacy-Daten in eine Rails-Anwendung integriert werden, ist eine Einbindung in die Tests der Anwendung von zentraler Bedeutung. Die Tabellen sind nicht zwingend in der schema.rb enthalten, da sie nicht direkt zur Anwendung gehören. Beispieldaten können somit nicht direkt in der Test-Umgebung bereitgestellt werden. Um die Anwendung zu testen, werden aber die externen Tabellen benötigt. Um den Zugriff auf diese Tabellen zu simulieren, können beim Vorbereiten der Test-Datenbank die Tabellen-Strukturen über separate Schema-Files geladen werden. Ferner soll beschrieben werden, wie diese Schema-Files erstellt werden können.
Im Gist wird eine Verbindung zur entfernten Datenbank aufgebaut und die Strukturen der Tabellen werden in Schema-Dateien geladen, die beim Testen in die Datenbank integriert werden.
Dabei kümmert sich der SchemaDumper im Task external_database:schema:dump von ActiveRecord um die Data Definition der entsprechenden Tabellen. Der rake-task zum Vorbereiten der Test-Datenbank kann um den Task external_database:schema:load erweitert werden.
Wenn der task db:test:prepare wie im Gist erweitert wird, werden die Tabellen-Strukturen aus den Schema-Dateien in die Test-Datenbank für die externen Daten geladen. Diese kann mit der normalen test-Datenbank identisch sein, das muss entsprechend in der database.yml konfiguriert werden.
Vorteil dieser Lösung ist eine realistische Abbildung der verwendeten Tabellen. Jede Änderung, die in den Legacy-Tabellen vorgenommen wird, wird durch Aufruf von rake external_database:schema:dump db:test:prepare in der Testumgebung berücksichtigt.
Fazit
Ruby on Rails und Oracle Database können unter Beachtung der beschriebenen Besonderheiten reibungslos zusammenarbeiten. Größenbeschränkungen und fehlende Datentypen sind zunächst ungewohnt, fallen aber nach längerer Verwendung kaum noch ins Gewicht. Die Integrationszenarien von Legacy-Daten mit Rails Anwendungen können dabei helfen einen vorhandenen Datenpool möglichst wenig invasiv in neuen Anwendungen zu verwenden. Ruby und insbesondere Ruby on Rails eignen sich somit hervorragend als moderne agile Technologien in Unternehmen die auf Oracle-Datenbanken setzen.
Bildrechte Ruby on Rails Logo: Yukihiro Matsumoto, Ruby Visual Identity Team - http://rubyidentity.org/, CC BY-SA 2.5