Plusieurs clés étrangères / associations de colonnes dans ActiveRecord / Rails
-
06-07-2019 - |
Question
J'ai des badges (comme StackOverflow).
Certains d’entre eux peuvent être attachés à des choses infalsifiables (par exemple, un badge pour > X commentaires sur un article sont attachés à l’article). Presque tous viennent à plusieurs niveaux (par exemple & Gt; 20, & Gt; 100, & Gt; 200), et vous ne pouvez avoir qu'un seul niveau par type de badge x (= badgeset_id
). / p>
Pour faciliter l'application de la contrainte d'un niveau par badge, je souhaite que les badgings spécifient leur badge par une clé étrangère à deux colonnes - level
et badge_id
- plutôt que par la clé primaire (<= >), bien que les badges aient 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 le fait (et n'essaie pas d'utiliser un has_many :through
), afin de pouvoir utiliser le undefined local variable or method 'level' for #<User:0x3ab35a8>
?
ETA: cela fonctionne partiellement (c'est-à-dire @ badging.badge), mais semble sale:
belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}'
Notez que les conditions sont entre guillemets simples et non pas doubles, ce qui les rend interprétées au moment de l'exécution plutôt qu'au moment du chargement.
Cependant, lorsque j'essaie d'utiliser ceci avec l'association: via l'association, le message d'erreur 'badges.level = #{badgings.level}'
s'affiche. Et rien d’évident (par exemple, badge_set_id
) ne semble fonctionner ...
ETA 2: Prendre le code EmFi et le nettoyer un peu fonctionne. Il faut ajouter <=> à Badge, 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 que ce soit probablement une meilleure solution - je ne considère pas cela comme une réponse réelle à la question de savoir comment effectuer a) des clés étrangères multi-clés, ou b) des associations de conditions dynamiques qui fonctionnent avec: à travers les associations. Donc, si quelqu'un a une solution pour cela, s'il vous plaît parlez-en.
La solution
On dirait que cela pourrait fonctionner mieux si vous séparez Badge en deux modèles. Voici comment procéder pour obtenir les fonctionnalités souhaitées. J'ai ajouté des champs nommés pour garder le code propre à la réalité.
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