Archiv für die Kategorie ‘Ruby’

Universeller Feldupdater mit Rails

Dienstag, 24. August 2010

Ich erstelle momentan ein kleines, sehr spezielles CMS mit Ruby on Rails, mein erstes komplettes Rails Projekt. Der Umstieg von Java in die Rails-Welt war zwar gewöhnungsbedürftig und hatte einige Hürden, aber es hat sich gelohnt. Hier ein kleines, schönes Beispiel wie man mit wenigen Zeilen Ruby ziemlich universellen Code erzeugen kann.

Die Anforderung war, alle Eingabefelder im CMS, z.B. die Beschriftung der Bilder oder Überschriften und Artikel, einzeln editierbar zu machen. D.h. man klickt bzw. doppelklickt das Feld an, editiert es und die Änderung wird beim Verlassen des Feldes gespeichert.
Undenkbar hier für jedes Feld eigene Methoden im Controller oder JavaScript anzulegen.

Die Lösung besteht aus drei Teilen wobei ich hier nur den Controller näher erläutern möchte.
Teil 1 ist eine Helpermethode die ein entspr. Eingabefeld erzeugt.
Teil 2 ist eine kleine JavaScript Funktion die den Wert des Feldes per Ajax an den Controller sendet.
Teil 3 ist der Controller der den neuen Wert speichern soll.

Idee war es dem Controller folgende Werte zu liefern:
- Name der Model Klasse
- ID des Datensatzes
- Name des Attributes
- Wert des Attributes
Die ersten drei Werte werden durch einen Punkt getrennt als ID des HTML input gesetzt, z.B.: Image.34.caption

Jetzt aber zum Controller, der nur aus einer public Methode besteht:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FieldsController < ApplicationController
 
  layout nil
 
  def update
    a = params[:key].split('.')
    update_field a[0], a[1], a[2], params[:value]
    head :status => 200
  end
 
  private
  def update_field classname, id, attr, value
    # must be a ActiveRecord::Base
    obj = ActiveRecord::Base.const_get(classname).find(id)
    obj.update_attribute(attr, value)
  end
end

Die als “key” übergebenen Werte werden erstmal zerlegt und der Methode update_field übergeben. Hier wird die Model-Klasse über die Kernel Funktion const_get geladen (Klassennamen sind in Ruby Konstanten). Das machen wir direkt auf ActiveRecord::Base, weil wir dort alle Model Klassen finden werden und somit nicht Kernel.const_get verwenden müssen. Auf der Modelklasse rufen wir direkt ein find mit der ID auf und bekommen die Instanz unseres Datenobjekts. Auf diesem setzen wir den Wert des Attributs mit update_attribute neu. Fertig.
Der Controller liefert dann noch den Status 200 zurück, damit unser AjaxRequest weiß, dass alles ok ist.

Auf ActiveRecord::Base gibt es noch weitere update Methoden, so könnte man z.B. auch mehrere Attribute updaten.


Christof Aenderl


RoR-Migrations und PostgreSQL-Datentypen

Montag, 01. März 2010

Migrationsskripte sind in Ruby on Rails das Konzept, um die Entwicklung von Datenbanken im Verlaufe der Produktentwicklung zu verwalten und zu steuern. Da es sich bei ihnen um Ruby-Skripte handelt, steht einem auch der volle Funktionsumfang von Ruby on Rails zur Verfügung.
Ein Hauptprinzip der Migrationsskripte ist deren Datenbankneutralität. Sie soll es ermöglichen, mit möglichst geringem Aufwand die Datenbank wechseln zu können. Dies geht auf Kosten der Unterstützung spezieller Datenbank-Features.
Arbeitet man nun mit einer PostgreSQL-Datenbank und möchte man seine Spalten auch mit PostgreSQL-spezifischen Datentypen versehen, so kann man das wie folgt bewerkstelligen:

In der Methode “native_database_types” der Klasse “ActiveRecord::ConnectionAdapters::PostgreSQLAdapter” werden die Mappings für die SQL-Datentypen definiert, die von den Migrationsskripten im Zusammenhang mit einer PostgreSQL-Datenbank benutzt werden. Genau diese Methode kann man nun überschreiben, um die Mappings anzupassen.

class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
def native_database_types
{
:primary_key => "bigserial primary key",
:string      => { :name => "character varying", :limit => 255 },
:text        => { :name => "text" },
:integer     => { :name => "integer" },
:float       => { :name => "float" },
:datetime    => { :name => "timestamp" },
:timestamp   => { :name => "timestamp" },
:time        => { :name => "time" },
:date        => { :name => "date" },
:binary      => { :name => "bytea" },
:boolean     => { :name => "boolean" },
:bigint      => { :name => "int8" }
}
end
end

Nun kann man diese Definition beispielsweise in psql_ext.rb im lib-Verzeichnis speichern und sie per require ‘psql_ext’ in die Migrationsskripte einbinden.


Christian Borkowski


Symbole zu Methoden von Objekten

Freitag, 27. November 2009

Bei der Programmierung eines Suchalgorithmus in Rails habe ich bei der Methode Enumerable#find einen einfachen Weg gesucht, die optionale Callback-Methode _ifnone_ zu setzen.

Die Intention des Algorithmus ist, falls es kein passendes Element (item) zur gestellten Suchbedingung gibt (item search condition), ein bestimmtes (hier: das letzte) Element zurückzugeben, statt nil.

Dafür bin ich unter Funktionale Programmierung auf den folgenden für mich sehr hilfreichen Code-Snippet gestoßen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Dieser Ausdruck verhindert eine doppelte Definition der Operatoren [] und []=
unless :test.respond_to?(:[])
  #
  # Operatoren [] und []= zur Klasse Symbol hinzufügen, um Singleton-Methoden
  # von Objekten zu lesen und zu setzen. Lies : als "Methode" und [] als "von".
  # Also bedeutet zum Beispiel :m[o] "Methode m von o".
  #
  class Symbol
    # Die durch dieses Symbol benannte Methode von obj zurückgeben. Dies kann
    # eine Singleton-Mehtode von obj (etwa eine Klassenmethode) oder eine Instanz-
    # methode sein, die in obj.class definiert oder von einer Oberklasse übernommen
    # wurde.
    # Beispiele:
    #   creator = :new[Object]  #Klassenmethode Object.new
    #   doubler = :*[2]           # *-Methode der Fixnum 2
    #
    def [](obj)
      obj.method self
    end
 
    # Eine Singleton-Methode für Objekt o definieren und Proc oder Method f
    # als Rumpf verwenden. Dieses Symbol dient als Name der Methode.
    # Beispiele:
    #   :singleton[o] = lamba { puts "Dies ist eine Singleton-Methode von o" }
    #   :class_method[String] = lamba { puts "Dies ist eine Klassenmethode" }
    #
    # Beachten: Sie können so keine Instanzmethoden erzeugen. Siehe Module.[]=
    #
    def []=(o,f)
      # self einer Variablen zuweisen, da der folgende Block einen anderen Kontext
      # haben wird.
      sym = self
      # Dies ist das Objekt, für das wir Singleton-Methoden definieren.
      eigenclass = (class << o; self end)
      # define_method ist privat, so dass wir instance_eval verwenden müssen.
      eigenclass.instance_eval { define_method(sym, f) }
    end
  end
end

Mein Anwendungsfall sieht dann folgendermaßen aus:

34
35
36
 ...
 valuearray.find(:last[valuearray]) {|item| /* item search condition */ }
 ...

Als Fallback für eine nicht erfolgreiche Suche verwende ich :last[valuearray], die last-Methode von dem Array-Objekt valuearray.

Auch wenn dieses Beispiel für den durchschnittlichen Ruby-Entwickler etwas überzogen daherkommt,
soll es die Aufmerksamkeit für diesen Themenkomplex der Programmiersprache anregen und die
Vielseitigkeit und Flexibilität von Ruby zeigen.


Hendrik Lange


Mein erstes eigenes Rails-PlugIn

Montag, 29. Juni 2009

Letztens hat mich ein Kollege gefragt: “Was ist das Problem, wenn man 2 Rails-Entwickler und 2 Java-Entwickler in ein Projekt steckt? Man hat hinterher 4 Rails-Entwickler…”.  Ok, Rails macht Spaß. Aber es ist nicht immer soooo ganz einfach auch mal hinter die Kulissen zu schauen. So habe ich auch ein wenig gebraucht, bis ich zum ersten mal ein einfaches eigenes Plug-In schreiben konnte, das automatisch die anderen Klassen in meinem Projekt bei Systemstart erweitert.

Mein Privat-Projekt: Ein System zum Verwalten von Segel-Regatten und den zugehörigen Anmeldungen.(www.regattamanager.org)

Das Problem: Zu jedem Schiff, User und Anmeldung gehört ein Segel-”Club”, der referenziert werden soll. Allerings gibt es Clubs wie Sand am Meer und somit gibt es niemals eine Tabelle mit allen möglichen Clubs, sondern die Teilnehmer sollen selber auch eigene Clubs “on-the-fly” anlegen können.

Die Lösung: Meine Entitäten Ship, User und Registration sollen zwar intern eine Referenz auf die club_id besitzen, nach aussen aber diese Referenz wie zwei eigene Felder club_name und club_shortcut “maskieren” (und damit die automatische Erstellung von nicht vorhandenen Clubs übernehmen).

[Anmerkung:  Dass ich überhaupt in der Situation bin, das Problem wie oben zu haben und zu lösen ist wohl "broken-by-design", aber so ist das in Freizeitprojekten... ;-) ]

Die Strategie: In Java hätte ich einfach die Vererbungshierarchie der Entitätsklassen erweitert. Das geht aber in Rails nicht, da jede Kindklasse von ActiveRecord auch persistiert werden muss (Konvention). Somit bleibt mir nur eins: ein Plugin, in dem ich meine Funktionen in alle betreffenden Klassen “einmixe”. Ich nenne das Plugin “ClubMasquerade”.

Und so funktioniert das mit den PlugIns:

1. Ich schreibe mein eigenes “module” in vendor/plugins/club_masquerade/lib/club_masquerade.rb

require 'activerecord'
 
module ClubMasquerade #:nodoc:
 
 def self.included(base)
   base.extend ClassMethods
 end
 
  module ClassMethods
   def acts_as_club_masquerade
      before_save :set_club_from_fields  
      validates_length_of :club_name, :within => 3..50, :allow_blank =>true, :allow_nil => true
      validates_length_of :club_shortcut, :within => 2..10, :allow_nil => true, :allow_blank => true  
      include ClubMasquerade::InstanceMethods
    end
  end
 
  #  Hier sind die zu erweiternden Objekt- / Instanzmethoden drin
  module InstanceMethods
    # ...
  end
 
end

Beachte: mein Modul “ClubMasquerade” hat 2 Unter-Module “ClubMasquerade::ClassMethods” und “ClubMasquerade::InstanceMethods”

2. Und nun kommt das Kunststück: durch diese magische Zeile wird in meinem Modul die Funktion “self.included” (siehe 1.) ausgeführt. Die darin beschrieben Funktion ruft per “Reflection” die Methode “include” für ActiveRecord::Base auf und fügt damit den Funktionsumfang meines Moduls ClubMasquerade dynamisch der Klasse hinzu.

Im Projekt-Ordner vendor/plugins/club_masquerade schreibe ich dazu in die Datei init.rb

ActiveRecord::Base.send(:include, ClubMasquerade)

3. Kurzer Zwischenstand: Wenn Rails beim Hochfahren des Systems nun die init.rb findet und ausführt (Automatismus), dann wird der Funktionsumfang von ActiveRecord erweitert um meine eigenen Funktionen. Das bedeutet, dass auch in allen Model-Klassen (die sich ja von ActiveRecord ableiten) die Funktion “acts_as_club_masquerade” zur Verfügung steht.

In jedem Model wird nun eingetragen:

class Ship < ActiveRecord::Base
  acts_as_club_masquerade
# ...
end

Durch den Aufruf dieser Klassenmethode beim Interpretieren der Klasse “Ship” wird der zugehörige Codeblock aus meinem Modul ausgeführt (siehe oben). Und was macht der Code-Block “acts_as_club_masquerade” ? Er fügt wiederum Instanzmethoden zum Funktionsumfang meines Objektes hinzu, nämlich genau die Funktionen, die ich im Objekt erweitern wollte. Es lebe Reflection. Hoch Introspection. Vive la revolution!

Das Ergebnis:
Alle Model-Klassen, die ich mit meinem “acts_as_club_masquerade” versehen habe (und nur die) besitzen einen um meine Wünsche erweiterten Funktionsumfang, den ich entsprechend nutzen kann.

So, ich hoffe, das hilft ;-) !


Andreas Jene