Ensure that one and only one default is defined for a set

sql-server

Solution

How about changing the schema to

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)
)

If there is an additional requirement that an address must not be present as both a primary and secondary address you can enforce this with a helper table and an indexed view.

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) 

Problem

I have a customer table that has a one to many relationship with an address table. I want to constrain the database so that a customer with addresses will always have one (and only one) default address. I can quite easily add a constraint to ensure that there is only ever one default address for each customer. I am however struggling on how to apply a constraint that ensures that an address is always marked as default. To summarize: - A customer is not required to have any addresses. - If a customer has addresses there must be a default address. - There must only be one default address per customer. Here is an example of the problem and some 'unit' tests. I am using a link table to join customers and addresses. ``` 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 ```

Original source