Follow me
RSS feed
My sources
My Viadeo

Les modèles dans Capcode

Greg | 10 Mar 2009

databaseJ'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...

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.