Schlagwort-Archive: ruby

Rails: Where mit Hash-Bedingung bei Joins

Viele Wege führen nach Rom. So auch bei Abfragen in Rails. Es gibt 3 verschiedene Wege, um Konditionen zu formulieren:

  1. String
  2. Array
  3. Hash

[code lang=“ruby“]class User < ActiveRecord::Base
def self.authenticate_unsafely(user_name, password)
where("user_name = ‚#{user_name}‘ AND password = ‚#{password}’").first
end

def self.authenticate_safely(user_name, password)
where("user_name = ? AND password = ?", user_name, password).first
end

def self.authenticate_safely_simply(user_name, password)
where(:user_name => user_name, :password => password).first
end
end[/code]

Die String-Methode ist unsicher, da die Daten nicht bereinigt werden und so SQL-Injections möglich sind.

Die Array-Methode ist sicher und erinnert syntaktisch stark an Prepared Statements.

Die Hash-Methode ist ebenfalls sicher, aber typisch Rails, weil einfach: lediglich ein Hash wird übergeben. Der Schlüssel (key) als Symbol entspricht der Spaltenbezeichnungen der Tabelle und der Wert (value) definiert die Bedingung.
Das Schöne an der Hash-Methode ist neben der Sicherheit, dass das Hash wesentlich einfacher dynamisch berechnet werden kann (als die Array-Methode):
[code lang=“ruby“]def all_by_employee_or_manager(employee_id=nil, manager_id=nil)
condition = Hash.new
condition[:employee_id] = employee_id unless employee_id.blank?
condition[:manager_id] = manager_id unless manager_id.blank?
where(condition)
end[/code]

Das Problem: NoMethodError

Sobald jedoch in der Abfrage gejoint wird, bekommt man bei einer Hash-Bedingung wie
[code lang=“ruby“]joins(:employees).where(:employees.name => name)[/code]
folgenden Fehler:
[code]NoMethodError: undefined method `name‘ for :employees:Symbol[/code]
Das Problem ist, dass das Symbol :employees.name nicht komplett als Symbol interpretiert wird, sondern nur bis zum Punkt.

Bei der Internetrecherche habe ich für gejointe Abfragen nur Konditionen im String- oder Array-Format finden können, was in meinem Anwendungsfall aber nicht praktikabel war, denn die Konditionen mussten dynamisch berechnet werden.

Die Lösung

Um Konditionen in gejointen Abfragen mit der Hash-Methode anzugeben, muss Ruby klar gemacht werden, dass employees.name komplett als Symbol verstanden werden soll. Das geht so:
[code lang=“ruby“]joins(:employees).where(:"employees.name" => name)[/code]
Einfach, wenn gewusst wie!

Rails: Dynamische Assoziation zwischen verschiedenen Datenbanken

Über Assoziationen lassen sich in Ruby on Rails verschiedene Modelle miteinander verbinden und so einfacher Daten abfragen, aktualisieren und einfügen. In diesem Artikel geht es um zwei Aspekte, die ich im Internet nur schwer recherchieren konnte:

  1. Assoziationen zwischen verschiedenen Datenbanken
  2. Dynamische Assoziationen (der Tabellenname des assoziierten Modells ist dynamisch und hängt von einem Attribut ab)

Anwendungsbeispiel

In meinem Fall geht es um das Modell Contact, welches in der Rails-Anwendung die Kontakte/Aufgaben eines Testmieters verwaltet. Pro Kontakt wird in LimeSurvey ein Teilnehmer eines Fragebogens hinterlegt, der den Fragebogen ausfüllen kann. Die eingegebenen Daten des Testmieters sollen in der Rails-Anwendung mit dem Kontakt (Contact) verbunden werden. Dabei gibt es diese Herausforderungen:

  • Die Datenbank, in dem die Tabelle contacts liegt, ist ralv während die Fragebogen-Antworten in der Datenbank limesurvey liegen.
  • Die Fragebogen-Eingaben liegen je nach Kontakt in verschiedenen Tabellen. Der Tabellenname ist z.B. lime_survey_661185, wobei 661185 die ID des Fragebogens in LimeSurvey ist. Die ID des Fragebogens wird im Kontakt gespeichert.

Die Struktur der Tabellen ist also:

Tabellenstruktur

wobei der Name der Tabelle lime_survey_* von contacts.fragebogen_id abhängt. Deutlicher wird dieser Aspekt in der Datenansicht:

Daten

Assoziationen zwischen verschiedenen Datenbanken

Kümmern wir uns zunächst um diesen einfacheren Part: Modelle aus unterschiedlichen Datenbanken. Als Erstes muss eine zusätzliche Datenbank-Verbindung in /config/database.yml eingerichtet werden:

[code]limesurvey:
adapter: mysql2
encoding: utf8
username: [DBUSER]
password: [DBPASSWORD]
database: limesurvey[/code]
Für die Tabellen lime_survey_* wird zudem ein Modell namens LimesurveyData erstellt:
[code lang=“ruby“]class LimesurveyData < ActiveRecord::Base
establish_connection :limesurvey
self.table_name_prefix = ‚limesurvey.‘
self.table_name = ‚lime_survey_661185‘
has_one :contact, :primary_key => "token", :foreign_key => "token"
end[/code]
Betrachten wir den Quellcode zeilenweise:
1. Zeile: Die Klasse erbt von ActiveRecord, womit sie die Funktionen eines Rails-Modells erhält.
2. Zeile: Für die Klasse soll die vorher in /config/database.yml eingerichtete Datenbank-Verbindung verwendet werden.
3. Zeile: Um generell eine andere Datenbank in einer SQL-Abfrage zu spezifizieren, wird folgende Syntax verwendet Datenbank.Tabelle.Spalte. Rails 3.1 spezifiziert standardmäßig nicht die Datenbank in Abfragen, daher wird die Datenbank über den table_name_prefix in allen Abfragen des Modells eingefügt, sodass klar ist, welche Datenbank bei diesem Modell zu verwenden ist.
4. Zeile: Der Tabellenname wird normalerweise aus dem Klassennamen errechnet. In diesem Fall soll zunächst statisch die Tabelle lime_survey_661185 verwendet werden.
5. Zeile: Die Assoziation zu dem Modell Contact wird als 1:1-Beziehung eingerichtet. Dabei wird festgelegt, dass über das Attribut token in beiden Tabellen die Verbindung hergestellt wird.

In der Contact-Klasse wird zusätzlich ebenfalls die Assoziation zur LimesurveyData-Klasse eingerichtet:
[code lang=“ruby“]class Contact < ActiveRecord::Base

has_one :limesurvey_data, :class_name => "LimesurveyData", :primary_key => "token", :foreign_key => "token"

end[/code]
Der table_name_prefix muss in der Contact-Klasse nicht gesetzt werden, da standardmäßig die richtige Datenbank verwendet wird.

Dynamische Assoziationen

Um die dynamische Assoziation, also den richtigen LimesurveyData-Tabellennamen in Abhängigkeit zur Fragebogen-Id zu setzen, haben wir den wichtigsten Befehl bereits in der LimesurveyData-Klasse kennengelernt: self.table_name

Die Frage ist, an welcher Stelle der Tabellenname gesetzt werden muss. Der Tabellenname kann nicht in der LimesurveyData-Klasse definiert werden, weil dort unklar ist, welche Fragebogen-Id für die Assoziation relevant ist. Die Contact-Klasse ist der bessere Kandidat, denn schließlich wird dort die Fragebogen-Id gespeichert. Aber wie kann der Tabellenname eines anderen Modells gesetzt werden? So:
[code lang=“ruby“]class Contact < ActiveRecord::Base

after_initialize :setTablename

has_one :limesurvey_data, :class_name => "LimesurveyData", :primary_key => "token", :foreign_key => "token"

def setTablename
LimesurveyData.table_name = "lime_survey_#{self.fragebogen_id}"
end

end[/code]
Starten wir diesmal in mit der setTablename-Methode in den Zeilen 7-9: Sie setzt den Tabellenname der Ḱlasse LimesurveyData auf den dynamischen Namen.
In Zeile 3 wird diese Funktion als Callback-Funktion definiert, die immer dann ausgeführt wird, wenn die Contact-Klasse initialisiert wurde.

Und das ist alles: Wir können nun ganz einfach von einem Kontakt auf die korrekten Fragebogen-Antworten zugreifen, obwohl die Daten in einer anderen Datenbank liegen und über verschiedene Tabellen verteilt sind.

Einstieg in Ruby on Rails

Aufgrund meines Jobs habe ich mich den letzten Monat mit Ruby on Rails beschäftigt und die ersten Funktionen in einer RoR-Anwendung ergänzt. Auch wenn ich seit Jahren PHP-Anwendungen schreibe und bereits Erfahrungen mit PHP-Frameworks gesammelt habe, ist dieses Wissen nur begrenzt für die Ruby on Rails-Entwickung von Nutzen. Warum?

Ruby

Ruby hat eine andere Syntax… Zugegeben: eine schönere und besser lesbare Syntax als PHP. Was ich z.B. sehr schön finde, ist folgender Code:
[code lang=’Ruby‘]puts i if i > 0[/code]
Wenn i größer als 0 ist, wird i ausgegeben. Ganz einfach, in einer Zeile, ohne geschweifte Klammern, auf den Punkt.

Natürlich findet man viele Gemeinsamkeiten in Ruby und PHP. Wäre ja komisch, wenn Ruby ohne Arrays und Objekte auskäme.
Der Ruby-Code ist deutlich besser lesbar, da z.B. bei Code-Blöcken mit Texteinrückungen gearbeitet wird, anstatt geschweifte Klammern zu verwenden, die dadurch obsolet werden.

Natürlich sollte als erstes Ruby gelernt werden und erst dann Rails. Für den Ruby-Einstieg eignet sich dieses interaktive TryRuby-Tutorial.

Rails

Rails-Anwendungen werden auf einer ganz anderen Abstraktionsebene programmiert als PHP-Anwendungen. Die zwei Grundsätze von RoR sind

  • Don’t Repeat Yourself
  • Convention over Configuration

Die Vorteile dieser Grundsätze sind

  1. Weniger Code
  2. Schnellere Entwicklung
  3. Konzentration auf die Webanwendung (anstatt sich mit Konfigurationen oder DB-Design zu beschäftigen)

Es muss also attestiert werden, dass Rails einem sehr viel Arbeit abnimmt. Man kann sich über die Rails-Konsole sogar einfache, funktionierende Grundgerüste (Scaffolds) der Webanwendung erstellen lassen, ohne auch nur eine Zeile Code schreiben zu müssen.

Dank den RubyGems können zusätzliche Erweiterungspakete in die Anwendung integriert werden, die die Funktionalität von Ruby on Rails ergänzen. Nested_form zur einfachen Handhabung mehrerer Modelle in einem Formular.
Oder einfache Suchfunktionalitäten mit meta_search gefällig? Welche Schritte sind in PHP notwendig, um eine Suche zu implementieren: Formular bauen, eingegebene Daten prüfen, säubern, übernehmen und eine SQL-Abfrage daraus bauen. Richtig? Mit meta_search benötigst du in RoR gerade mal neun popelige Zeilen Code, um in Artikeln nach Titeln zu suchen:
Im Controller der Artikel müssen die Suchparameter entgegen genommen werden (4 Zeilen):
[code lang=’ruby‘]def index
@search = Article.search(params[:search])
@articles = @search.all # lade alle Einträge
end[/code]

Und im Formular wird die Suche definiert (5 Zeilen):
[code lang=’ruby‘]<%= form_for @search do |f| %>
<%= f.label :title_contains %>
<%= f.text_field :title_contains %>
<%= f.submit %>
<% end %>[/code]
wobei title das zu durchsuchende Feld in der Datenbank ist und _contains gibt an, dass der Suchparameter als Teilstring verwendet wird. Jeder Programmierer wird angesichts dieser wenigen Code-Zeilen mit der Zunge schnalzen, denn viel produktiver kann wohl nicht programmiert werden.

Aber dieser hohe Abstraktionsgrad hat auch seine Nachteile: Was tun, wenn die Suche nicht das tut, was man erwartet? Welche Optionen stehen einem zur Verfügung? Wie richtet man eigene Suchmethoden ein? Wie sieht’s mit Sicherheitslücken aus? Welche Variablen und Funktionen stehen zur Verfügung? Welche Abhängigkeiten bestehen?
Wie bei allen Frameworks begibt man sich in eine neue Umgebung, die man erst erkunden muss. Man muss sich auf Rails voll einlassen und sollte die Art und Weise von Rails akzeptieren. Die Herangehensweise „in PHP habe ich das so implementiert, wie setze ich das jetzt 1:1 in RoR um„, ist zum Scheitern verurteilt. Dazu hat Rails zu viele Konventionen, die man kennen und benutzen sollte, weil sie einem das Leben leichter machen.

Ich habe für mich festgestellt, dass das pure Einlesen in die Rails-Materie relativ wenig bringt. Besser sind Tutorials, in denen man aktiv werden muss. Ein gutes, witzig gemachtes und kostenloses Tutorial ist Rails for Zombies. Den kostenpflichtigen Fortsetzungsteil namens Rails for Zombies 2 von der Code School kann ich ebenfalls nur empfehlen.

Fazit

Auf der Ruby on Rails-Seite prangt folgendes Zitat von Tim O’Reilly:

Ruby on Rails is a breakthrough in lowering the barriers of entry to programming. Powerful web applications that formerly might have taken weeks or months to develop can be produced in a matter of days.

Ein schönes Schlussfazit. Ich würde allerdings nicht sagen, dass es die Einstiegsbarrieren für die Programmierung senkt. RoR ist in meinen Augen kein Anfänger-Framework. Natürlich kümmert Rails sich um viele Aspekte, aber ich halte es gerade bei Anfängern für wichtig, dass sie die zugrunde liegenden Mechanismen verstehen. Und das ist bei all der „Rails-Magie“ nicht möglich.