2009-10-27 7 views
7

J'ai des badges (sorta comme StackOverflow).Clés étrangères/associations de plusieurs colonnes dans ActiveRecord/Rails

Certains d'entre eux peuvent être attachés à des objets pouvant être badgés (par exemple, un badge pour> X commentaires sur un article est attaché à la publication). Presque tous viennent dans plusieurs niveaux (par exemple> 20,> 100,> 200), et vous ne pouvez avoir qu'un seul niveau par type de badge x badgeable (= badgeset_id).

Pour le rendre plus facile à appliquer la contrainte d'un niveau par badge, je veux badgings de spécifier leur insigne par une clé étrangère à deux colonnes - badgeset_id et level - plutôt que par clé primaire (badge_id), bien que badges a aussi une clé primaire standard.

Dans le code:

class Badge < ActiveRecord::Base 
    has_many :badgings, :dependent => :destroy 
    # integer: badgeset_id, level 

    validates_uniqueness_of :badgeset_id, :scope => :level 
end 

class Badging < ActiveRecord::Base 
    belongs_to :user 
    # integer: badgset_id, level instead of badge_id 
    #belongs_to :badge # <-- how to specify? 
    belongs_to :badgeable, :polymorphic => true 

    validates_uniqueness_of :badgeset_id, :scope => [:user_id, :badgeable_id] 
    validates_presence_of :badgeset_id, :level, :user_id 

    # instead of this: 
    def badge 
    Badge.first(:conditions => {:badgeset_id => self.badgeset_id, :level => self.level}) 
    end 
end 

class User < ActiveRecord::Base 
    has_many :badgings, :dependent => :destroy do 
    def grant badgeset, level, badgeable = nil 
     b = Badging.first(:conditions => {:user_id => proxy_owner.id, :badgeset_id => badgeset, 
     :badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)}) || 
     Badging.new(:user => proxy_owner, :badgeset_id => badgeset, :badgeable => badgeable) 
     b.level = level 
     b.save 
    end 
    end 
    has_many :badges, :through => :badgings 
    # .... 
end 

Comment puis-je spécifier une association belongs_to qui fait cela (et ne pas essayer d'utiliser un badge_id), afin que je puisse utiliser le has_many :through?

ETA: Cela fonctionne en partie (ie @ badging.badge fonctionne), mais se sent sale:

belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}' 

Notez que les conditions sont en simples entre guillemets, pas le double, ce qui en fait interprété lors de l'exécution plutôt que le temps de chargement. Cependant, lorsque j'essaie de l'utiliser avec l'association: through, j'obtiens l'erreur undefined local variable or method 'level' for #<User:0x3ab35a8>. Et rien d'évident (par exemple 'badges.level = #{badgings.level}') ne semble fonctionner ...

ETA 2: Prendre le code d'EmFi et le nettoyer un peu fonctionne. Il faut ajouter badge_set_id à Badge, ce qui est redondant, mais bon.

Le code:

class Badge < ActiveRecord::Base 
    has_many :badgings 
    belongs_to :badge_set 
    has_friendly_id :name 

    validates_uniqueness_of :badge_set_id, :scope => :level 

    default_scope :order => 'badge_set_id, level DESC' 
    named_scope :with_level, lambda {|level| { :conditions => {:level => level}, :limit => 1 } } 

    def self.by_ids badge_set_id, level 
    first :conditions => {:badge_set_id => badge_set_id, :level => level} 
    end 

    def next_level 
    Badge.first :conditions => {:badge_set_id => badge_set_id, :level => level + 1} 
    end 
end 

class Badging < ActiveRecord::Base 
    belongs_to :user 
    belongs_to :badge 
    belongs_to :badge_set 
    belongs_to :badgeable, :polymorphic => true 

    validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id] 
    validates_presence_of :badge_set_id, :badge_id, :user_id 

    named_scope :with_badge_set, lambda {|badge_set| 
    {:conditions => {:badge_set_id => badge_set} } 
    } 

    def level_up level = nil 
    self.badge = level ? badge_set.badges.with_level(level).first : badge.next_level 
    end 

    def level_up! level = nil 
    level_up level 
    save 
    end 
end 

class User < ActiveRecord::Base 
    has_many :badgings, :dependent => :destroy do 
    def grant! badgeset_id, level, badgeable = nil 
     b = self.with_badge_set(badgeset_id).first || 
     Badging.new(
      :badge_set_id => badgeset_id, 
      :badge => Badge.by_ids(badgeset_id, level), 
      :badgeable => badgeable, 
      :user => proxy_owner 
     ) 
     b.level_up(level) unless b.new_record? 
     b.save 
    end 
    def ungrant! badgeset_id, badgeable = nil 
     Badging.destroy_all({:user_id => proxy_owner.id, :badge_set_id => badgeset_id, 
     :badgeable_id => badgeable.try(:id), :badgeable_type => badgeable.try(:class)}) 
    end 
    end 
    has_many :badges, :through => :badgings 
end 

Bien que cela fonctionne - et il est probablement une meilleure solution - Je ne considère pas une réponse réelle à la question de savoir comment faire a) clés étrangères multi-clés, ou b) les associations de conditions dynamiques qui fonctionnent avec: à travers des associations. Donc, si quelqu'un a une solution pour cela, parlez s'il vous plaît.

Répondre

1

On dirait que cela pourrait mieux fonctionner si vous séparez Badge en deux modèles. Voici comment je décomposer pour obtenir la fonctionnalité que vous voulez. J'ai jeté des portées nommées pour garder le code qui fait les choses propres.

class BadgeSet 
    has_many :badges 
end 

class Badge 
    belongs_to :badge_set 
    validates_uniqueness_of :badge_set_id, :scope => :level 

    named_scope :with_level, labmda {|level 
    { :conditions => {:level => level} } 
    } 

    named_scope :next_levels, labmda {|level 
    { :conditions => ["level > ?", level], :order => :level } 
    } 

    def next_level 
    Badge.next_levels(level).first 
    end 
end 

class Badging < ActiveRecord::Base 
    belongs_to :user 
    belongs_to :badge 
    belongs_to :badge_set 
    belongs_to :badgeable, :polymorphic => true 

    validates_uniqueness_of :badge_set_id, :scope => [:user_id, :badgeable_id] 
    validates_presence_of :badge_set_id, :badge_id, :user_id 

    named_scope :with_badge_set, lambda {|badge_set| 
    {:conditions => {:badge_set_id => badge_set} } 
    } 

    def level_up(level = nil) 
    self.badge = level ? badge_set.badges.with_level(level).first 
     : badge.next_level 
    save 
    end 
end 

class User < ActiveRecord::Base 
    has_many :badgings, :dependent => :destroy do 
    def grant badgeset, level, badgeable = nil 
     b = badgings.with_badgeset(badgeset).first() || 
     badgings.build(
      :badge_set => :badgeset, 
      :badge => badgeset.badges.level(level), 
      :badgeable => badgeable 
     ) 

     b.level_up(level) unless b.new_record? 

     b.save 
    end 
    end 
    has_many :badges, :through => :badgings 
    # .... 
end 
+0

Cela fonctionne, plus ou moins. Ce n'est pas tout à fait une réponse à la question, même si c'est une réponse au problème, et je le crédite en tant que tel. J'ai nettoyé votre code et l'ai mis dans la question. – Sai

+0

Je sais. Ce que vous demandiez semblait aller au-delà de ce qui est facile à faire avec Rails. Avez-vous cherché des plugins? En un coup d'œil, http://compositekeys.rubyforge.org/ semble faire ce que vous cherchez. – EmFi

Questions connexes