2009-10-27 11 views
7

I badge (come StackOverflow).Associazioni/chiavi esterne a colonne multiple in ActiveRecord/Rails

Alcuni di questi possono essere allegati a cose che si possono verificare (ad esempio un badge per> X commenti su un post è allegato al post). Quasi tutti sono disponibili in più livelli (ad es.> 20,> 100,> 200) e puoi avere un solo livello per ogni tipo di badge x indebitabile (= badgeset_id).

Per rendere più facile per far rispettare il vincolo di un livello-per-badge, voglio badgings di specificare il distintivo da una chiave esterna a due colonne - badgeset_id e level - piuttosto che dalla chiave primaria (badge_id), anche se badge ha anche una chiave primaria standard.

in codice:

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 

Come è possibile specificare un'associazione belongs_to che lo fa (e non cerca di utilizzare un badge_id), in modo che posso usare il has_many :through?

ETA: Questo funziona parzialmente (cioè @ badging.badge fabbrica), ma si sente sporco:

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

noti che le condizioni sono in singoli apici e non doppio, il che rende interpretato a runtime piuttosto rispetto al tempo di caricamento.

Tuttavia, quando si tenta di utilizzare questo con l'associazione: through, viene visualizzato l'errore undefined local variable or method 'level' for #<User:0x3ab35a8>. E nulla di ovvio (ad esempio 'badges.level = #{badgings.level}') sembra funzionare ...

ETA 2: Prendere il codice di EmFi e ripulirlo funziona un po '. È necessario aggiungere badge_set_id a Badge, che è ridondante, ma vabbè.

Il codice:

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 

Anche se questo funziona - ed è probabilmente una soluzione migliore - io non considero questa una risposta reale alla domanda di come fare a) multi-chiave chiavi esterne, o b) associazioni di condizioni dinamiche che funzionano con: attraverso associazioni. Quindi se qualcuno ha una soluzione per questo, per favore parla.

risposta

1

Sembra che potrebbe funzionare meglio se si separa Badge in due modelli. Ecco come scomporlo per ottenere la funzionalità che desideri. Ho gettato alcuni ambiti con nome per mantenere il codice che effettivamente fa le cose pulite.

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

Che funziona, più o meno. Non è una risposta alla domanda, anche se è una risposta al problema e lo sto accreditando come tale. Ho ripulito il codice e inserito la domanda. – Sai

+0

Lo so. Quello che stavi chiedendo sembrava andare oltre ciò che è facile fare con Rails. Hai cercato plugin? A prima vista, http://compositekeys.rubyforge.org/ sembra che potrebbe fare ciò che stai cercando. – EmFi

Problemi correlati