Как мы можем создать ограничение таблицы, которое ограничивает количество одинаковых значений в столбце?

В нашей розничной системе мы планируем перенести информацию о корзине из файлов cookie браузера в таблицу. В настоящее время мы реализуем ограничения корзины в приложении C#, но хотели бы перенести эту бизнес-логику в базу данных в виде ограничений, если это возможно.

CREATE TABLE [dbo].[BasketProduct](
    [BasketProductId] [int] IDENTITY(1,1) NOT NULL,
    [BasketId] [int] NOT NULL,
    [ProductId] [int] NOT NULL,
    [Quantity] [int] NOT NULL,
 CONSTRAINT [PK_BasketProduct] PRIMARY KEY CLUSTERED 
([BasketProductId] ASC))

Мы хотим, чтобы каждая корзина содержала не более 10 уникальных элементов, но разрешала любое количество каждого из них, поэтому значение BasketId должно присутствовать в таблице не более 10 раз.

Кажется, что для этого нет очевидных ограничений, поскольку агрегаты, включая оконные функции, не разрешены в вычисляемых столбцах.

Может ли кто-нибудь порекомендовать подходящий метод?


person cloudsafe    schedule 23.12.2019    source источник
comment
Добавьте, что произойдет, если требование с 10 уникальных предметов будет изменено на 20? Или; да 10 уникальных предметов, кроме менеджеров/суперпользователей у них может быть 30? Я бы контролировал это на уровне вставки с помощью флага максимально допустимых элементов.   -  person uberbloke    schedule 23.12.2019
comment
Мы уже пошли на компромисс, отказавшись от 10 столбцов: по одному для каждого продукта. Ограничение было бы достаточно гибким, чтобы мы могли изменить его без нарушения работы системы, но мы довольны отсутствием исключений из правила.   -  person cloudsafe    schedule 23.12.2019
comment
Это похоже на то, что должен обрабатывать прикладной уровень, или вы должны обрабатывать его в SP, которые управляют INSERT/DELETE/UPDATE. CONSTRAINT — это неправильное место, так как они обрабатывают правила для конкретной строки, а не для подмножеств таблицы.   -  person Larnu    schedule 23.12.2019
comment
Я бы не рекомендовал пытаться сделать это только с помощью DRI.   -  person Damien_The_Unbeliever    schedule 23.12.2019
comment
Во-первых, вы должны определить соответствующий естественный ключ, что важно по многим причинам. Столбец идентификации в качестве первичного ключа ничего не сделает для предотвращения дублирования. Вы также говорите, что не более 10 уникальных элементов - термин, который не имеет четкого определения. Если было 12 строк с одинаковыми значениями BasketId и ProductID, разрешено это или нет? Размахивайте руками и говорите, что этого никогда не будет — такие предположения со временем будут опровергнуты.   -  person SMor    schedule 23.12.2019
comment
Рассматривали ли вы возможность использования триггера при вставке?   -  person Sean Brookins    schedule 23.12.2019
comment
@SMИли значение BasketId должно присутствовать в таблице не более 10 раз. Константа находится только на BasketId с максимальным количеством 10 одинаковых значений.   -  person cloudsafe    schedule 23.12.2019
comment
@Damien_The_Unbeliever Это интересно. Я смотрел в этом направлении, но ограничения индексированного представления меня смущали. Я действительно хочу, чтобы это было зафиксировано на уровне ограничений, потому что есть много разработок и приложений. Я не могу доверять всем приложениям, чтобы они имели правильную бизнес-логику. Я не исключаю этого!   -  person cloudsafe    schedule 23.12.2019
comment
Как бы мне не хотелось это рекомендовать.... но как насчет.... тьфу, не могу поверить, что собираюсь это сказать.... как насчет триггера... тьфу. .. Мне нужно пойти вымыть рот сейчас.   -  person TechGnome    schedule 23.12.2019
comment
@SeanBrookins TechGnome Это может сработать, но .. Мне не нравится, я использую только для аудита.   -  person cloudsafe    schedule 23.12.2019
comment
Я не люблю делать много вещей, но если хранимая процедура не будет работать для вас, то единственной другой вещью, которую я вижу, чтобы заставить ее работать, не сильно раздражая остальных, будет триггер. .. и никто не любит триггеры.   -  person Sean Brookins    schedule 23.12.2019


Ответы (2)


Это далеко не идеально, но, как я уже упоминал в комментарии, я бы справился с этим в SP, который выполняет INSERT/UPDATE/DELETE (вряд ли необходимость в DELETE). Итак, для INSERT у вас будет что-то вроде этого:

USE Sandbox;
GO

CREATE TABLE [dbo].[BasketProduct](
    [BasketProductId] [int] IDENTITY(1,1) NOT NULL,
    [BasketId] [int] NOT NULL,
    [ProductId] [int] NOT NULL,
    [Quantity] [int] NOT NULL,
 CONSTRAINT [PK_BasketProduct] PRIMARY KEY CLUSTERED 
([BasketProductId] ASC));
GO

CREATE PROC AddBasketProduct @BasketID int, @ProductID int, @Quantity int AS
BEGIN

    DECLARE @DistinctProducts int;

    SELECT @DistinctProducts = COUNT(DISTINCT ProductID)
    FROM dbo.BasketProduct WITH (UPDLOCK) --As we need to control concurrency issues
    WHERE BasketId = @BasketID
      AND ProductID != @ProductID ;

    IF @DistinctProducts >= 10
        THROW 71245, N'Cannot have more than 10 different products in a single basket.',16; --Choose an error number and state appropraite for your applciation
    ELSE
        INSERT INTO dbo.BasketProduct (BasketId,
                                       ProductId,
                                       Quantity)
        VALUES(@BasketID,@ProductID,@Quantity);
END;

GO
--Make some sample data
INSERT INTO dbo.BasketProduct (BasketId,
                               ProductId,
                               Quantity)
VALUES(1,1,1),
      (1,2,1),
      (1,3,1),
      (1,4,1),
      (1,5,1),
      (1,6,1),
      (1,7,1),
      (1,8,1),
      (1,9,1),
      (1,10,1); --10 products.
GO
--11th product, will fail
EXEC dbo.AddBasketProduct @BasketID = 1,
                          @ProductID = 11,
                          @Quantity = 1;

GO

--Repetition of product 2, will work
EXEC dbo.AddBasketProduct @BasketID = 1,
                          @ProductID = 2,
                          @Quantity = 1;
GO

DROP PROC dbo.AddBasketProduct;
DROP TABLE dbo.BasketProduct;

DB‹>Fiddle

person Larnu    schedule 23.12.2019
comment
Это сработает, если мы внесем некоторые изменения. Нет никакой модели безопасности, чтобы предотвратить доступ разработчиков к таблицам, поэтому обход sprocs (и они будут, я уверен). Это могло бы сработать, если бы я заблокировал его и использовал индексированное представление для хранения счетчиков корзин. С более чем 10 000 пользователей, добавляющих элементы, может быть много проверок количества. - person cloudsafe; 23.12.2019

Пожалуйста, не делайте этого, функции в определениях таблиц вызывают всевозможные проблемы...

но в качестве дополнения к моему комментарию выше, это будет делать то, что вы хотите

Но, пожалуйста, не делай этого

use tempdb;

DROP TABLE IF EXISTS CheckTbl
CREATE TABLE CheckTbl (col1 int);  
GO  
CREATE OR ALTER FUNCTION CheckCount(@tocheck int)  
RETURNS int  
AS   
BEGIN  
    DECLARE @return INTEGER = (select count(*) from CheckTbl where col1 = @tocheck);
    return @return;
END;  
GO  
ALTER TABLE CheckTbl  
ADD CONSTRAINT chkRowCount CHECK (dbo.CheckCount(col1) <= 2 );  
GO  

INSERT INTO CheckTbl VALUES(1) -- allowed
INSERT INTO CheckTbl VALUES(2) -- allowed
INSERT INTO CheckTbl VALUES(1) -- allowed
INSERT INTO CheckTbl VALUES(2) -- allowed
INSERT INTO CheckTbl VALUES(1) -- NOT allowed

См. также Пользовательская функция с проверочным ограничением SQL Server 2008 для решение с использованием индексированного представления, которое можно адаптировать к этой ситуации

person uberbloke    schedule 23.12.2019
comment
Это может быть полезно, если счетчик берется из агрегированного индексированного представления, а не из таблицы. - person cloudsafe; 23.12.2019