Les modèles dans Capcode
J'avais dit que la prochaine fois que je parlerais de Capcode, cela serait pour montrer son utilisation avec Cappuccino... Pardonnez-moi, mais il faudra encore attendre.
En effet, je me suis dit1 que vous parler de l'ajout des modèles était aussi une partie intéressante. En effet, bonne nouvelle, j'ai ajouté la possibilité de créer ses modèles avec couch_foo et DataMapper.
Vous avez pu vous en rendre compte avec l'exemple de blog donné dans la seconde partie de mon article sur la création d'un framework avec Rack, l'utilisation de couch_foo est relativement facile. En effet, il suffit de créer les classes (héritant de CouchFoo::Base) mappant le modèle de donné, d'initialiser la connexion à la base de données via CouchFoo::Base.set_database et le tour est joué. Dans l'exemple de blog, j'avais mis en place une simple classe Story (avec trois propriétés : title, body et date) :
require 'rubygems'
require 'couch_foo'
...
CouchFoo::Base.set_database(:host => "http://localhost:5984", :database => "my_blog")
class Story < CouchFoo::Base
property :title, String
property :body, String
property :date, String
end
Par la suite, l'écriture, la lecture la mise à jour ou la suppression de données se font respectivement via les méthodes CouchFoo::Base.create, CouchFoo::Base.find, CouchFoo::Base.update et CouchFoo::Base.delete. Bien entendu, couch_foo supporte les conditions, les associations...
Faisons la même chose avec DataMapper.
La création de la classe Story est sensiblement identique. La différence est que dans le cas de DataMapper, cette classe n'hérite d'aucune classe, mais qu'elle doit inclure le module DataMapper::Resource :
class Story
include DataMapper::Resource
property :id, Integer, :serial => true
property :title, String
property :body, String
property :date, String
end
Autre petite différence que vous aurez notée : la présence de la propriété :id. En effet, avec DataMapper, les tables doivent obligatoirement avoir une clé. Pour cela il faut soit déclarer la propriété de type serial, soit indiquer explicitement que c'est une clé. Dans le premier cas, vous avez deux possibilités :
property :id, Serial
ou
property :id, Integer, :serial => true
Dans le second cas, voici la méthode d'écriture :
property :name, String, :key => true
La connexion à la base se fait quant à elle en utilisant la méthode DataMapper.setup. Lors de cet appel, nous avons le choix entre utiliser une URL de connexion ou un hachage. Ainsi, les deux méthodes suivantes sont identiques :
DataMapper.setup( :default, "mysql://user:password@localhost:3306/my_database")
DataMapper.setup( :default, {
:adapter => 'mysql',
:database => 'my_database',
:username => 'user',
:password => 'password',
:host => 'localhost',
:port => 3306
})
Enfin, l'écriture, la lecture la mise à jour ou la suppression de données se font respectivement via les méthodes DataMapper::Model.new, DataMapper::Model.get!, DataMapper::Resource.update_attributes et DataMapper::Resource.destroy. Là encore, vous pouvez jouer avec les conditions, les associations et autres joyeusetés.
Comme vous avez pu le voir, nous n'avons pas parlé de migration. En fait avec couch_foo, il n'y a nul besoin de faire une quelconque demande pour créer le modèle de données. Cette opération est faite automatiquement lors de l'initialisation de la connexion. Avec DataMapper, il faut faire une demande explicite. Pour cela nous avons le choix entre plusieurs méthodes : DataMapper.auto_migrate! ou DataMapper.auto_upgrade!. Vous l'aurez certainement compris, mais auto_migrate! est destructeur alors qu'auto_upgrade! ne l'est pas.
Sachant tout cela, nous pouvons maintenant ajouter les modèles dans Capcode. Pour cela nous allons créer deux fichiers, l'un pour couch_foo (couchdb.rb) et l'autre pour DataMapper (dm.rb).
# couchdb.rb
require 'rubygems'
require 'couch_foo'
require 'yaml'
require 'logger'
module Capcode
Base = CouchFoo::Base
module Resource
end
class << self
def db_connect( dbfile, logfile )
dbconfig = YAML::load(File.open(dbfile)).keys_to_sym
Base.set_database(dbconfig)
Base.logger = Logger.new(logfile)
end
end
end
# dm.rb
require 'rubygems'
require 'dm-core'
require 'yaml'
require 'logger'
module Capcode
class Base
end
Resource = DataMapper::Resource
class << self
def db_connect( dbfile, logfile ) #:nodoc:
dbconfig = YAML::load(File.open(dbfile)).keys_to_sym
DataMapper.setup(:default, dbconfig)
DataMapper::Logger.new(logfile, :debug)
DataMapper.auto_upgrade!
end
end
end
Sachant que dans un cas, notre modèle doit hériter d'une classe et dans l'autre il doit inclure un module, nous avons ajouté dans les deux fichiers un module et une classe. Ainsi, la création des modèles peut avoir sensiblement la même syntaxe.
Exemple de modèle créé avec couchdb.rb :
require 'capcode/base/couchdb'
class Story < Capcode::Base
include Capcode::Resource
property :title, String
property :body, String
property :date, String
end
Exemple de modèle créé avec dm.rb :
require 'capcode/base/dm'
class Story < Capcode::Base
include Capcode::Resource
property :id, Integer, :serial => true
property :title, String
property :body, String
property :date, String
end
Enfin, afin d'éviter de devoir demander explicitement la création de la base, nous allons modifier la méthode run en y ajoutant le morceau de code suivant :
# Start database
if self.methods.include? "db_connect"
db_connect( conf[:db_config], conf[:log] )
end
Et ActiveRecord ?
ActiveRecord est un peu plus long à mettre en place. En effet, non seulement il faut créer les modèles, mais en plus, il faut mettre en place les migrations. Avec couch_foo et DataMapper les informations relatives au modèle de données sont directement dans les classes d'accesseur. Avec ActiveRecord, ces informations sont à part. Donc soit vous créez le modèle à la main, soit vous devez créer des migrations. Si nous reprenons les exemples donnés ci-dessus, voici l'enchaînement demandé :
1. Création du script de migration :
# migrate/001_create_stories.rb
class CreateStories < ActiveRecord::Migration
def self.up
create_table :stories do |t|
t.string :title
t.string :body
t.string :date
end
end
def self.down
drop_table :stories
end
end
2. Création d'un Rakefile pour la création du schéma de base :
# Rakefile
require 'active_record'
require 'yaml'
task :default => :migrate
desc "Migration de la base via les scripts situés dans migrate. Vous pouvez utiliser VERSION=x"
task :migrate => :environment do
ActiveRecord::Migrator.migrate('migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
end
task :environment do
ActiveRecord::Base.establish_connection(
YAML::load(File.open('database.yml'))
)
ActiveRecord::Base.logger =
Logger.new(File.open('database.log', 'a'))
end
3. Lancement de la migration :
$ rake
(in /Users/greg/temp/ar)
== CreateStories: migrating ==================================================
-- create_table(:stories)
-> 0.0029s
== CreateStories: migrated (0.0037s) =========================================
4. Utilisation :
# test.rb
require 'rubygems'
require 'active_record'
require 'yaml'
dbconfig = YAML::load(File.open('database.yml'))
ActiveRecord::Base.establish_connection(dbconfig)
class Story < ActiveRecord::Base
end
Story.new( :title => "Hello", :body => "Bonjour le monde", :date => Time.now.to_s ).save
Story.find( :all ).each do |s|
puts s.title
puts s.date
puts s.body
end
Pour exécuter cet exemple, nous avons également besoin d'un fichier de configuration YAML :
# database.yml
adapter: sqlite3
database: base.db
J'ai quelque peu exagéré. Il est tout à fait possible de tout mettre dans un même fichier (migration, Rakefile, utilisation). C'est par exemple ce que fait Camping...
Quoi qu'il en soit, la mise en place des modèles avec ActiveRecord demande un effort légèrement plus important. De plus, couch_foo et bien plus proche de DataMapper qu'ActiveRecord dont il se revendique de copier le style. Enfin, je trouve DataMapper bien plus agréable à utiliser. ActiveRecord n'en reste pas moins un excellent ORM, mais qui ne trouvera pas sa place dans Capcode2.
Tout cela nous amène à une nouvelle version de Capcode.
1 Il ne s'est rien dit du tout ! C'est jusqu'il en a besoin.
2 En tout cas pas avec moi...