Zum Vergleich
Im folgenden gibt es einen kleinen Vergleich verschiedener Datenbank-Anbindung mit Ruby.
Die Tests erfolgen im folgenden Umfeld:
- Betriebssystem Windows
- SQLite3 wird verwendet.
- Ruby Version 1.8 (+ Vorschau auf 1.9).
- Es ist angedacht verschiedene Datenbanken in einem Programm zu verwenden.
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.
- activerecord ./test_active_record.txt (2.3.8)
- datamapper ./test_datamapper.txt
- sequel ./test_sequel.txt (3.13.0)
Weitere - hier nicht beschriebene - Möglichkeiten:
- dbi - sehr nahe am SQL. Ich hatte Probleme sqlite anzubinden.
- sqlite3 - Wie dbi, allerdings ohne Probleme für sqlite
- winole siehe Source: http://snippets.dzone.com/posts/show/3906
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:
- Testdaten ./filmdaten.yaml
Und falls beim lesen jemand Lust auf die Filme von Buster Keaton bekommt:
Testfälle
- Datenbank anmelden
- SQLite
- ODBC
- Models anlegen
- Tabelle erzeugen (create table)
- Tabelleneinträge anlegen (insert)
- Tabelleneinträge lesen (select)
- Tabelleneinträge ändern (update)
- Views anlegen und verwenden
- 'Native' SQL verwenden
Weitere Testfälle
Bisher hier nicht aufgenommen:
- select mit order/sort
- kombiniertes select (Spalten/Zeilen/oder...)
- Joins (inner/outer)
- Migrations
An Datenbank anmelden
SQLite
active_record
Testvariante :connect_sqlite
ActiveRecord::Base.establish_connection( :adapter => "sqlite3", :database => ":memory:" )
- Multiple DB can be defined with class DB < ActiveRecord::Base
datamapper
Testvariante :connect_sqlite
#Without filename -> DB in memory DB = DataMapper.setup(:default, 'sqlite::memory:')
sequel
Testvariante :connect_sqlite
DB = Sequel.sqlite()
- Ohne Dateiname wird die DB im Hauptspeicher angelegt.
- Insbesonders beim Synchron arbeiten kann es deutlich schneller sein.
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:
- http://www.mail-archive.com/datamapper@googlegroups.com/msg02894.html
- http://tuts9.com/questions/25524/how-to-disconnect-datamappers-repository-database
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
- Es wird implizit eine Tabelle films angenommen
- Abweichende Tabellennamen können angegeben werden.
datamapper
Testvariante :create_model
class Film include DataMapper::Resource property :id, Serial property :title, String property :year, Integer end
- Model Film verweist auf Tabelle films.
sequel
Testvariante :create_model
class Film < Sequel::Model(:films) end
- Model Film verweist auf Tabelle films.
- 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
- 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.
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.
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!
- auto_migrate! passt die DB den Models an.
- Übliche Ablauf über rake db:automigrate
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')
- Das Update-Kommando gibt die Anzahl geänderter Datensätze zurück.
Views anlegen und verwenden
active_record
Testvariante :create_view
nil
- Ansatzpunkte:
- Migration
- Raw sql mit execute
datamapper
Testvariante :create_view
nil
- Ansatzpunkte:
- Migration
- Raw sql mit execute
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>
- Symbole bekommen zusätzliche Methoden, die Selection größer als ... ermöglichen.
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"}>
- Mit Blöcken sind weitere Vergleiche möglich
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':
- Bei der Definition des Models, kann mit der lazy-option angegeben werden, ob eine Spalte gelesen wird.
- 'lazy'-Spalten werden nur bei Bedarf gelesen.
- Siehe http://datamapper.org/articles/spotlight_on_laziness.html
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
- Mit ActiveRecord::SchemaDumper.dump wird ruby-code zum erzeugen der DB-Schemas ausgegeben.
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]
- Film.key liefert alle Schlüsselfelder.
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')
- 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_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"
- Array mit hash.
- Statt ActiveRecord::Base kann jedes Model verwendet werden.
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]
- SQL-Kommandos können mit DB#run gestartet werden.
Testvariante :use_sql_select
DB.fetch( "select title from films where year = 1923" ){|f| p f }
{:title=>"Three Ages"} {:title=>"Our Hospitality"}
- 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.