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:

Nachfragen sind sicherlich angebracht, wenn man den Eindruck hat, es stimmt etwas nicht. Da es sich hier um Einstiegsexperimente handelt, kann es gut sein, das es einfachere Möglichkeiten gibt oder falsch verwendete Features gibt.

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:')
DB = Sequel.sqlite()
  • Multiple DB can be defined with class DB < ActiveRecord::Base
  • Ohne Dateiname wird die DB im Hauptspeicher angelegt.
  • Insbesonders beim Synchron arbeiten kann es deutlich schneller sein.

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.

Datenbankaktionen ohne Models werden weiter unten vorgestellt.

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
  )

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.

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

Tabelleneinträge lesen (select)

Verarbeitungsmethoden

Mit Array als Ergebnis

active_recorddatamappersequel
Testvariante :select2array
#Model Film wurde bereits definiert
res = Film.find(:all)
puts res.inspect[0..42] + '...'
res = Film.all()
puts res.inspect[0,45]
#Model Film wurde bereits definiert
res = Film.all
puts res.inspect[0,40]
[#<Film id: 1, title: "The Saphead", year: ...
[#<Film @id=1 @title="The Saphead" @year=1920
[#<Film @values={:year=>1920, :title=>"T

Die Definition eines Models ist notwendig (?)

Die Definition von Models ist möglich.

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={:year=>1920, :title=>"The Saphead"}>
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>
#<Film @values={:year=>1924, :title=>"Sherlock Jr."}>
#<Film @values={:year=>1924, :title=>"The Navigator"}>
#<Film @values={:year=>1925, :title=>"Seven Chances"}>
#<Film @values={:year=>1925, :title=>"Go West"}>
#<Film @values={:year=>1926, :title=>"Battling Butler"}>
#<Film @values={:year=>1926, :title=>"The General"}>
#<Film @values={:year=>1927, :title=>"College"}>
#<Film @values={:year=>1928, :title=>"Steamboat Bill Jr."}>
#<Film @values={:year=>1928, :title=>"The Cameraman"}>
#<Film @values={:year=>1929, :title=>"Spite Marriage"}>

Ergebnisseinschränkungen

Select mit Filter (where)

active_recorddatamappersequel
Testvariante :select_where
Film.find(  :all, 
              :conditions => "year = '1923'"
          ).each{|f| p f}
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={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>
Testvariante :select_where_gt
Film.find(  :all, 
              :conditions => "year > '1927'"
            ).each{|f| p f}
Film.all(:year.gt => 1927 ).each{|f| p f }
#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={:year=>1928, :title=>"Steamboat Bill Jr."}>
#<Film @values={:year=>1928, :title=>"The Cameraman"}>
#<Film @values={:year=>1929, :title=>"Spite Marriage"}>

In :conditions steht die SQL Where-Klausel.

  • Symbole bekommen zusätzliche Methoden, die Selection größer als ... ermöglichen.
  • Mit Blöcken sind weitere Vergleiche möglich
Testvariante :select_where_in
Film.find(:all, 
            :conditions => "year in ( '1920', '1923')"
          ).each{|f| p f}
Film.all(:year => [ 1920, 1923 ] ).each{|film|
  p film
}
#Model Film wurde bereits definiert
Film.where( :year => [ 1920, 1923 ] ).all
Film.filter( :year => [ 1920, 1923 ] ).each{|f|
  p f
}
#<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=1 @title="The Saphead" @year=1920>
#<Film @id=2 @title="Three Ages" @year=1923>
#<Film @id=3 @title="Our Hospitality" @year=1923>
#<Film @values={:year=>1920, :title=>"The Saphead"}>
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>

In :conditions steht die SQL Where-Klausel.

Testvariante :select_where_range
Film.find(:all, 
            :conditions => "year between '1920' and '1923'"
          ).each{|f| p f}
Film.all(:year => 1920..1923 ).each{|film|
  p film
}  
#Model Film wurde bereits definiert
Film.where( :year => 1920..1923  ).all
Film.filter( :year => 1920..1923 ).each{ |f| 
  p f 
}
#<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=1 @title="The Saphead" @year=1920>
#<Film @id=2 @title="Three Ages" @year=1923>
#<Film @id=3 @title="Our Hospitality" @year=1923>
#<Film @values={:year=>1920, :title=>"The Saphead"}>
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>

In :conditions steht die SQL Where-Klausel.

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':

Datenbankinformationen

Tabellenbeschreibungen

active_recorddatamappersequel
Testvariante :check_schema
Film.reset_column_information() #Change in meantime?
p Film.base_class
puts Film.columns_hash.to_yaml
Film.columns  #Array
Film.properties.each{|prop|
  p prop
}
puts DB.schema(:films).to_yaml
Film(id: integer, title: string, year: string)
--- 
title: !ruby/object:ActiveRecord::ConnectionAdapters::SQLiteColumn 
  default: 
  limit: 255
  name: title
  "null": true
  precision: 
  primary: false
  scale: 
  sql_type: varchar(255)
  type: :string
id: !ruby/object:ActiveRecord::ConnectionAdapters::SQLiteColumn 
  default: 
  limit: 
  name: id
  "null": false
  precision: 
  primary: true
  scale: 
  sql_type: INTEGER
  type: :integer
year: !ruby/object:ActiveRecord::ConnectionAdapters::SQLiteColumn 
  default: 
  limit: 255
  name: year
  "null": true
  precision: 
  primary: false
  scale: 
  sql_type: varchar(255)
  type: :string
#<DataMapper::Property::Serial @model=Film @name=:id>
#<DataMapper::Property::String @model=Film @name=:title>
#<DataMapper::Property::Integer @model=Film @name=:year>
--- 
- - :title
  - :type: 
    :allow_null: true
    :primary_key: false
    :default: 
    :ruby_default: 
    :db_type: string
- - :year
  - :type: 
    :allow_null: true
    :primary_key: false
    :default: 
    :ruby_default: 
    :db_type: string(4)
  • Mit ActiveRecord::SchemaDumper.dump wird ruby-code zum erzeugen der DB-Schemas ausgegeben.
Testvariante :check_schema_columns
p Film.column_names
p Film.properties.map{|prop| prop.name}
p DB[:films].columns
["id", "title", "year"]
[:id, :title, :year]
[:title, :year]
  • Film.key liefert alle Schlüsselfelder.

Datenbankaktion ohne Model

active_recorddatamappersequel
Testvariante :no_model_insert
DB[:films].insert( 
    :title => 'Three Ages', :year => 1923
  )

Insert funktioniert auch ohne Model

Testvariante :no_model_select2array
res = DB[:films].all
puts res.inspect[0,45]
[{:year=>1920, :title=>"The Saphead"}, {:year
Testvariante :no_model_select2block
DB[:films].each{|film|
  p film
}
{:year=>1920, :title=>"The Saphead"}
{:year=>1923, :title=>"Three Ages"}
{:year=>1923, :title=>"Our Hospitality"}
{:year=>1924, :title=>"Sherlock Jr."}
{:year=>1924, :title=>"The Navigator"}
{:year=>1925, :title=>"Seven Chances"}
{:year=>1925, :title=>"Go West"}
{:year=>1926, :title=>"Battling Butler"}
{:year=>1926, :title=>"The General"}
{:year=>1927, :title=>"College"}
{:year=>1928, :title=>"Steamboat Bill Jr."}
{:year=>1928, :title=>"The Cameraman"}
{:year=>1929, :title=>"Spite Marriage"}
Testvariante :no_model_select_where
DB[:films].filter( :year => 1923 ).all
DB[:films].where( :year => 1923 ).each{|f|
  p f
}
{:year=>1923, :title=>"Three Ages"}
{:year=>1923, :title=>"Our Hospitality"}

Select funktioniert auch ohne Model

Testvariante :no_model_select_col
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

Testvariante :no_model_update
DB[:films].update(:year => 'egal')
  • Update funktioniert auch ohne Model
  • Das Update-Kommando gibt die Anzahl geänderter Datensätze zurück.

'Native' SQL verwenden

Als ORM macht es nur bedingt Sinn reines SQL zu verwenden. In manchen Situationen kann es aber sinnvoll bzw. notwendig sein.

Bei lesenden Zugriffen mag es interessant sein, eine entsprechende View zu definieren und auf diese über Models zuzugreifen.

Zum nachlesen:

active_recorddatamappersequel
Testvariante :use_sql
ActiveRecord::Base.connection.execute(
    "create table actors (
    id integer primary key, 
    name varchar(255)
  )"
  )
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_select
ActiveRecord::Base.connection.select_values(
    "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 }
"Three Ages"
"Our Hospitality"
["Three Ages", "Our Hospitality"]
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}
  • Array mit hash.
  • Statt ActiveRecord::Base kann jedes Model verwendet werden.
  • 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_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.