简体   繁体   English

ActiveRecord / Rails中的多列外键/关联

[英]Multiple column foreign keys / associations in ActiveRecord/Rails

I have badges (sorta like StackOverflow). 我有徽章(类似于StackOverflow的徽章)。

Some of them can be attached to badgeable things (eg a badge for >X comments on a post is attached to the post). 其中一些可以附加到带有徽章的事物上(例如,帖子上附加了> X评论的徽章)。 Almost all come in multiple levels (eg >20, >100, >200), and you can only have one level per badgeable x badge type (= badgeset_id ). 几乎所有的标签都具有多个级别(例如,> 20,> 100,> 200),并且每个可标记x标记类型(= badgeset_id )只能具有一个级别。

To make it easier to enforce the one-level-per-badge constraint, I want badgings to specify their badge by a two-column foreign key - badgeset_id and level - rather than by primary key ( badge_id ), though badges does have a standard primary key too. 为了使执行每个徽章一个级别的约束更加容易,我希望徽章使用两列外键( badgeset_idlevel )而不是主键( badge_id )来指定其徽章,尽管徽章确实具有标准主键也是。

In 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

How I can specify a belongs_to association that does that (and doesn't try to use a badge_id ), so that I can use the has_many :through ? 我如何指定一个做到这一点的belongs_to关联(并且不尝试使用badge_id ),以便可以使用has_many :through

ETA: This partially works (ie @badging.badge works), but feels dirty: 预计到达时间:这部分有效(即@ badging.badge有效),但感觉很脏:

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

Note that the conditions is in single quotes, not double, which makes it interpreted at runtime rather than loadtime. 请注意,条件用引号引起来,而不是双引号,这使得它在运行时而不是加载时进行解释。

However, when trying to use this with the :through association, I get the error undefined local variable or method 'level' for #<User:0x3ab35a8> . 但是,当尝试将其与:through关联一起使用时,我得到undefined local variable or method 'level' for #<User:0x3ab35a8>的错误undefined local variable or method 'level' for #<User:0x3ab35a8> And nothing obvious (eg 'badges.level = #{badgings.level}' ) seems to work... 似乎没有任何明显的效果(例如'badges.level = #{badgings.level}' )...

ETA 2: Taking EmFi's code and cleaning it up a bit works. ETA 2:提取EmFi的代码并对其进行一些清理。 It requires adding badge_set_id to Badge, which is redundant, but oh well. 它需要将badge_set_id添加到Badge,这是多余的,但是很好。

The 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

While this works - and it's probably a better solution - I don't consider this an actual answer to the question of how to do a) multi-key foreign keys, or b) dynamic-condition associations that work with :through associations. 尽管这可行-并且可能是更好的解决方案-但我不认为这是对如何执行a)多键外键或b)与:through关联一起使用的动态条件关联问题的实际答案。 So if anyone has a solution for that, please speak up. 因此,如果有人对此有解决方案,请说出来。

Seems like it might workout best if you separate Badge into two models. 如果将“徽章”分为两个模型,似乎最适合锻炼。 Here's how I'd break it down to achieve the functionality you want. 这是我将其分解以实现所需功能的方式。 I threw in some named scopes to keep the code that actually does things clean. 我添加了一些命名作用域,以保持实际执行操作的代码的整洁。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM