验证具有重叠项目范围的现有记录,将 nil 视为无限

Validate existing records with overlap item ranges taking nil as infinite

我来这里是因为我一直在四处寻找一些可以帮助查找具有重叠项目范围的现有记录的查询,但我找不到任何东西。假设我有一个名为 Cart 的模型。 Cart 具有以下属性:item_minitem_max 其中 item_max 可以为 null,当为 nil 时将被视为无限。在我的模型中,我想添加一个验证,这样就不会保存具有重叠项目范围的记录。

我创建了一个查询,但它不适用于我的所有测试用例:

saved cart: item_min: 2, item_max: nil
try to save cart: `item_min: 1, item_max: 1` VALID
try to save cart: `item_min: 1, item_max: 2` VALID
try to save cart: `item_min: 1, item_max: 6` INVALID
try to save cart: `item_min: 4, item_max: 7` INVALID
try to save cart: `item_cart: 4, item_max: nil` INVALID
saved cart: `item_min: 2, item_max: 7`
try to save `cart: item_min: 1, item_max: 1` VALID
try to save cart: `item_min: 8, item_max: 10` VALID
try to save cart: `item_min: 8, item_max: nil` VALID
try to save cart: `item_min: 1, item_max: 2` INVALID
try to save cart: `item_min: 1, item_max: 8` INVALID
try to save cart: `item_min: 1, item_max: 5` INVALID
try to save cart: `item_min: 5, item_max: 10` INVALID
try to save cart: `item_min: 3, item_max: 5` INVALID
try to save cart: `item_min: 1, item_max: nil` INVALID

我创建了以下查询:


  def validate_item_count_range
    if item_count_max.nil?
      overlap_carts = Cart.where(item_count_max: nil)
    else
      overlap_carts = Cart.where(
        "item_count_min >= ? AND item_count_max <= ?", item_count_min, item_count_max,
      ).or(
        Cart.where(
          "item_count_min <= ? AND item_count_max IS NULL", item_count_min,
        ),
      )
    end

    errors.add(:item_count_min, "overlaps with existing carts") if overlap_carts.present?
  end

但是,此验证不适用于我的所有测试用例。你能帮我改进我的查询,让 mi 测试用例可以通过吗?

顺便说一句,我正在使用 postgresql

使用Range#overlaps?, ActiveRecord::Calculations#pluck and Array#any?

无特殊SQL查询

if Cart.pluck(:item_min, :item_max).any? { |min, max| (min..max).overlaps?(item_min..item_max) }
  errors.add(:base, :overlaps_with_existing_carts)
end

无限范围有一个明确的开始值,但有一个 nil 结束值。你可以省略这个 nil

(8..nil) == (8..)
# => true

这样的范围包括从开始值开始的所有值

(8..nil).overlaps?(4..6)
# => false

(8..nil).overlaps?(4..9)
# => true

当然,这种方法适用于通常的范围

(4..6).overlaps?(6..8)
# => true

(4..6).overlaps?(1..3)
# => false

正如 Jad 在评论中所写,如果有数百万条记录,使用数组进行此类验证的性能将会很低。使用 built-in ranges in PostgreSQL 进行 SQL 查询的想法:

if Cart.exists?(["numrange(item_count_min, item_count_max, '[]') && numrange(?, ?, '[]')", item_count_min, item_count_max])
  errors.add(:base, :overlaps_with_existing_carts)
end

RDBMS 将优化此类查询。它会比使用巨型数组

更有效

[]在此查询中表示包含下限和上限(默认情况下上限是独占的)

使用NULL表示范围无界

&& 运算符检查重叠

SELECT numrange(10, NULL, '[]') && numrange(20, 40, '[]');
-- ?column? 
-- ----------
--  t

SELECT numrange(10, 20, '[]') && numrange(20, 40, '[]');
-- ?column? 
-- ----------
--  t