Asociaciones/claves foráneas de varias columnas en ActiveRecord/Rails
-
06-07-2019 - |
Pregunta
Tengo insignias (algo así como StackOverflow).
Algunos de ellos se pueden adjuntar a elementos identificables (p. ej.se adjunta a la publicación una insignia para >X comentarios en una publicación).Casi todos vienen en múltiples niveles (p. ej.>20, >100, >200), y solo puedes tener un nivel por cada tipo de insignia con distintivo (= badgeset_id
).
Para que sea más fácil aplicar la restricción de un nivel por insignia, quiero que las insignias especifiquen su insignia mediante una clave externa de dos columnas: badgeset_id
y level
- en lugar de por clave principal (badge_id
), aunque las insignias también tienen una clave principal estándar.
En codigo:
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 un belongs_to
asociación que hace eso (y no intenta utilizar un badge_id
), para poder utilizar el has_many :through
?
Hora estimada de llegada:Esto funciona parcialmente (es decir,@badging.badge funciona), pero se siente sucio:
belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}'
Tenga en cuenta que las condiciones están en soltero comillas, no dobles, lo que hace que se interprete en tiempo de ejecución en lugar de en tiempo de carga.
Sin embargo, cuando intento usar esto con la asociación :through, aparece el error undefined local variable or method 'level' for #<User:0x3ab35a8>
.Y nada obvio (por ej. 'badges.level = #{badgings.level}'
) parece funcionar...
ETA 2:Tomar el código de EmFi y limpiarlo un poco funciona.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
Si bien esto funciona, y probablemente sea una mejor solución, no considero que esto sea una respuesta real a la pregunta de cómo hacer a) claves externas de múltiples claves, ob) asociaciones de condiciones dinámicas que funcionan con asociaciones :through.Entonces, si alguien tiene una solución para eso, que lo diga.
Solución
Parece que podría entrenar mejor si separa la Insignia en dos modelos. Así es como lo desglosaría para lograr la funcionalidad que desea. Agregué algunos ámbitos con nombre para mantener el código que realmente hace las cosas limpias.
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