Kleine Tricks in Ruby on Rails

Hier eine kleine Sammlung von Dingen, die mir geholfen haben beim Lösen der Probleme die sich einem N00b so stellen.

Exportieren von Datensätzen ins YAML Format

Rails Fixtures sind Testdatensätze, die im relativ übersichtlichen YAML-Format gespeichert werden. Möchte man bestehende Datenbankinhalte als Testdatensätze verwenden, so können Sie direkt in YAML exportiert werden.

Ich halte mich an den Beitrag von NetManiac. Den Code habe ich nur soweit ergänzt, als dass ich das Exportieren der magischen Felder „id“, „created_at“ und „updated_at“ verhindere und den Dateinamen ändere, damit keine bestehenden Fixturen überschrieben werden.

Aufgerufen mit rake extract_fixtures['mymodel'] speichert es mymodel.dumped.yml im richtigen Verzeichnis.

Datei: libs/tasks/extract_fixtures.rake:

desc 'Create YAML test fixtures from data in an existing database.
Defaults to development database. Set RAILS_ENV to override. Use args
table and limit to dump one table and limit number of records'

task :extract_fixtures, :table, :limit, :needs => :environment do |t, args|
  args.with_defaults(:table => nil, :limit => nil)
  limit = args.limit.nil? ? "" : "LIMIT #{args.limit}"
  sql = "SELECT * FROM %s #{limit}"
  skip_tables = ["schema_info" ]
  if args.table.nil?
    tables = ActiveRecord::Base.connection.tables - skip_tables
  else
    tables = [ "#{args.table}"]
  end

  ActiveRecord::Base.establish_connection
  tables.each do |table_name|
    i = "000"
    File.open("#{RAILS_ROOT}/test/fixtures/#{table_name}.dumped.yml" , 'w' ) do |file|
      data = ActiveRecord::Base.connection.select_all(sql % table_name)
      file.write data.inject({}) { |hash, record|
        record.delete_if{|key, value| ['id', 'created_at', 'updated_at'].include?(key)}
        hash["#{table_name}_#{i.succ!}"] = record
        hash
      }.to_yaml
    end
  end
end

ActiveRecord Attribute beim Auslesen mit Defaultwerten überschreiben

Wenn ein Attribut immer beim Auslesen mit eineb bestimmten Wert belegt werden soll wenn noch nichts vorhanden ist, so geht dies mittels folgendem Codefragment:

def my_attribute
  read_attribute(:my_attribute) or another_attribute.capitalize+" "+yet_another_attribute
end

has_and_belongs_to_many mit Checkboxen in Fieldsets

Eine Rolle hat viele Permissions und umgekehrt. Um solche Assoziationen abzubilden, kann man collection_select verwenden, das aber leider nur ein hässliches Listfeld ausgibt. Eine ambitioniertere Variante arbeitet mit Checkboxen.

HABTM mit Checkboxen

HABTM mit Checkboxen

Zuerst wird in Permission.get_indexed_by_controller ein Hash of Hashes gemacht, der als ersten Schlüssel die Überschriften hat (controller) als zweiten Schlüssel die Feldbeschriftungen mit dem Zustand als Inhalt. Folgendes Partial gibt ein Formular aus und versieht sie mit „(Un-)Check all“ Links:

<%= link_to_function 'Check all', "$$('input.permission').each(function(checkbox) { checkbox.checked = true; })" %> |
<%= link_to_function 'Uncheck all', "$$('input.permission').each(function(checkbox) { checkbox.checked = false; })" %>
<% @permissions_indexed_by_controller=Permission.get_indexed_by_controller %>
<br />
<%- for controller in @permissions_indexed_by_controller.keys -%>
  <% field_set_tag controller do %>
    <%- for permission in @permissions_indexed_by_controller[controller] -%>
      <%= check_box_tag 'role[permission_ids][]', permission.id, @role.permissions.include?(permission),{ :id => "role_permission_ids_"+ permission.id.to_s, :class => "permission"} -%><%= permission.description %>
    <%- end -%>
  <% end %>
<%- end -%>

Wichtig ist, dass im entsprechenden Controller in der update Funktion ein leeres Array erzeugt wird wenn nichts übergeben wird, sonst ändert sich nichts wenn man alle Häckchen rausnimmt:

params[:role][:permission_ids] ||= []

Erzeugen eines Hash of Hashes in Ruby

Die Hash/Array implementierung in PHP ist immernoch ungeschlagen. Will man in Ruby ein Hash of Hashes machen und einen Wert mittels << hinzufügen wenn noch kein leerer Hash besteht, so tut es blöd. Insbesondere wenn man Records in einem Hash abspeichen will, hilft folgendes Idiom:

      (ret[map_key] ||= []) << record

Active Record Pattern in PHP

StorageIn PHP gibt es verschiedene Implementierungen des Active Record Patterns. Auch wenn es uns nimmer so auffällt, die objektorientierte Welt des Codes und die relationale Welt der Daten sind zwei verschiedene Paradigmen, und es braucht unheimlich viel Leim um sie zusammenzukleben. Objektorientierte Datenbanken entwickeln sich, sind jedoch noch nicht Standard (um es mal vorsichtig auszudrücken). Das Active Record Pattern hilft uns mit ein Bisschen Standard-Klebe.

Mit diesem Pattern können Daten aus der Datenbank gesucht und direkt in Objekte übergeführt werden. Änderungen an den Objekten und das Speichern bewirken, dass diese Änderungen auch in die Datenbank übernommen werden.

Ich habe mich für MyActiveRecord von Jake Grimley entschieden, weil es sauber implementiert, nicht zu gross und trotzdem nicht featurearm ist. Die Dokumentation ist ziemlich gut und reicht aus um zu starten.

Benutzung von MyActiveRecord

Suchen und finden

Der Dreh- und Angelpunkt dürfte die Funktion FindAll($class, $where=0, $orderby='id ASC', $limit=10000, $offset=0) sein. Sie liefert eine Kollektion von Objekten der entsprechenden Klasse zurück:

foreach (MyActiveRecord :: FindAll('thing') as $thing) {
  echo "I am a {$thing->name}!<br />\n";
}

Weiterlesen

Einfache Objektpersistenz mit Netminds Persistence for Java

Um einen weiteren komplexen und fehleranfälligen Teil unter meine Applikationen zu schieben, wollte ich Objektpersistenz mal ausprobieren. Es gibt da verschiedene Möglichkeiten:

  • Objektorientierte DB
  • Persistenzframework bzw Mapper auf eine relationale DB

Für Java gibts da die Schwergewichte Hybernate und JDO (bspw. mittels JPOX), die mir aber für ein kurzes Schnuppern zu schwergewichtig waren. Ich habe mich für Netminds „Persistence for Java“ entschieden. Nachtrag: Das Projekt wurde umbenannt und heisst nun Beankeeper.

Bestehende Unterlagen:

Erstellen eines Projekts

Ich habe zum Ausprobieren Eclipse verwendet.

  1. Neues Java Projekt erstellen
  2. „lib/“ Verzeichnis anlegen
  3. Die Libraries netmind, log4j und java-cup-11-runtime sind im Download von Net Persistence dabei. Diese ins „lib/“ Verzeichnis kopieren.
  4. Den JDBC-Treiber (ev. den für MySQL) auch in dieses Verzeichnis.
  5. Rechtsklick auf das Projekt -> Properties -> Java Build Path -> Libraries -> Add JARs und alle Libs hinzufügen.
  6. Eine Datenbank und einen DB-Benutzer erstellen. Schema braucht es keines.

Quirks:

Beim Start gabs bei mir folgende Meldung:

Exception in thread "main" hu.netmind.persistence.StoreException: there are multiple network interfaces, but no recognizable preferred subnetwork ip

Ein Blick auf den Quellcode ergab, dass er in /java/hu/netmind/persistence/node/NodeServer.java, Zeile 151 irgendwie alle Interfaces durchgeht und nur eines auswählt wenn es mit „10.“ oder „192.“ beginnt. Meines beginnt mit 172. und so musste ich das in dieser Quelldatei ergänzen. Ant und Maven treiben mir den Angstschweiss auf die Stirn, aber ein simples „ant“ im Basisverzeichnis des Downloads hat mir eine neue Lib gemacht. Super!

Code

Hier der Code… Als Resultat sollte er komplett und verständlich sein….

AddressStarter.java:

package ch.baden.Modul223.LA1002;

import hu.netmind.persistence.Store;
import java.util.*;

public class AdressStarter {
    public static void main(String[] args) {
        Store store = new Store("com.mysql.jdbc.Driver",
                "jdbc:mysql://localhost/persistence?user=persistence&amp;password=persistence");
        Address myAddress = new Address("Sarg", "Skaldrom Y.", "000-000");
        store.save(myAddress);
        List addresses = store.find("find Address");
        System.out.println("Total results: " + addresses.size());
        for (Object object : addresses) {
            Address bean = (Address) object;
            System.out.println(bean.toString());
        }
    }
}

Address.java:

package ch.baden.Modul223.LA1002;

public class Address {
    private String name;

    private String vorname;

    private String nummer;

    public Address(String name, String vorname, String nummer) {
        super();
        this.name = name;
        this.vorname = vorname;
        this.nummer = nummer;
    }

    public Address() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNummer() {
        return nummer;
    }

    public void setNummer(String nummer) {
        this.nummer = nummer;
    }

    public String getVorname() {
        return vorname;
    }

    public void setVorname(String vorname) {
        this.vorname = vorname;
    }

    public String toString() {
        return this.name + " " + this.vorname + "/" + this.nummer;
    }
}