2009-10-27 17 views
7

Tengo insignias (como StackOverflow).Llaves foráneas de columna múltiple/asociaciones en ActiveRecord/Rails

Algunos de ellos se pueden adjuntar a elementos identificables (por ejemplo, una insignia para> X comentarios en una publicación se adjunta a la publicación). Casi todos vienen en varios niveles (por ejemplo,> 20,> 100,> 200), y solo puede tener un nivel por tipo de insignia x identificable (= badgeset_id).

Para que sea más fácil hacer cumplir la restricción de un nivel-por-insignia, quiero badgings especificar su insignia por una clave externa de dos columnas - badgeset_id y level - en lugar de la clave principal (badge_id), aunque insignias tiene una clave primaria estándar también.

En código:

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 

cómo puedo especificar una asociación belongs_to que hace eso (y no trata de utilizar un badge_id), de modo que pueda utilizar el has_many :through?

ETA: Esto funciona parcialmente (es decir, @ badging.badge obras), pero se siente sucia:

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

Tenga en cuenta que las condiciones individuales está en cotizaciones, no doble, lo que hace que sea interpretado en tiempo de ejecución en lugar que tiempo de carga

Sin embargo, cuando trato de usar esto con la asociación: a través de, obtengo el error undefined local variable or method 'level' for #<User:0x3ab35a8>. Y nada obvio (por ejemplo, 'badges.level = #{badgings.level}') parece funcionar ...

ETA 2: Tomar el código de EmFi y limpiarlo funciona un poco. Requiere agregar badge_set_id a Badge, que es redundante, pero bueno.

El código:

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 

Aunque esto funciona - y es probablemente una mejor solución - no considero esto una respuesta real a la pregunta de cómo hacer a) las claves externas de múltiples claves, o b) asociaciones de condición dinámica que trabajan con: a través de asociaciones. Entonces, si alguien tiene una solución para eso, por favor hable.

Respuesta

1

Parece que podría funcionar mejor si separa la insignia en dos modelos. Así es como lo dividiría para lograr la funcionalidad que desea. Lancé algunos ámbitos nombrados para mantener el código que realmente limpia las cosas.

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

que funciona, más o menos. No es exactamente una respuesta a la pregunta, aunque es una respuesta al problema, y ​​lo atribuyo como tal. He limpiado su código y lo he puesto en la pregunta. – Sai

+0

Lo sé. Lo que estaba preguntando parecía ir más allá de lo que se hace fácilmente con Rails. ¿Has buscado complementos? De un vistazo, http://compositekeys.rubyforge.org/ parece que podría hacer lo que estás buscando. – EmFi