【问题标题】:Multiple column foreign keys / associations in ActiveRecord/RailsActiveRecord/Rails 中的多列外键/关联
【发布时间】:2009-10-27 20:10:53
【问题描述】:

我有徽章(有点像 StackOverflow)。

其中一些可以附加到可标记的东西上(例如,帖子上 >X cmets 的徽章附加到帖子上)。几乎所有都有多个级别(例如 >20、>100、>200),并且每个可徽章 x 徽章类型只能有一个级别 (= badgeset_id)。

为了更容易实施每徽章一级的约束,我希望徽章通过两列外键 - badgeset_idlevel 指定其徽章 - 而不是通过主键 (@987654324 @),虽然徽章也有一个标准的主键。

在代码中:

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 #&lt;User:0x3ab35a8&gt;。而且没有什么明显的(例如'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

虽然这可行 - 它可能是一个更好的解决方案 - 我不认为这是对如何执行 a) 多键外键或 b) 与 :through 一起使用的动态条件关联的问题的实际答案协会。因此,如果有人对此有解决方案,请说出来。

【问题讨论】:

    标签: mysql ruby-on-rails activerecord associations


    【解决方案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
    

    【讨论】:

    • 那行得通,或多或少。这不是问题的完全答案,尽管它是问题的答案,我相信它是这样的。我已经清理了您的代码并将其放入问题中。
    • 我知道。您所要求的似乎超出了 Rails 可以轻松完成的范围。你找过插件吗?乍一看,compositekeys.rubyforge.org 似乎可以满足您的需求。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多