Rails - 模块化的多对多关系

时间:2016-03-23 10:21:53

标签: ruby-on-rails

上下文

在Ruby on Rails应用程序的上下文中,在学校的项目中。

让我们考虑一个基于团队的游戏的背景,有许多角色可供选择。我想在不同的语境中表示两个角色之间的亲密关系,这意味着两个角色是组合在一起还是彼此面对,或者即使一个角色存在于游戏中而另一个角色缺失。

然后我会在我的数据库中看到类似这样的表

  • 字符
  • 同盟 - 关系
  • 敌人关系
  • PlayingSingle-关系

这些<name>-Relation表中的每一个都表示字符之间的多对多关系,并且附加分数代表关系的强度

当然,角色之间的关系可能会发生变化。我们可能会出于任何原因决定一个关系变得无关紧要,或者我们在刚出现之前没有想到的其他关系。

在显示方面,我们希望查找特定关系中的最佳和最差其他字符。

问题

我想出了类似的东西。

class Relation < ActiveRecord::Base    
  scope :best, ->(character_id) {
    Character.find(where(character_left: character_id).order("score desc").limit(5).pluck(:character_right))
  }
end

其中character_leftcharacter_right是关系中要考虑的两个字符,分数是债券的强项。

但是,在获取数据时,我的老师认为最好在Characters模型中使用范围来查找特定关系中的最佳和最差其他字符。这是因为,当他想要显示字符时,正在做HTML代码的队友不会对关系的结构发表任何看法。他告诉我有关使用has_and_belongs_to_many的信息,他向我绘制了一些他希望看起来像Character.best(:relation)的代码来获取数据。

虽然我认为我所做的更好(显然:))。具有将从Relation模型中获取字符的范围,因为它们会出现和消失,从而保持请求关系细节。这使我们无法在每次遇到关系时修改角色模型。 看起来像Relation.best(:hero)的某些东西对我来说似乎更清晰。

你怎么看?围绕这种非常具体的情况有什么好的做法。有没有正确的方法在Ruby on Rails应用程序中应用和使用模块化的多对多关系?

1 个答案:

答案 0 :(得分:1)

您使用score列在正确的轨道上并使用它来订购关系。但是,您需要考虑一个字符可以位于连接模型的任一列中。

class Character
  has_many :relationships_as_left, foreign_key: 'left_id'
  has_many :relationships_as_right, foreign_key: 'right_id'
end

# renamed to not get it mixed up with ActiveRecord::Relation
class Relationship
  belongs_to :left, class_name: 'Character'
  belongs_to :right, class_name: 'Character'
end

您希望确保设置唯一索引和正确的外键:

class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.references :left, index: true, foreign_key: false
      t.references :right, index: true, foreign_key: false
      t.integer :score, index: true
      t.timestamps null: false
    end

    add_foreign_key :relationships, :characters, column: :left_id
    add_foreign_key :relationships, :characters, column: :right_id
    add_index :relationships, [:left_id, :right_id], unique: true
  end
end

查询此表有点棘手,因为可以在relationships.left_idrelationships.right_id中引用字符。

class Relationship < ActiveRecord::Base
  belongs_to :left, class_name: 'Character'
  belongs_to :right, class_name: 'Character'

  def self.by_character(c)
    sql = "relationships.left_id = :id OR relationships.right_id = :id"
    where( sql, id: c.id )
  end

  def self.between(c1, c2)
    where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))
  end

  def other_character(c)
     raise ArgumentError unless c == left || c == right
     c == left ? right : left
  end
end

between方法需要一些解释:

where(left_id: [c1,c2]).merge(where(right_id: [c1,c2]))

这将生成以下查询:

SELECT 
   "relationships".* FROM "relationships" 
WHERE 
   "relationships"."left_id" IN (1, 2) 
AND 
   "relationships"."right_id" IN (1, 2)

你和你的教授都是错的 - Character上的范围不起作用,因为范围是类级别,你想要的是检查实例上的关系。

class Character

  def worst_enemies(limit = 10)
    relations = Relationship.joins(:left, :right)
                            .by_character(self)
                            .order('relationship.score ASC')
                            .limit(limit)
    relations.map do |r|
      r.other_character(self)
    end
  end
end

你可以用子查询更优雅地做到这一点。