Rails在使用多态关联时组合和排序ActiveRecord关系

Posted

技术标签:

【中文标题】Rails在使用多态关联时组合和排序ActiveRecord关系【英文标题】:Rails Combining and Sorting ActiveRecord Relations when using polymorphic associations 【发布时间】:2022-01-03 04:50:09 【问题描述】:

我正在开发一个小型集合跟踪器,我觉得 STI 可以真正简化这个问题,但似乎普遍的共识是尽可能避免 STI,所以我已经分解了我的模型。目前,它们都是相同的,但我确实有一些不同的元数据,我可以看到自己附加到它们。

无论如何,根是Platform,其中有许多GamesSystemsPeripherals 等,我试图在可过滤的动态表中的视图上显示所有这些关系,可排序和可搜索。

例如,查询可以是@platform.collectables.search(q).order(:name)

# Schema: platforms[ id, name ]
class Platform < ApplicationRecord
  has_many :games
  has_many :systems
  has_many :peripherals
end

# Schema: games[ id, platform_id, name ]
class Game < ApplicationRecord
  belongs_to :platform
end

# Schema: systems[ id, platform_id, name ]
class System < ApplicationRecord
  belongs_to :platform
end

# Schema: peripherals[ id, platform_id, name ]
class Peripheral < ApplicationRecord
  belongs_to :platform
end

在上面,当我将它们添加到 Collection 时,多态性开始发挥作用:

# Schema: collections[ id, user_id, collectable_type, collectable_id ]
class Collection < ApplicationRecord
  belongs_to :user
  belongs_to :collectable, polymorphic: true
end

现在,当我查看Platform 时,我希望看到它的所有游戏、系统和外围设备,我称之为收藏品。我将如何查询所有这些同时能够作为一个整体进行排序(即:“名称 ASC”)。下面在理论上可行,但这会改变与数组的关系,这会阻止我在数据库级别进行进一步过滤、搜索或重新排序,因此我无法标记另一个 scopeorder

class Platform < ApplicationRecord
...

  def collectables
    games + systems + peripherals
  end
end

我偶然发现了Delegated Types,这听起来像是朝着我正在寻找的方向迈出的一步,但也许我错过了一些东西。

我很想尝试 STI 路线,我认为这些模型差异不大,不同的东西可以存储在 JSONB 列中,因为它主要只是用于填充视图的元数据,而不是真正搜索.基本上是这样的模型,但它似乎很不受欢迎,我觉得我一定错过了什么。

# Schema: collectables[ id, platform_id, type, name, data ]
class Collectable < ApplicationRecord
  belongs_to :platform
end

class Platform < ApplicationRecord
  has_many :collectables

  def games
    collectables.where(type: 'Game')
  end

  def systems
    collectables.where(type: 'System')
  end

  ...
end

【问题讨论】:

您要克服的实际问题是什么?多态性如何解决这个问题?当一个“集合”看起来只是其部分的总和时,为什么它是必要的?就我个人而言,我觉得 STI 或多态性可能只会使情况复杂化,但也许我不太清楚这个问题。如果这些模型共享某些属性(仅通过命名约定),那很好。如果他们共享演示文稿或方法,您可以通过组合模块简化此逻辑。 @engineersmnky 当我需要将收藏品与用户相关联时,多态性开始发挥作用,所以我认为这不是问题的一部分,因为我可以在视图中正确过滤用户集合由多态列。我遇到的问题是,当我想显示与平台相关的所有收藏品时,仍然能够过滤、搜索或排序(动态表)。组合这 3 个对象会将它们变成一个数组,这会阻止我在数据库级别上有效地执行搜索。所以实际上...@platform.collectables.search(q).order("name desc") 如果您不想使用 STI,另一种想法是使用 UNION SQL 短语 我不认为 number_of_players 是游戏的元数据,它应该是一个属性。我什至不知道 Rails 中存在委托类型,但它们似乎确实是一个合适的解决方案 “我没有看到这些模型有太大的不同”——著名的遗言。 【参考方案1】:

这里的一个解决方案是Delegated Type(一个相对较新的Rails 功能),基本上可以概括为通过多态实现多表继承。所以你有一个包含共享属性的基表,但每个类也有自己的表 - 从而避免了 STI 的一些关键问题。

# app/models/concerns/collectable.rb
# This module defines shared behavior for the collectable "subtypes"
module Collectable
  TYPES = %w Game System Peripheral 
  extend ActiveSupport::Concern
  included do
    has_one :base_collectable, as: :collectable
    accepts_nested_attributes_for :base_collectable
  end 
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord 
  # this sets up a polymorhic association
  delegated_type :collectable, types: Collectable::TYPES
end
class Game < ApplicationRecord
  include Collectable
end
class Peripheral < ApplicationRecord
  include Collectable
end
class System < ApplicationRecord
  include Collectable
end

然后您可以通过连接模型设置多对多关联:

class Collection < ApplicationRecord
  belongs_to :user 
  has_many :collection_items
  has_many :base_collectables, through: :collection_items

  has_many :games, 
    through: :base_collectables, 
    source_type: 'Game'
  has_many :peripherals, 
    through: :base_collectables, 
    source_type: 'Peripheral'
  has_many :systems, 
    through: :base_collectables, 
    source_type: 'Systems'
end
class CollectionItem < ApplicationRecord
  belongs_to :collection
  belongs_to :base_collectable
end
# This model contains the base attributes shared by all the collectable types
# rails g model base_collectable name collectable_type collectable_id:bigint
class BaseCollectable < ApplicationRecord 
  # this sets up a polymorhic association
  delegated_type :collectable, types: %w Game System Peripheral 
  has_many :collection_items
  has_many :collections, through: :collection_items
end

这使您可以将其视为同质集合并按base_collectables 表上的列排序。

大但是-关系模型仍然不喜欢作弊者

多态关联是对对象关系阻抗不匹配的一种肮脏欺骗,它使一个列具有主键引用,另一个列存储类名。它不是实际的外键,因为如果不先将记录从数据库中拉出,则无法解决关联。

这意味着您无法设置has_many :collectables, through: :base_collectables 关联。而且您不能急切地加载所有委托类型或按游戏、外围设备或系统表上的列对整个集合进行排序。

它确实适用于特定类型,例如:

has_many :games, 
    through: :base_collectables, 
    source_type: 'Game'

因为表格可以预先知道。

在基于表而不是面向对象的关系数据库中,这只是一个难以破解的难题,其中外键形式的关系指向单个表。

VS STI + JSON

这里的关键问题是,您填充到 JSON 列中的所有数据本质上都是无模式的,并且您受到 JSON 支持的 6 种类型(都不是日期或体面的数字类型)和使用的限制数据可能非常困难。

JSON 的主要特点是简单,它作为一种传输格式工作得很好。作为一种数据存储格式,它并不是那么好。

这是从 EAV 表或将 YAML/JSON 序列化到 varchar 列中的一大进步,但它仍然有很多警告。

其他可能的解决方案

性病。修复了一个多态性问题并引入了更多。 使用三个表的联合填充的materized view 可以被视为表,因此可以简单地附加一个 ActiveRecord 关系。 联合查询。与上面的想法基本相同,但您只需使用原始查询结果或将它们输入到表示无意义表的模型中。 非关系数据库。 MongoDB 等基于文档的数据库可让您通过设计创建灵活的文档和非同质集合,同时提供更好的类型支持。

【讨论】:

这其实和我现在走的路线很相似,新的委托类型感觉是解决问题的准确方法。我更喜欢你的一些命名,而不是我目前的选择(BaseCollectable)。我对正常的 Rails 多态行为并不陌生,所以新的委托类型并不算太糟糕。如果我将name 属性从Collectables 移出并将其放到BaseCollectable 上,它可以解决我最初的搜索、过滤和排序问题。允许我@platform.base_collectables.where(X).order(:name)。我认为这是正确的选择。

以上是关于Rails在使用多态关联时组合和排序ActiveRecord关系的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 4.2 中为多对多关联创建表单

Ruby on Rails: :include 与子模型的多态关联

rails中的多态关联

Rails 关联帮助 - 我是不是使用多态关联?

iOS Core Data:为 Rails 多态关联设置关系的方法

带有 has_many belongs_to 关联的 Rails activerecord 查询