Я здесь, потому что искал некоторые запросы, которые могли бы помочь найти существующие записи с перекрывающимися диапазонами элементов, но ничего не нашел. Допустим, у меня есть модель по имени Cart
. Корзина имеет следующие атрибуты: item_min
и item_max
, где item_max
можно обнулить и будет считаться бесконечным, когда ноль. В моей модели я хочу добавить проверку, чтобы записи с перекрывающимися диапазонами элементов не могли быть сохранены.
Я создал запрос, но он не работает для всех моих тестовых случаев:
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
Однако эта проверка не работает для всех моих тестовых случаев. Не могли бы вы помочь мне улучшить мой запрос, чтобы мои тестовые случаи могли пройти?
Кстати, я использую postgresql
Использование Range#overlaps?
, ActiveRecord::Calculations#pluck
и 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
Как написал Джад в комментарии, производительность такой проверки с массивами будет низкой, если есть миллион записей. Идея SQL-запроса с использованием встроенные диапазоны в PostgreSQL:
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
СУРБД оптимизирует такой запрос. Это будет намного эффективнее, чем работать с гигантским массивом
[]
в этом запросе означает включающие нижнюю и верхнюю границы (по умолчанию верхняя граница исключается)
Использование NULL
означает, что диапазон не ограничен
&&
оператор проверяет перекрытия
SELECT numrange(10, NULL, '[]') && numrange(20, 40, '[]');
-- ?column?
-- ----------
-- t
SELECT numrange(10, 20, '[]') && numrange(20, 40, '[]');
-- ?column?
-- ----------
-- t
в зависимости от количества
Cart
, с которыми вы играете, вы можете отфильтровать их (доpluck
), что-то вроде «где item_min <= new_item_max или item_max >= new_item_min», но приведенный выше код должен быть хорош, пока у вас не будет 1000 тележек или так :)