Beispiele und Hinweise zur Nutzung von Datenbank-Mappern


Sie sind hier: RubyDatenbanken


Zum Vergleich

Im folgenden gibt es einen kleinen Vergleich verschiedener Datenbank-Anbindung mit Ruby.

Die Tests erfolgen im folgenden Umfeld:

Bei Fragen oder Hinweisen: Im Ruby-Forum gibt es einen Thread zu dieser Seite:

Dieses Dokument gibt es in zwei Fassungen:

Geprüfte DB-Mapper

Die jeweiligen Testfiles sind angehängt, in Klammern die Version des gems, die ich nutzte.

Weitere - hier nicht beschriebene - Möglichkeiten:

Für die folgende Einteilung danke ich Skade aus dem Ruby-Forum.

ActiveRecord

ActiveRecord ist ein ORM - ein Objekt-Relationaler-Mapper. Es setzt also Relationen aus der Datenbank automatisch in Objekte im Programm um - die im AR-Lingus nun mal Models heissen.

DataMapper

DataMapper zäumt das Pferd etwas anders auf, aber es gilt grundsätzlich das gleiche: DataMapper ist eine Objektpersistenzschicht, die auch SQL kann. DataMapper kann genauso problemlos in Couch oder Redis persistieren, wenn man das denn will, auch wenn gewisse Features (zum Beispiel Relationen) von diesen Stores nicht unterstützt werden. In DataMapper könnte man sogar sagen, dass es ohne DataMapper-Objekt garnicht geht, weil man sonst ja nix zum persistieren hat.

Sequel

Sequel dahingegen ist kein ORM, sondern eine Bibliothek zur Interaktion mit SQL-Datenbanken. Sequel::Model ist daher nur eine Erweiterung, der man durchaus auch anmerkt, dass sie nicht das Hauptaugenmerk der Sache ist. Unbequem ist sie zwar nicht, man muss sich aber viel selbst zusammmenbauen.

Testverfahren

Je Mapper gibt es ein Rakefile, das die identischen Testfälle als Task enthält. Die einzelnen Task führen den Test aus und geben (sofern sinnvoll) ihr Ergebnis aus.

Die Tests werden im folgenden mit Testcode, Ausgabe und Kommentar gelistet.

Als Testdaten wurden verwendet:

Und falls beim lesen jemand Lust auf die Filme von Buster Keaton bekommt:

Testfälle

Weitere Testfälle

Bisher hier nicht aufgenommen:

An Datenbank anmelden

SQLite

active_recorddatamappersequel
Testvariante :connect_sqlite
ActiveRecord::Base.establish_connection( 
    :adapter  => "sqlite3",
    :database => ":memory:"
  )
#Without filename -> DB in memory
DB = DataMapper.setup(:default, 'sqlite::memory:')
#Without filename -> DB in memory
DB = Sequel.sqlite()
  • Multiple DB can be defined with class DB < ActiveRecord::Base

ODBC

active_recorddatamappersequel
Testvariante :connect_odbc
#dsn, username and password must be known.
ActiveRecord::Base.establish_connection( 
  :adapter=>"sqlserver", 
  :mode=>"odbc", 
  :dsn    => $dsn,
  :username => $username, 
  :password=> $password
)
#dsn, username and password must be known.
#
#???
#
#dsn, username and password must be known.
DB = Sequel.connect(
  :adapter=>'ado', 
  :host=> $server, 
  :database=>$dsn, 
  :user=> $username, 
  :password=>$password
)

Bisher erfolglos. Siehe http://forum.ruby-portal.de/viewtopic.php?f=22&t=11545

Abmelden (disconnect)

active_recorddatamappersequel
Testvariante :disconnect
ActiveRecord::Base.connection.disconnect!
nil
DB.disconnect

Scheint es nicht zu geben:

Models

Models sind Objekte, die Tabelleneinträge entsprechen. Über Models sind DB-Aktionen vereinfacht möglich.

Models definieren

active_recorddatamappersequel
Testvariante :create_model
class Film < ActiveRecord::Base
  #set_table_name :films
end
class Film
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :year, Integer
end
class Film <  Sequel::Model(:films); end
  • Es wird implizit eine Tabelle films angenommen
  • Abweichende Tabellennamen können angegeben werden.
  1. Model Film verweist auf Tabelle films.
  1. Model Film verweist auf Tabelle films.
  2. Abweichende Tabellennamen sind möglich

Datenbank manipulieren

Tabelle erzeugen (create table)

active_recorddatamappersequel
Testvariante :create_tables
ActiveRecord::Schema.define do 
    create_table :films do |t|
    #~ primary_key :fid
    t.column :title, :string
    t.column :year, :string, :size => 4
  end
end
require 'dm-migrations'
class Film
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :year, Integer
end
DataMapper.auto_migrate!
DB.create_table :films do 
  #~ primary_key :fid
  column :title, :string
  column :year, :string, :size => 4
end
  • Es wird implizit ein Attribut 'id' angelegt.
  • Spalten werden im Block angelegt.
  • ActiveRecord::Migration schreibt Messages auf STDOUT. Umdefinieren von ActiveRecord::Migration.write verhindert dies.
  • auto_migrate! passt die DB den Models an.
  • Übliche Ablauf über rake db:automigrate

Das Verzichten auf den primary_key mag nicht mit jeder DB funktionieren.

Testvariante :create_tables_2
ActiveRecord::Schema.define do 
  create_table( :films )
  add_column(:films, :title, :string)
  add_column(:films, :year, :string, :size => 4)
end
  • Tabelle wird ohne Spalten angelegt
  • Spalten werden anschliessend angelegt.

Tabelleneinträge anlegen (insert)

active_recorddatamappersequel
Testvariante :insert_values
#Model Film wurde bereits definiert
Film.create(:title => 'Three Ages', :year => 1923)
Film.create(:title => 'Three Ages', :year => 1923)
Film.new(
    :title => 'Steamboat Bill Jr.', :year => 1928
  ).save
Film.insert( 
    :title => 'Three Ages', :year => 1923
  )
Testvariante :insert_values_no_model
DB[:films].insert( 
    :title => 'Three Ages', :year => 1923
  )

Insert funktioniert auch ohne Model

Tabelleneinträge ändern (update)

active_recorddatamappersequel
Testvariante :update
#1 = interne id
Film.update(1, :year => 'egal' )
Film.find(:all, :conditions => "year = 'egal'")
#~ Film.update(:year => 0 )
Film.all(:year => 1923 ).update(:year => 0 )
#Model Film wurde bereits definiert  
Film.update(:year => 'egal')
  • Das Update-Kommando gibt die Anzahl geänderter Datensätze zurück.
Testvariante :update_no_model
DB[:films].update(:year => 'egal')
  • Update funktioniert auch ohne Model
  • Das Update-Kommando gibt die Anzahl geänderter Datensätze zurück.

Views anlegen und verwenden

active_recorddatamappersequel
Testvariante :create_view
nil
nil
DB.create_view(:films1923, 
    DB[:films].
      where(:year => 1923).
      select(:title)
  )
DB[:films1923].each{|f| p f}
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}
  • Ansatzpunkte:
    • Migration
    • Raw sql mit execute
  • Ansatzpunkte:
    • Migration
    • Raw sql mit execute

'Native' SQL verwenden

active_recorddatamappersequel
Testvariante :use_sql_select
ActiveRecord::Base.connection.execute(
    "select title from films where year = '1923'"
  ).each{|f| p f }
p DB.select(
    "select title from films where year = '1923'"
  )
DB.fetch(
  "select title from films where year = 1923"
){|f| p f }
{0=>"Three Ages", "title"=>"Three Ages"}
{0=>"Our Hospitality", "title"=>"Our Hospitality"}
["Three Ages", "Our Hospitality"]
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}
  • Array mit hash.
  • SQL-selects können mit fetch gestartet werden.
  • Ohne Block kann die Anweisung in einer Variablen gespeichert und später ausgeführt werden.
Testvariante :use_sql
puts "Before: #{DB.tables.inspect}"
DB.run( "create table actors (
    id integer primary key, 
    name varchar(255)
  )"
)
puts "After:  #{DB.tables.inspect}"
Before: [:films]
After:  [:films, :actors]
  • SQL-Kommandos können mit DB#run gestartet werden.
Testvariante :use_sql_inverse
puts Film.select(:title).
          where(:year => 1923).sql
SELECT `title` FROM `films` WHERE (`year` = 1923)
  • Das Kommando #sql gibt für die Aktionen die entsprechenden SQL-Kommandos zurück.

Tabelleneinträge lesen (select)

Verarbeitungsmethoden

Mit Array als Ergebnis

active_recorddatamappersequel
Testvariante :select2array
#Model Film wurde bereits definiert
Film.find(:all)
Film.all()
#Model Film wurde bereits definiert
Film.all

Die Definition eines Models ist notwendig (?)

Die Definition von Models ist möglich.

Testvariante :select2array_no_model
DB[:films].all

Blockverarbeitung

active_recorddatamappersequel
Testvariante :select2block
#Model Film wurde bereits definiert
Film.find(:all).each{|film|
  p film
}
Film.each{|film|
  film
}
Film.select{|film|
  p film
}
#Model Film wurde bereits definiert
Film.each{|film|
  p film
}
#<Film id: 1, title: "The Saphead", year: "1920">
#<Film id: 2, title: "Three Ages", year: "1923">
#<Film id: 3, title: "Our Hospitality", year: "1923">
#<Film id: 4, title: "Sherlock Jr.", year: "1924">
#<Film id: 5, title: "The Navigator", year: "1924">
#<Film id: 6, title: "Seven Chances", year: "1925">
#<Film id: 7, title: "Go West", year: "1925">
#<Film id: 8, title: "Battling Butler", year: "1926">
#<Film id: 9, title: "The General", year: "1926">
#<Film id: 10, title: "College", year: "1927">
#<Film id: 11, title: "Steamboat Bill Jr.", year: "1928">
#<Film id: 12, title: "The Cameraman", year: "1928">
#<Film id: 13, title: "Spite Marriage", year: "1929">
#<Film @id=1 @title="The Saphead" @year=1920>
#<Film @id=2 @title="Three Ages" @year=1923>
#<Film @id=3 @title="Our Hospitality" @year=1923>
#<Film @id=4 @title="Sherlock Jr." @year=1924>
#<Film @id=5 @title="The Navigator" @year=1924>
#<Film @id=6 @title="Seven Chances" @year=1925>
#<Film @id=7 @title="Go West" @year=1925>
#<Film @id=8 @title="Battling Butler" @year=1926>
#<Film @id=9 @title="The General" @year=1926>
#<Film @id=10 @title="College" @year=1927>
#<Film @id=11 @title="Steamboat Bill Jr." @year=1928>
#<Film @id=12 @title="The Cameraman" @year=1928>
#<Film @id=13 @title="Spite Marriage" @year=1929>
#<Film @values={:title=>"The Saphead", :year=>1920}>
#<Film @values={:title=>"Three Ages", :year=>1923}>
#<Film @values={:title=>"Our Hospitality", :year=>1923}>
#<Film @values={:title=>"Sherlock Jr.", :year=>1924}>
#<Film @values={:title=>"The Navigator", :year=>1924}>
#<Film @values={:title=>"Seven Chances", :year=>1925}>
#<Film @values={:title=>"Go West", :year=>1925}>
#<Film @values={:title=>"Battling Butler", :year=>1926}>
#<Film @values={:title=>"The General", :year=>1926}>
#<Film @values={:title=>"College", :year=>1927}>
#<Film @values={:title=>"Steamboat Bill Jr.", :year=>1928}>
#<Film @values={:title=>"The Cameraman", :year=>1928}>
#<Film @values={:title=>"Spite Marriage", :year=>1929}>
Testvariante :select2block_no_model
DB[:films].each{|film|
  p film
}
{:title=>"The Saphead", :year=>1920}
{:title=>"Three Ages", :year=>1923}
{:title=>"Our Hospitality", :year=>1923}
{:title=>"Sherlock Jr.", :year=>1924}
{:title=>"The Navigator", :year=>1924}
{:title=>"Seven Chances", :year=>1925}
{:title=>"Go West", :year=>1925}
{:title=>"Battling Butler", :year=>1926}
{:title=>"The General", :year=>1926}
{:title=>"College", :year=>1927}
{:title=>"Steamboat Bill Jr.", :year=>1928}
{:title=>"The Cameraman", :year=>1928}
{:title=>"Spite Marriage", :year=>1929}

Ergebnisseinschränkungen

Select mit Filter (where)

active_recorddatamappersequel
Testvariante :select_where
#Model Film wurde bereits definiert
res = Film.find(:all, :conditions => "year = '1923'")
res = Film.all(:year => 1923 )  #equal
#Model Film wurde bereits definiert
Film.filter( :year => 1923 ).all
Film.where( :year => 1923 ).each{|f|
  p f
}
#<Film id: 2, title: "Three Ages", year: "1923">
#<Film id: 3, title: "Our Hospitality", year: "1923">
#<Film @id=2 @title="Three Ages" @year=1923>
#<Film @id=3 @title="Our Hospitality" @year=1923>
#<Film @values={:title=>"Three Ages", :year=>1923}>
#<Film @values={:title=>"Our Hospitality", :year=>1923}>
Testvariante :select_where_gt
res = Film.find(:all, :conditions => "year > '1927'")
res = Film.all(:year.gt => 1927 )
#Model Film wurde bereits definiert
res = Film.where{ :year > 1927 }.all
res = Film.filter{ :year > 1927 }.each{|f| p f }
#<Film id: 11, title: "Steamboat Bill Jr.", year: "1928">
#<Film id: 12, title: "The Cameraman", year: "1928">
#<Film id: 13, title: "Spite Marriage", year: "1929">
#<Film @id=11 @title="Steamboat Bill Jr." @year=1928>
#<Film @id=12 @title="The Cameraman" @year=1928>
#<Film @id=13 @title="Spite Marriage" @year=1929>
#<Film @values={:title=>"Steamboat Bill Jr.", :year=>1928}>
#<Film @values={:title=>"The Cameraman", :year=>1928}>
#<Film @values={:title=>"Spite Marriage", :year=>1929}>
  • Symbole bekommen zusätzliche Methoden, die Selection größer als ... ermöglichen.
  • Mit Blöcken sind weitere Vergleiche möglich
Testvariante :select_where_no_model
DB[:films].filter( :year => 1923 ).all
DB[:films].where( :year => 1923 ).all

Select funktioniert auch ohne Model

Testvariante :select_where_in
#Model Film wurde bereits definiert
Film.where( :year => [ 1920, 1923 ] ).all
Film.filter( :year => [ 1920, 1923 ] ).each{|f|
  p f
}
#<Film @values={:title=>"The Saphead", :year=>1920}>
#<Film @values={:title=>"Three Ages", :year=>1923}>
#<Film @values={:title=>"Our Hospitality", :year=>1923}>
Testvariante :select_where_range
#Model Film wurde bereits definiert
res = Film.where( :year => 1920..1923  ).all
res = Film.filter( :year => 1920..1923 ).all
#<Film @values={:title=>"The Saphead", :year=>1920}>
#<Film @values={:title=>"Three Ages", :year=>1923}>
#<Film @values={:title=>"Our Hospitality", :year=>1923}>

Select mit Spaltenauswahl

active_recorddatamappersequel
Testvariante :select_col
#Model Film wurde bereits definiert
res = Film.find(:all, :select => "title")
#Nicht wirklich die Lösung
class Film
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :year, Integer, :lazy => true
end
res = Film.all
#Model Film wurde bereits definiert
Film.select(:title).each{|f|
  p f
}
#<Film title: "The Saphead">
#<Film title: "Three Ages">
#<Film title: "Our Hospitality">
#<Film title: "Sherlock Jr.">
#<Film title: "The Navigator">
#<Film title: "Seven Chances">
#<Film title: "Go West">
#<Film title: "Battling Butler">
#<Film title: "The General">
#<Film title: "College">
#<Film title: "Steamboat Bill Jr.">
#<Film title: "The Cameraman">
#<Film title: "Spite Marriage">
#<Film @id=1 @title="The Saphead" @year=<not loaded>>
#<Film @id=2 @title="Three Ages" @year=<not loaded>>
#<Film @id=3 @title="Our Hospitality" @year=<not loaded>>
#<Film @id=4 @title="Sherlock Jr." @year=<not loaded>>
#<Film @id=5 @title="The Navigator" @year=<not loaded>>
#<Film @id=6 @title="Seven Chances" @year=<not loaded>>
#<Film @id=7 @title="Go West" @year=<not loaded>>
#<Film @id=8 @title="Battling Butler" @year=<not loaded>>
#<Film @id=9 @title="The General" @year=<not loaded>>
#<Film @id=10 @title="College" @year=<not loaded>>
#<Film @id=11 @title="Steamboat Bill Jr." @year=<not loaded>>
#<Film @id=12 @title="The Cameraman" @year=<not loaded>>
#<Film @id=13 @title="Spite Marriage" @year=<not loaded>>
#<Film @values={:title=>"The Saphead"}>
#<Film @values={:title=>"Three Ages"}>
#<Film @values={:title=>"Our Hospitality"}>
#<Film @values={:title=>"Sherlock Jr."}>
#<Film @values={:title=>"The Navigator"}>
#<Film @values={:title=>"Seven Chances"}>
#<Film @values={:title=>"Go West"}>
#<Film @values={:title=>"Battling Butler"}>
#<Film @values={:title=>"The General"}>
#<Film @values={:title=>"College"}>
#<Film @values={:title=>"Steamboat Bill Jr."}>
#<Film @values={:title=>"The Cameraman"}>
#<Film @values={:title=>"Spite Marriage"}>

Nicht wirklich die Lösung.

Alternative 'lazy':

Testvariante :select_col_no_model
DB[:films].select(:title).each{|f|
  p f
}
{:title=>"The Saphead"}
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}
{:title=>"Sherlock Jr."}
{:title=>"The Navigator"}
{:title=>"Seven Chances"}
{:title=>"Go West"}
{:title=>"Battling Butler"}
{:title=>"The General"}
{:title=>"College"}
{:title=>"Steamboat Bill Jr."}
{:title=>"The Cameraman"}
{:title=>"Spite Marriage"}

Select funktioniert auch ohne Model