Pourquoi ActiveRecord n’utilise pas le nom des associations pour faire ses jointures SQL ?

C'est une vrai question à laquelle je n'ai pas de réponse. Pourtant ça me paraît tellement naturel que je me demande s'il y a une raison technique à ça.

Ci dessous quelques exemples pour illustrer:

class Contract < ActiveRecord::Base
  belongs_to :sender, :class_name => "Company"
  belongs_to :receiver, :class_name => "Company"
end

On commence à imaginer le problème qui va se poser, deux relations qui pointent vers une même classe. Voyons un peu le SQL généré en faisant des jointures.

Contract.all(:joins => [:sender, :receiver])

=>

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` ON `companies`.id = `contracts`.sender_id INNER JOIN `companies` receivers_contracts ON `receivers_contracts`.id = `contracts`.receiver_id

Pour sender AR va donc conserver le nom de la table originale. Pour receiver comme le nom de table companies est déjà utilisé il va devoir en utiliser un autre (receivers_contracts).

D'où ma question: pourquoi ne pas utiliser le nom de l'association (et de façon systématique) ?

Je ne vois pas d'obstacle à cela dans la mesure où le nom de l'association risque fort d'être unique. Le seul problème que je vois est une jointure du même nom dans une classe déjà jointe. Par exemple si Company possédait une jointure nommée sender qu'on voulait utiliser dans le même temps.

Mais dans ce cas précis on pourrait utiliser des alias du style receivers_contracts partout pour générer un:

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` senders_contracts ON `senders_contracts`.id = `contracts`.sender_id INNER JOIN `companies` receivers_contracts ON `receivers_contracts`.id = `contracts`.receiver_id

Actuellement on ne peut pas faire de:

Contract.all(:joins => [:sender, :receiver], :conditions => { :sender => { :name => "foo" } })

Pire, quand on ne connaît pas le comportement par défaut d'AR il faut être devin pour savoir ce que fait:

Contract.all(:joins => [:sender, :receiver], :conditions => { :companies => { :name => "foo" } })

Est ce que je suis entrain de filtrer sur l'expéditeur ou le destinataire ?

Si on veut faire notre recherche en utilisant AR il faudrait donc faire:

Contract.all(:joins => [:sender, :receiver], :conditions => { :companies => { :name => "foo"}, :receivers_bills => { :name => "bar" } })

On ne peut pas dire que ce soit très élégant ni intuitif.

Pourtant ce SQL fonctionne:

SELECT `contracts`.* FROM `contracts` INNER JOIN `companies` sender ON `sender`.id = `contracts`.sender_id INNER JOIN companies receiver ON receiver.id = contracts.receiver_id WHERE (`sender`.`name` = 'foo' AND `receiver`.`name` = 'bar');

Si quelqu'un a une explication logique à ça je suis preneur.

DM-sweatshop, le petit truc en plus par rapport à Factory Girl

En jouant avec Oupsnow, j'ai découvert une astuce sur DM-sweatshop qui est vraiment génial. Aucun exemple n'indique ça et pourtant, c'est selon moi révolutionnaire et même un atout face à Factory Girl.

dm-sweatshop

Pour ceux qui ne le savent pas dm-sweatshop est un gem de génération de donnée, inspiré de Factory Girl.

Factory Girl a été conçu pour éviter les fixtures dans Rails. Il a aussi l'avantage de permettre une meilleure vue des données présentes dans sa base de donnée durant son test. Je conseille tout à fait son utilisation et migre progressivement tous mes tests à l'utilisation de ce genre d'outil.

dm-sweatshop est le Factory Girl dédié à DataMapper, même si Factory Girl peux tout à fait être utilisable avec DataMapper.

Son petit plus

dm-sweatshop contrairement à Factory gril, génére un Hash d'attribut, qu'il surcharge avec les données fournis et génére un objet à partir de ce hash. Ceci entraine par exemple, l'impossibilité d'utiliser les méthodes d'association, il faut directement associer les identifiants.

Mais là où est son petit plus est que justement à cause de cette génération d'un Hash, il y a un Proc qui est généré et rien n'empêche de mettre du code ruby pour générer d'autre objets ou récupérer des objets déjà existant. On peux ainsi écrire l'exemple suivant :

Member.fixture {
 user = User.first(:login.not => 'admin') || User.gen!
 project = Project.first || Project.gen!
 not_project_id = []
 while project.has_member?(user)
   not_project_id << project.id
   project = Project.first(:id.not => not_project_id) || Project.gen!
 end
 { :user_id => user.id,
 :project_id => project.id,
 :function_id => (Function.first ? Function.first.id : Function.gen.id),}
}

Dans cette exemple, je crée un objet Membre qui est la liaison entre un Projet et un User. Mais au préalable, j'ai récupéré et créé des éléments Projet et User si rien n'existait.

Grâce à dm-sweatshop, c'est enfin fini, les problèmes de multiples liaisons qui avec Factory Girl ne peuvent être contourné (d'après mon expérience en tout cas) ce qui entrainait la création de méthode complète pour créer tout ça. Désormais tout est au même endroit.

Je ne saurais donc vous encourager à utiliser dm-sweatshop sur tout projet DataMapper à la place de Factory Girl.


Article original écrit par Cyril Mougel et publié sur Shiny happy people coding | lien direct vers cet article | Si vous lisez cet article ailleurs que sur Shiny happy people coding, c'est qu'il a été reproduit illégalement et sans autorisation.

ROSXAuth is dead !

rubyDans mon article d’hier soir, je vous annonçais fièrement la mise à disposition des sources de GemMenu et du module ROSXAuth. Ce dernier a été développé dans l’unique but de permettre d’exécuter une commande avec des privilèges d’administrateur sous Mac. Comme je l’avais également signalé, ce développement était quelque peu bancal qui m’obligeait à passer par un sudo ! De plus, l’utilisation de ROSXAuth engendrait une dépendance dans GemMenu ce qui ne me plaisait pas trop. Et bien le problème est résolu d’une façon qui m’a valu de me répéter en boucle le fameux dicton « Pourquoi faire simple quand on peut faire compliquer ? »

Voici la solution que j’ai adoptée :

require 'osx/cocoa'
 
script = OSX::NSAppleScript.alloc.initWithSource(
  "do shell script \"/usr/bin/gem update -y\" with administrator privileges"
)
errorInfo = OSX::OCObject.new
data = script.executeAndReturnError(errorInfo)
if data.nil?
  OSX::NSLog(
    "UPDATE ERROR: #{errorInfo.objectForKey(OSX::NSAppleScriptErrorMessage)}"
  )
else
  puts data.stringValue()
end

Eh oui ! Il suffit de passer par AppleScript. C’est tellement simple que je suis tombé à côté !

Je viens de pousser dans GitHub la version corrigée de GemMenu.

GemMenu (et non MenuGems)

devVoilà, comme je l’avais promis il y a quelques jours, je viens de pousser GemMenu sous GitHub. Ceux qui suivent (merci !) auront remarqué que j’ai poussé en même temps ROSXAuth. J’ai en effet décidé de sortir une petite partie du projet pour en faire un module à part entière. Comme son nom le laisse penser, ROSXAuth est une petite classe permettant, avec Ruby, de lancer un script avec les droits de l’administrateur.

Je n’ai pas pris le temps de packager proprement les deux projets. ROSXAuth devrait prendre sa place sur RubyForge, ce qui permettra de le récupérer comme un gem « classique ». Pour le moment, vous pouvez passer pas GitHub. Notez bien que ce module est nécessaire pour utiliser GemMenu. Si vous ne l’installez pas, le menu se lancera sans problème, mais vous ne pourrez pas faire les mises à jour.

En ce qui concerne GemMenu, il faudra récupérer les sources et compiler le projet vous même.

Ceux qui vont passer le pas devraient se rendre rapidement compte des limitations de l’outil. Voici donc une petite liste de courses pour les jours à venir :

  • Ajouter dans la liste des gems à mettre à jour une entrée permettant de faire un update complet. Ainsi quand on clic sur un package donné, seul ce dernier est mis à jour.
  • Améliorer l’activation et la désactivation des entrées du menu en fonction des actions en cours. Par exemple, ne pas permettre de quitter l’application pendant une mise à jour.
  • Mettre en place des animations au niveau de l’icône du menu indiquant qu’une action est en cours (recherche de mise à jour et mise à jour en cours)
  • Permettre la mise à jour sans authentification.
  • Installer automatiquement ROSXAuth s’il n’est pas présent.
  • Internationalisation de l’interface.
  • A plus long terme, permettre de gérer plusieurs installations (cela peut être intéressant si vous avez, par exemple, Ruby et jRuby installés sur votre machine)

Autre amélioration à prévoir : il n’y a aucun retour sur l’update. En fait, tout est loggué dans la console.
GemMenu-logs
Je pense qu’il serait préférable de permettre d’afficher ce log. A voir !

Je suis également très énervé contre le panneau de préférences. Il est, à mon goût, très moche et mal organisé. Malheureusement, j’ai l’impression d’avoir touché mes limites de designeur et je serais très heureux d’avoir un peu d’aide pour l’améliorer.
GemMenu-prefs

Si vous regarder le code, vous remarquerez très certainement que je gère l’authentification pour l’exécution de la mise à jour d’une façon un peu bancale. ROSXAuth est très simple à utiliser. Il suffit de créer une instance de la classe et d’utiliser ROSXAuth.exec pour exécuter le processus avec les droits root :

require 'rubygems'
require 'rosxauth'
 
h = ROSXAuth.new()
 
# execution de `/bin/ls -l /Users/greg'
f = h.exec( "/bin/ls", ["-l", "/Users/greg"] )
if f.nil?
  puts "Aie !"
else
  IO.for_fd( f ).each do |g|
    puts g
  end
end

Tout cela est fait en utilisant le service de gestion des autorisations de MacOSX. C’est la première fois que je jouais avec et j’ai dû passer à côté de certaines subtilités, car si vous vous amusez à exécuter la commande gem via le module, vous n’aurez le droit qu’a une insulte en guise de résultat :

ERROR:  While executing gem ... (SecurityError)
    Insecure operation - exist?

En gros il y a une légère discordance entre le niveau de sécurité imposé par rubygems et celui qui est positionné via ROSXAuth. Idéalement il faudrait que l’on puisse positionner les autorisations de façon fine lors de l’instanciation. Je vais y travailler. En attendant, j’ai trouvé une solution, mais elle est très moche :

autz = ROSXAuth.new()
...
output = autz.exec( 
  "/usr/bin/sudo", 
  [@gemExecutable.stringValue().to_s, "update", "-y" ] 
)

Dernier petit détail, j’ai ajouté la notification via Growl. Si vous voulez en profiter, il faudra installer growlnotifier.

Anyway, c’est sur GitHub, donc vous pouvez jouer avec…

Découverte de Python

MenuGems

rubyJe dois vous avouer une chose, je suis un grand flemmard. La preuve en est, mon dernier petit développement. En fait, je me suis rendu compte que je passe très peu de temps avec un shell ouvert sur mon Mac. En effet, développant avec TextMate, NetBeans ou Xcode, je n’ai que très rarement besoin d’ouvrir un Terminal. Pour tout vous dire, je me suis même rendu compte que le seul moment où je le fais, c’est pour mettre à jour mes Gems Ruby. Et bien ce temps est maintenant révolu grâce à GemMenu.

MenuGem

Ce menu, qui s’insère dans la barre système, vous permet de voir si vous avez des gems à mettre à jour. Cette recherche se fait bien entendu automatiquement à intervalle régulier. Et par un simple clic, vous pouvez lancer la mise à jour.

Et voilà comment ne plus jamais avoir à ouvrir le terminal ;)

Je pousserai les sources sur GitHub dans quelques jours, juste pour moi le temps de faire quelque petites corrections et améliorations. Le développement étant fait avec Ruby/Cocoa, vous ne devriez pas avoir de mal à reprendre le code à votre compte pour améliorer l’outil.

Sortie de Ruby 1.9.2 preview 1

Ruby 1.9.2 preview 1 vient de sortir.

Il s'agit d'une pré-version pour la série 1.9.2, en somme un snapshot de la branche de développement actuelle. À ce titre, il existe encore des bugs qui rendent cette version instable. N'hésitez pas à participer à leur résolution. Au programme :

  • L'aspect objet de l'API Socket a été renforcé.
  • Le module Time a été réécrit et amélioré : les valeurs minimale et maximale ont disparues, et le problème de l'an 2038 pour les dates (Unix Millennium bug) n'en est plus un.
  • Ajout d'une nouvelle classe Random pour la génération de séquences aléatoires de nombres.
  • Pour les utilisateurs de merb : Method#parameters fait son apparition.

Voyez le fichier NEWS et le ChangeLog pour plus de détails.

Télécharger

Sortie de Ruby 1.9.1-p243

La version 1.9.1-p243 de Ruby est maintenant disponible.

Il s'agit d'un ensemble de patchs pour la série 1.9.1, avec plusieurs résolutions de bugs. Voyez le ChangeLog pour les détails.

Télécharger

XML::DSL

class XML
  class TagError < ArgumentError
  end
 
  class DSL
    def initialize( helper, &block )
      @__x_d_level = 0
      @__x_d_helper = helper
      @__x_d_helper.instance_variables.each do |ivar|
        self.instance_variable_set(
          ivar, @__x_d_helper.instance_variable_get(ivar)
        )
      end
      @__x_d_builder = ""
      instance_eval(&block) if block
    end
 
    def __
      " "*@__x_d_level
    end
 
    def _(x)
      @__x_d_builder << __ << x << "\n"
    end
 
    def tag!(sym, *args, &block)
      tag = {
        :bra => "<",
        :ket => " />",
        :name => sym.id2name,
        :close => block_given?(),
        :attrs => "",
        :value => ""
      }
 
      args.each do |a|
        if a.class == Hash
          a.each do |k, v|
            tag[:attrs] << " #{k.to_s}='#{v}'"
          end
        elsif a.class == String
          tag[:close] = true
          tag[:value] << a << "\n"
        end
      end
 
      if tag[:name].match( /\?$/ )
        tag[:name].gsub!( /\?$/, "" )
        tag[:bra] = "<?"
        tag[:ket] = "?>"
 
        if tag[:close] == true
          raise XML::TagError, "Malformated traitment tag!"
        end
      end
 
      @__x_d_builder << __ << tag[:bra] << "#{tag[:name]}#{tag[:attrs]}"
      if tag[:close]
        @__x_d_builder << ">\n"
      else
        @__x_d_builder << tag[:ket] << "\n"
      end
 
      @__x_d_level += 2
 
      @__x_d_builder << __ << tag[:value] if tag[:value].size > 0
      instance_eval(&block) if block
 
      @__x_d_level -= 2
 
      if tag[:close]
        @__x_d_builder << __ << "</#{tag[:name]}>\n"
      end
    end
 
    def cdata( x = "", &block )
      @__x_d_builder << __ << "<![CDATA["
      if x.match( /\n/ ) or block
        @__x_d_level += 2
        @__x_d_builder << "\n" << __ << x << "\n" if x.size > 0
        instance_eval(&block) if block
        @__x_d_level -= 2
        @__x_d_builder << __ 
      else
        @__x_d_builder << x if x.size > 0
      end
      @__x_d_builder << "]]>\n"
    end
 
    def to_s
      @__x_d_builder
    end
 
    def method_missing(sym, *args, &block)
      if @__x_d_helper.respond_to?(sym, true)
        @__x_d_helper.send(sym, *args, &block)
      else
        tag!(sym, *args, &block)
      end
    end
  end
end

Servir une application Cappuccino avec Capcode

projetsVoici un premier screencast sur l’utilisation de Capcode avec Cappuccino. Dans cette vidéo vous verrez comment servir des pages statiques avec Capcode et surtout comment prendre en compte de façon simple la « compilation»  d’une application Cappuccino.

Pour ceux qui se désolent de la qualité de la vidéo sur YouTube, voici le lien vers l’original : http://gregoire.lejeune.free.fr/Capcode-cast1.mov