Несколько внешних ключей/ассоциаций столбцов в ActiveRecord/Rails

У меня есть значки (вроде StackOverflow).

Некоторые из них могут быть прикреплены к значкам (например, значок для > X комментариев к сообщению прикрепляется к сообщению). Почти все они имеют несколько уровней (например, >20, >100, >200), и у вас может быть только один уровень для каждого типа значка x значка (= badgeset_id).

Чтобы упростить применение ограничения «один уровень для каждого значка», я хочу, чтобы значки указывали свой значок с помощью внешнего ключа из двух столбцов — badgeset_id и level, а не с помощью первичного ключа (badge_id), хотя у значков есть стандартный первичный ключ тоже.

В коде:

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

Как я могу указать ассоциацию belongs_to, которая делает это (и не пытается использовать badge_id), чтобы я мог использовать has_many :through?

ETA: это частично работает (например, @badging.badge работает), но кажется грязным:

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

Обратите внимание, что условия заключены в одинарные кавычки, а не в двойные, поэтому они интерпретируются во время выполнения, а не во время загрузки.

Однако при попытке использовать это с ассоциацией :through я получаю сообщение об ошибке undefined local variable or method 'level' for #<User:0x3ab35a8>. И ничего очевидного (например, 'badges.level = #{badgings.level}') не работает...

ETA 2: взять код EmFi и немного почистить его. Это требует добавления badge_set_id к значку, что избыточно, но да ладно.

Код:

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

Хотя это работает - и это, вероятно, лучшее решение - я не считаю это фактическим ответом на вопрос о том, как сделать а) многоключевые внешние ключи или б) ассоциации с динамическими условиями, которые работают с: через ассоциации. Так что, если у кого-то есть решение для этого, пожалуйста, говорите.


person Sai    schedule 27.10.2009    source источник


Ответы (1)


Похоже, будет лучше, если вы разделите Badge на две модели. Вот как я бы разбил его, чтобы добиться желаемой функциональности. Я добавил несколько именованных областей, чтобы код, который действительно делает вещи, оставался чистым.

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
person EmFi    schedule 27.10.2009
comment
Это работает, более или менее. Это не совсем ответ на вопрос, хотя это и есть ответ на проблему, и я считаю это таковым. Я очистил ваш код и поместил его в вопрос. - person Sai; 28.10.2009
comment
Я знаю. То, о чем вы спрашивали, казалось, выходит за рамки того, что легко сделать с помощью Rails. Вы искали плагины? На первый взгляд, compositekeys.rubyforge.org может сделать то, что вы ищете. - person EmFi; 28.10.2009