【问题标题】:Is it possible to use a custom equality operator with Ruby's Set?是否可以在 Ruby 的 Set 中使用自定义相等运算符?
【发布时间】:2019-07-18 02:31:38
【问题描述】:

我需要区分两个父母之间的子对象集合。每个大约有 30,000 个对象,并且有大约十几个不同的属性。 Ruby 的 Set 类提供了一种快速的方法来从另一个集合中减去一个集合,并获得差值。我一直在用 JSON 数据做这个,整个过程只用了几秒钟。

现在我使用 ActiveRecord 来获取数据集。当然,一旦孩子从数据库中解组出来,它们就会包括属性:id:created_at:updated_at。不幸的是,这会自动破坏 diff 中的比较,因为这些字段总是不同的,并导致比较失败。

在这组属性中,我真的只关心:label:data。也就是我想比较两个集合之间标签相同的对象,看看它们的数据是否不同。

我可以在我的班级中添加一个自定义等价运算符:

def ==(other)
    self.label == other.label && self.data == other.data
end

这适用于单个对象的比较。如果(仅)它们的标签和数据匹配,则它们被认为是相等的。但是,为了确定等效性,此操作似乎并未使用此覆盖:

@diff = (@left.to_set - @right.to_set)

我希望 Set 会使用对象的类的重写 == 运算符,但情况似乎并非如此。我的差异只是一侧或另一侧,取决于差异的顺序。有没有办法做到这一点? (我也已经尝试过覆盖.eql?。)


由于评论太长,这里是这个想法的 SQL 实现。

WITH 
    t1 AS (SELECT * FROM tunings WHERE calibration_id = 7960),
    t2 AS (SELECT * FROM tunings WHERE calibration_id = 7965)
SELECT t1.label, t1."data", t2."data" FROM t1 FULL OUTER JOIN t2 ON t1.label = t2.label
WHERE t1."data" != t2."data" OR t1."data" IS NULL OR t2."data" IS NULL

我什至还没有提出的另一个速度问题是,当我在视图中显示差异时,我必须从相应的集合中查找“正确”值,而这又需要 10 秒。这一切都是一步完成的。

由于 CTE,我猜我无法将其放入 ActiveRecord 语义中,我只需要传递带有种子值的原始 SQL,但我希望被证明是错误的。

另外,我在学术上仍然对原始问题感兴趣。

【问题讨论】:

  • 数据库中的数据是如何存储的? DBM 具有各种出色的功能来执行此类操作,并且它们的运行速度比检索数据并在代码中执行要快得多。看起来它的存储方式并没有使 DBM 可以轻松地做到这一点。我建议寻找一种更好的方式来存储数据。
  • 好吧,废话。我花了一整天的时间来研究这个,我什至没有想到这一点。在通常的 Rails 实践中,父母的所有孩子都在一个表中,由 parent_id 字段标识。我刚刚尝试了 FULL OUTER JOIN,并且 1)这与“设置”方法之间存在差异,因此需要更多的工作,2)但只需要 1.5 秒,以及 3)我将不得不弄清楚如何让 AR 做这个查询。一个积极的方面是,通过这样做,我意识到我还没有在我的 label 或 parent_id 字段上创建索引,并且这样做大大加快了这两种方法的速度。
  • David,提醒您在回复评论时应包含目标用户名 (@theTinMan),以便 SO 将评论通知他们。在这里 TinMan 会收到通知,但这只是因为这是您之前的唯一评论。
  • @tadman,虽然这行得通,但我可以很容易地想象在繁忙的生产服务器中的情况,由于来回移动数据,即使转储 JSON 并将其作为字符串进行比较也可能代价高昂。如果 DBM 将数据视为字符串并进行比较,那么在代码需要知道究竟发生了什么变化之前,它会快得多,然后它会再次减慢速度,并且比存储更多数据的成本还要高细粒度地。我与之合作的最后一个团队也在做同样的事情,存储序列化的对象,然后来回移动它们以进行比较。我解释了为什么不,但他们不在乎。
  • 只是解释 Ruby 在内部做了什么。

标签: ruby-on-rails ruby set


【解决方案1】:

根据Ruby Set class: equality of sets,需要同时覆盖Object#eql?Object#hash

【讨论】:

  • 这很有效,并且使直接比较方法只需 2.5 秒。 (这比我能想到的任何算法方法都要好一个数量级。)看起来我不需要也覆盖==。我去掉了那个覆盖,时间保持不变。
  • 如果有人想比较基于集合的方法与纯 SQL 的时间:我尝试使用 Arel 进行原始查询,直到我终于注意到它的“外连接”实际上只是一个“左外连接。”所以我终于像上面那样生成了 SQL(在正确的地方使用了替换的参数),现在整个东西在 1.6 秒内渲染。
【解决方案2】:

以下是在一般 Ruby 中如何做到这一点,而无需重新定义类的身份。

first = [{ id: 1, label: "foo", data: "foo"},
         { id: 2, label: "bar", data: "bar"},
         { id: 3, label: "baz", data: "baz"}]
second = [{ id: 1, label: "foo", data: "foo"},
          { id: 2, label: "baz", data: "baz"},
          { id: 3, label: "quux", data: "quux"}]

first_groups = first.group_by { |e| e.values_at(:label, :data) }
second_groups = second.group_by { |e| e.values_at(:label, :data) }

first_minus_second_keys = first_groups.keys.to_set - second_groups.keys.to_set
first_minus_second = first_minus_second_keys.flat_map { |k| first_groups[k] }

(这是用于哈希列表;对于 AR 类,您可以将 e.values(:label, :data) 替换为 [e.label, e.data]

也就是说,我同意铁皮人的观点:在数据库级别执行此操作会更好。

【讨论】:

  • 纯代码解决方案对我来说仍然很有吸引力,但这个示例突出了我在尝试编写代码时遇到的许多问题,但找不到原始问题的答案。 .values_at() 不存在 ActiveRecord 对象,所以我必须使用 e.attributes.values_at() 然后我不知道用什么来代替 :label 使其工作。在那一点上调试我在块中得到的东西是很困难的。
  • "(这是用于哈希列表;对于 AR 类,您可以将 e.values(:label, :data) 替换为 [e.label, e.data])"(除了我在 values_at 中输入了 values)。这会调用适当的方法,而不是深入底层。你也可以[:label, :data].map { |m| e.send(m) },应该是等价的。
  • 不,我发现错字了,但你的意思很清楚。我尝试了e.labele["label"](因为那时它是一个哈希),但是这两个都导致[nil, nil] 作为一个键,这就是我放弃的地方。对数据调用另一种方法来转换它只会增加基于集合的方法的性能损失。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-04
  • 1970-01-01
  • 2016-10-04
  • 2018-07-15
  • 2014-11-04
  • 1970-01-01
  • 2011-09-01
相关资源
最近更新 更多