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_record

Testvariante :connect_sqlite
ActiveRecord::Base.establish_connection( 
    :adapter  => "sqlite3",
    :database => ":memory:"
  )

datamapper

Testvariante :connect_sqlite
#Without filename -> DB in memory
DB = DataMapper.setup(:default, 'sqlite::memory:')

sequel

Testvariante :connect_sqlite
DB = Sequel.sqlite()

ODBC

active_record

Testvariante :connect_odbc
#dsn, username and password must be known.
ActiveRecord::Base.establish_connection( 
  :adapter=>"sqlserver", 
  :mode=>"odbc", 
  :dsn    => $dsn,
  :username => $username, 
  :password=> $password
)

datamapper

Testvariante :connect_odbc
#dsn, username and password must be known.
#
#???
#

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

sequel

Testvariante :connect_odbc
#dsn, username and password must be known.
DB = Sequel.connect(
  :adapter=>'ado', 
  :host=> $server, 
  :database=>$dsn, 
  :user=> $username, 
  :password=>$password
)

Abmelden (disconnect)

active_record

Testvariante :disconnect
ActiveRecord::Base.connection.disconnect!

datamapper

Testvariante :disconnect
nil

Scheint es nicht zu geben:

sequel

Testvariante :disconnect
DB.disconnect

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_record

Testvariante :create_model
class Film < ActiveRecord::Base
  #set_table_name :films
end

datamapper

Testvariante :create_model
class Film
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :year, Integer
end
  1. Model Film verweist auf Tabelle films.

sequel

Testvariante :create_model
class Film <  Sequel::Model(:films)
end
  1. Model Film verweist auf Tabelle films.
  2. Abweichende Tabellennamen sind möglich

Datenbank manipulieren

Tabelle erzeugen (create table)

active_record

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
Testvariante :create_tables_2
ActiveRecord::Schema.define do 
  create_table( :films )
  add_column(:films, :title, :string)
  add_column(:films, :year, :string, :size => 4)
end

datamapper

Testvariante :create_tables
require 'dm-migrations'
class Film
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :year, Integer
end
DataMapper.auto_migrate!

sequel

Testvariante :create_tables
DB.create_table :films do 
  #~ primary_key :fid
  column :title, :string
  column :year, :string, :size => 4
end

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

Tabelleneinträge anlegen (insert)

active_record

Testvariante :insert_values
#Model Film wurde bereits definiert
Film.create(:title => 'Three Ages', :year => 1923)

datamapper

Testvariante :insert_values
Film.create(:title => 'Three Ages', :year => 1923)
Film.new(
    :title => 'Steamboat Bill Jr.', :year => 1928
  ).save

sequel

Testvariante :insert_values
Film.insert( 
    :title => 'Three Ages', :year => 1923
  )

Tabelleneinträge ändern (update)

active_record

Testvariante :update
#1 = interne id
Film.update(1, :year => 'egal' )
Film.find(:all, :conditions => "year = 'egal'")

datamapper

Testvariante :update
#~ Film.update(:year => 0 )
Film.all(:year => 1923 ).update(:year => 0 )

sequel

Testvariante :update
#Model Film wurde bereits definiert  
Film.update(:year => 'egal')

Views anlegen und verwenden

active_record

Testvariante :create_view
nil

datamapper

Testvariante :create_view
nil

sequel

Testvariante :create_view
DB.create_view(:films1923, 
    DB[:films].
      where(:year => 1923).
      select(:title)
  )
DB[:films1923].each{|f| p f}
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}

Tabelleneinträge lesen (select)

Verarbeitungsmethoden

Mit Array als Ergebnis

active_record

Testvariante :select2array
#Model Film wurde bereits definiert
res = Film.find(:all)
puts res.inspect[0..42] + '...'
[#<Film id: 1, title: "The Saphead", year: ...

Die Definition eines Models ist notwendig (?)

datamapper

Testvariante :select2array
res = Film.all()
puts res.inspect[0,45]
[#<Film @id=1 @title="The Saphead" @year=1920

sequel

Testvariante :select2array
#Model Film wurde bereits definiert
res = Film.all
puts res.inspect[0,40]
[#<Film @values={:year=>1920, :title=>"T

Die Definition von Models ist möglich.

Blockverarbeitung

active_record

Testvariante :select2block
#Model Film wurde bereits definiert
Film.find(:all).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">

datamapper

Testvariante :select2block
Film.each{|film|
  film
}
Film.select{|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>

sequel

Testvariante :select2block
#Model Film wurde bereits definiert
Film.each{|film|
  p film
}
#<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_record

Testvariante :select_where
Film.find(  :all, 
              :conditions => "year = '1923'"
          ).each{|f| p f}
#<Film id: 2, title: "Three Ages", year: "1923">
#<Film id: 3, title: "Our Hospitality", year: "1923">
Testvariante :select_where_gt
Film.find(  :all, 
              :conditions => "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">

In :conditions steht die SQL Where-Klausel.

Testvariante :select_where_in
Film.find(:all, 
            :conditions => "year in ( '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">

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 id: 1, title: "The Saphead", year: "1920">
#<Film id: 2, title: "Three Ages", year: "1923">
#<Film id: 3, title: "Our Hospitality", year: "1923">

In :conditions steht die SQL Where-Klausel.

datamapper

Testvariante :select_where
res = Film.all(:year => 1923 )  #equal
#<Film @id=2 @title="Three Ages" @year=1923>
#<Film @id=3 @title="Our Hospitality" @year=1923>
Testvariante :select_where_gt
Film.all(:year.gt => 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>
Testvariante :select_where_in
Film.all(:year => [ 1920, 1923 ] ).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>
Testvariante :select_where_range
Film.all(:year => 1920..1923 ).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>

sequel

Testvariante :select_where
#Model Film wurde bereits definiert
Film.filter( :year => 1923 ).all
Film.where( :year => 1923 ).each{|f|
  p f
}
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>
Testvariante :select_where_gt
#Model Film wurde bereits definiert
res = Film.where{ :year > 1927 }.all
res = Film.filter{ :year > 1927 }.each{|f| p f }
#<Film @values={:year=>1928, :title=>"Steamboat Bill Jr."}>
#<Film @values={:year=>1928, :title=>"The Cameraman"}>
#<Film @values={:year=>1929, :title=>"Spite Marriage"}>
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={:year=>1920, :title=>"The Saphead"}>
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>
Testvariante :select_where_range
#Model Film wurde bereits definiert
Film.where( :year => 1920..1923  ).all
Film.filter( :year => 1920..1923 ).each{ |f| 
  p f 
}
#<Film @values={:year=>1920, :title=>"The Saphead"}>
#<Film @values={:year=>1923, :title=>"Three Ages"}>
#<Film @values={:year=>1923, :title=>"Our Hospitality"}>

Select mit Spaltenauswahl

active_record

Testvariante :select_col
#Model Film wurde bereits definiert
res = Film.find(:all, :select => "title")
#<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">

datamapper

Testvariante :select_col
#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
#<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>>

Nicht wirklich die Lösung.

Alternative 'lazy':

sequel

Testvariante :select_col
#Model Film wurde bereits definiert
Film.select(:title).each{|f|
  p f
}
#<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"}>

Datenbankinformationen

Tabellenbeschreibungen

active_record

Testvariante :check_schema
Film.reset_column_information() #Change in meantime?
p Film.base_class
puts Film.columns_hash.to_yaml
Film.columns  #Array
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
Testvariante :check_schema_columns
p Film.column_names
["id", "title", "year"]

datamapper

Testvariante :check_schema
Film.properties.each{|prop|
  p prop
}
#<DataMapper::Property::Serial @model=Film @name=:id>
#<DataMapper::Property::String @model=Film @name=:title>
#<DataMapper::Property::Integer @model=Film @name=:year>
Testvariante :check_schema_columns
p Film.properties.map{|prop| prop.name}
[:id, :title, :year]

sequel

Testvariante :check_schema
puts DB.schema(:films).to_yaml
--- 
- - :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)
Testvariante :check_schema_columns
p DB[:films].columns
[:title, :year]

Datenbankaktion ohne Model

active_record

datamapper

sequel

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

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

Testvariante :use_sql
ActiveRecord::Base.connection.execute(
    "create table actors (
    id integer primary key, 
    name varchar(255)
  )"
  )
Testvariante :use_sql_select
ActiveRecord::Base.connection.select_values(
    "select title from films where year = '1923'"
  ).each{|f| p f }
"Three Ages"
"Our Hospitality"

datamapper

Testvariante :use_sql
Testvariante :use_sql_select
p DB.select(
    "select title from films where year = '1923'"
  )
["Three Ages", "Our Hospitality"]

sequel

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]
Testvariante :use_sql_select
DB.fetch(
  "select title from films where year = 1923"
){|f| p f }
{:title=>"Three Ages"}
{:title=>"Our Hospitality"}
Testvariante :use_sql_inverse
puts Film.select(:title).
          where(:year => 1923).sql
SELECT `title` FROM `films` WHERE (`year` = 1923)