Sql server 确保为一个集合定义了且仅定义了一个默认值

Sql server 确保为一个集合定义了且仅定义了一个默认值,sql-server,Sql Server,我有一个客户表,它与地址表有一对多的关系。 我希望约束数据库,以便具有地址的客户始终具有且仅具有一个默认地址 我可以很容易地添加一个约束,以确保每个客户只有一个默认地址。然而,我正在努力研究如何应用一个约束,以确保地址始终标记为默认地址 总结如下: 客户不需要有任何地址。 如果客户有地址,则必须有默认地址。 每个客户只能有一个默认地址。 下面是一个问题示例和一些“单元”测试。我正在使用链接表连接客户和地址 CREATE TABLE Customer ( Id INT PRIMARY KE

我有一个客户表,它与地址表有一对多的关系。 我希望约束数据库,以便具有地址的客户始终具有且仅具有一个默认地址

我可以很容易地添加一个约束,以确保每个客户只有一个默认地址。然而,我正在努力研究如何应用一个约束,以确保地址始终标记为默认地址

总结如下:

客户不需要有任何地址。 如果客户有地址,则必须有默认地址。 每个客户只能有一个默认地址。 下面是一个问题示例和一些“单元”测试。我正在使用链接表连接客户和地址

CREATE TABLE Customer
(
    Id INT PRIMARY KEY,
    Name VARCHAR(100) NOT NULL
)

CREATE TABLE [Address]
(
    Id INT PRIMARY KEY,
    Address VARCHAR(500) NOT NULL
)

CREATE TABLE CustAddress
(
    CustomerId INT,
    AddressId INT,
    [Default] BIT NOT NULL,
    FOREIGN KEY (CustomerId) REFERENCES Customer(Id),
    FOREIGN KEY (AddressId) REFERENCES [Address](Id)
)

INSERT INTO Customer VALUES (1, 'Mr Greedy')

INSERT INTO [Address] VALUES (1, 'Roly-Poly House, Fatland')
INSERT INTO [Address] VALUES (2, 'Giant Cottage, A Cave')

-- Should succeed
INSERT INTO CustAddress VALUES (1, 1, 1)
INSERT INTO CustAddress VALUES (1, 2, 0)

DELETE FROM CustAddress

-- Should fail as no default address set
INSERT INTO CustAddress VALUES (1, 1, 0)

DELETE FROM CustAddress

-- Should fail as we end up with no defualt address set
INSERT INTO CustAddress VALUES (1, 1, 1)
INSERT INTO CustAddress VALUES (1, 2, 0)
UPDATE CustAddress SET [Default] = 0 WHERE CustomerId = 1 AND AddressId = 1

DELETE FROM CustAddress

-- Should fail as we end up with no defualt address set
INSERT INTO CustAddress VALUES (1, 1, 1)
INSERT INTO CustAddress VALUES (1, 2, 0)
DELETE FROM CustAddress WHERE CustomerId = 1 AND AddressId = 1

dash建议检查约束

类似于从customerid所在的表中选择count*= @customerid和默认值=1=1

可以使用,所以我创建了这个答案

CREATE FUNCTION NumberOfCustomerDefaultAddresses
(
    @CustomerId INT
)
RETURNS INT
AS
BEGIN
    RETURN (
        SELECT COUNT(*)
        FROM CustAddress
        WHERE CustomerId = @CustomerId
        AND [Default] = 1
    )
END
GO

ALTER TABLE CustAddress ADD CONSTRAINT CHK_DefaultAddress CHECK (dbo.NumberOfCustomerDefaultAddresses(CustomerId) = 1)

这样做的原因是,它会停止导致未设置默认地址的插入。但它无法检测更改默认标志的更新和删除默认记录的删除。

将架构更改为

CREATE TABLE Customer
(
    Id INT PRIMARY KEY,
    Name VARCHAR(100) NOT NULL
)

CREATE TABLE [Address]
(
    Id INT PRIMARY KEY,
    Address VARCHAR(500) NOT NULL
)


CREATE TABLE CustDefaultAddress
(
    CustomerId INT PRIMARY KEY, /*Ensures no more than one default*/
    AddressId INT,
    FOREIGN KEY (CustomerId) REFERENCES Customer(Id),
    FOREIGN KEY (AddressId) REFERENCES [Address](Id)
)


CREATE TABLE CustSecondaryAddress
(
    CustomerId INT REFERENCES CustDefaultAddress(CustomerId), 
                   /*No secondary address can be added unless default one exists*/
    AddressId INT REFERENCES [Address](Id),
    PRIMARY KEY(CustomerId, AddressId)
)
如果有另外一个要求,即地址不能同时作为主地址和辅助地址出现,则可以使用帮助表和索引视图来强制执行该要求

CREATE TABLE dbo.TwoRows
  (
     X INT PRIMARY KEY
  );

INSERT INTO dbo.TwoRows
VALUES      (1),
            (2)

GO

CREATE VIEW V
WITH SCHEMABINDING
AS
  SELECT D.AddressId,
         D.CustomerId
  FROM   dbo.CustDefaultAddress D
         JOIN dbo.CustSecondaryAddress S
           ON D.AddressId = S.AddressId
              AND D.CustomerId = S.CustomerId
         CROSS JOIN dbo.TwoRows

GO

CREATE UNIQUE CLUSTERED INDEX IX
  ON V(AddressId, CustomerId) 

如果我没有错过这些要求,我想你可以用一个

它不像表设计解决方案那么优雅,而且需要更复杂的触发器我更喜欢触发器,但它将通过您当前的所有测试

它实际上是这样做的:

在Insert或Update的情况下,它将实际验证整个数据集新旧对,以查看每个客户是否有一个且只有一个注意到默认位的总和。如果存在0个或多个默认值,则会引发错误。 在删除的情况下,它将只验证每个客户的剩余地址,以便在地址存在时,仅在默认情况下使用相同的规则。 最后,如果没有错误,它将执行与预期相同的操作; 在表和数据上工作的触发器如下所示:

CREATE TRIGGER dbo.CustAddress1DefaultAddress
    ON  dbo.CustAddress
    Instead of INSERT, DELETE, UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    declare @cnt int, @operation char(1);
    IF exists (select * from inserted)
    and not exists (select * from deleted) --only insert, no delete/update
        select @operation = 'I';
    else if exists (select * from inserted)
        and exists (select * from deleted) --update
        Select @operation = 'U';
    else
        Select @operation = 'D';
    print 'operation = ' + @operation;

    begin try
    if @operation in ('I', 'U')
    begin
        ;with defaultsPerCustAdd(SumDefault, CustomerId)
        as (
            select sum (x.[Default]), x.CustomerId
            from (
                select i.CustomerId, cast(i.[Default] as tinyint) as [Default]
                from inserted as i
                union all
                select ca.CustomerId, cast(ca.[Default] as tinyint) as [Default]
                from dbo.CustAddress as ca
                join inserted i on i.CustomerId = ca.CustomerId
                and i.AddressId != ca.AddressId
            ) as x
            group by x.CustomerId
        )
        select *
        from defaultsPerCustAdd as d
        where d.SumDefault = 0
        OR d.SumDefault > 1;
        set @cnt = @@ROWCOUNT;
    end
    else -- Delete
    begin
        ;with defaultsPerCustAdd(SumDefault, CustomerId)
        as (
            select sum (x.[Default]), x.CustomerId
            from (
                select ca.CustomerId, cast(ca.[Default] as tinyint) as [Default]
                from dbo.CustAddress as ca
                join deleted d on d.CustomerId = ca.CustomerId
                and d.AddressId != ca.AddressId
            ) as x
            group by x.CustomerId
        )
        select *
        from defaultsPerCustAdd as d
        where d.SumDefault = 0
        OR d.SumDefault > 1;
        set @cnt = @@ROWCOUNT;
    end;

    if @cnt > 0
        raiserror('error when validating one default address per customer', 16, 1)

    if @operation = 'I'
        insert dbo.CustAddress(CustomerId, AddressId, [Default])
        select i.CustomerId, i.AddressId, i.[Default]
        from inserted as i
    else if @operation = 'U'
        update ca
        set [default] = i.[default]
        from dbo.CustAddress as ca
        join inserted as i on i.AddressId = ca.AddressId and i.CustomerId = ca.CustomerId
    else if @operation = 'D'
        delete ca
        from dbo.CustAddress as ca
        join deleted as d on d.AddressId = ca.AddressId and d.CustomerId = ca.CustomerId

    end try
    begin catch
        print 'error when validating one default address per customer';
    end catch;
END
GO

如何将客户链接到地址?它是通过一个看起来像CustomerId、AddressId的链接表实现的吗?或者CustomerId是否与默认列一起出现在地址表中?不管听起来像是检查约束的一个很好的例子,其中customerid=@customerid和default=1=1的表中的select count*是一个很好的开始。确保每个客户只有一个默认地址的约束可以通过唯一的部分索引强制执行:创建唯一索引CustAddress CustomerId上的OneDefaultAddressPerCustomer,其中[Default]=1;但是为了确保有一个默认而不是没有默认,这是另一个更复杂的问题。马丁,我创建了这个答案,以便突出我在使用它时遇到的问题。在评论中很难做到的事情。如果您有解决方案,请发布一个答案,因为我看不出如何使用筛选索引。在CustAddressCustomerId上创建唯一索引IX,其中[Default]=1Ah实际上确保不超过一个。不完全一样。要确保正好是一个,您需要更改架构并为默认地址向Customer添加一个不允许为NULL的列。当Customer没有任何地址时,不允许为NULL的列将如何工作?它不会。我发现我最初对这个问题的审视遗漏了几个重要方面!谢谢Marian,很高兴有一个替代解决方案,特别是一个不涉及更改架构的解决方案,因为我遇到过这种情况,需要维护一个遗留应用程序。@GrahamAmbrose不客气。这正是我的想法,你不能总是改变系统。虽然最好事先有一个优雅的设计,但有时你只需要一些有用的东西。+1你可能需要一个带有READCOMMITTEDLOCK的显式设计,尽管在case中运行非常稳定,非常有意义。遗憾的是,从数据处理的角度来看,它不是很方便,但我们不能期望所有东西都是免费的@是的,在这个实现中交换主地址和辅助地址将是一件相当麻烦的事情。