Rails在使用多态关联时组合和排序ActiveRecord关系
Posted
技术标签:
【中文标题】Rails在使用多态关联时组合和排序ActiveRecord关系【英文标题】:Rails Combining and Sorting ActiveRecord Relations when using polymorphic associations 【发布时间】:2022-01-03 04:50:09 【问题描述】:我正在开发一个小型集合跟踪器,我觉得 STI 可以真正简化这个问题,但似乎普遍的共识是尽可能避免 STI,所以我已经分解了我的模型。目前,它们都是相同的,但我确实有一些不同的元数据,我可以看到自己附加到它们。
无论如何,根是Platform
,其中有许多Games
、Systems
、Peripherals
等,我试图在可过滤的动态表中的视图上显示所有这些关系,可排序和可搜索。
例如,查询可以是@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”)。下面在理论上可行,但这会改变与数组的关系,这会阻止我在数据库级别进行进一步过滤、搜索或重新排序,因此我无法标记另一个 scope
或 order
。
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关系的主要内容,如果未能解决你的问题,请参考以下文章
Ruby on Rails: :include 与子模型的多态关联