Chiavi / associazioni esterne a più colonne in ActiveRecord / Rails
-
06-07-2019 - |
Domanda
Ho dei badge (una specie di StackOverflow).
Alcuni di essi possono essere associati a cose dannose (ad esempio un badge per & GT; X commenti su un post sono allegati al post). Quasi tutti sono disponibili in più livelli (ad es. & Gt; 20, & Gt; 100, & Gt; 200) e puoi avere solo un livello per ogni tipo di badge x badging (= badgeset_id
).
Per semplificare l'applicazione del vincolo di un livello per badge, desidero che i badgings specifichino il loro badge mediante una chiave esterna a due colonne - level
e badge_id
- anziché mediante chiave primaria (<= >), sebbene i badge abbiano anche una chiave primaria standard.
Nel 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 posso specificare un'associazione belongs_to
che lo fa (e non prova a usare un has_many :through
), in modo che io possa usare undefined local variable or method 'level' for #<User:0x3ab35a8>
?
ETA: funziona parzialmente (vale a dire @ badging.badge funziona), ma sembra sporco:
belongs_to :badge, :foreign_key => :badgeset_id, :primary_key => :badgeset_id, :conditions => 'badges.level = #{level}'
Nota che le condizioni sono tra virgolette , non doppie, il che la rende interpretata in fase di esecuzione anziché in fase di caricamento.
Tuttavia, quando provo ad usarlo con l'associazione: through, ottengo l'errore 'badges.level = #{badgings.level}'
. E nulla di ovvio (ad esempio badge_set_id
) sembra funzionare ...
ETA 2: prendere il codice EmFi e ripulirlo un po 'funziona. Richiede l'aggiunta di <=> 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
Mentre funziona - ed è probabilmente una soluzione migliore - non considero questa una risposta effettiva alla domanda su come fare a) chiavi esterne multi-chiave o b) associazioni a condizioni dinamiche che funzionano con: attraverso associazioni. Quindi, se qualcuno ha una soluzione per questo, per favore parla.
Soluzione
Sembra che potrebbe allenarsi meglio se separi Badge in due modelli. Ecco come lo analizzerei per ottenere la funzionalità desiderata. Ho inserito alcuni ambiti denominati per mantenere pulito il codice che effettivamente pulisce le cose.
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