My current design is makes me want to create a FK to a non-unique/non-PK. Is this a code smell and/or does my overall design make sense?
CREATE TABLE [User].[ConnectionRequest]
(
[ConnectionRequestId] INT NOT NULL PRIMARY KEY IDENTITY,
[Requestor] INT NOT NULL, -- FK to internal user table
[Target] INT NOT NULL, -- FK to ConnectionRequestTarget table
[Status] INT NOT NULL, -- FK to status enum descriptions
[Created] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT [FK_Name_Truncated_For_Space]
FOREIGN KEY ([Target])
REFERENCES [User].[ConnectionRequestTarget]([ConnectionRequestTargetId]),
CONSTRAINT [Unique_ConnectionRequest_Target_Status] UNIQUE ([Target], [Status])
-- Other FK's removed for brevity
)
CREATE TABLE [User].[ConnectionRequestTarget]
(
[ConnectionRequestTargetId] INT NOT NULL PRIMARY KEY IDENTITY,
[InternalUserId] INT NULL, -- FK to internal user table, can be updated
[ReferralId] INT NULL, -- FK to referral table
[RequestCreated] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
-- FK's removed for brevity
);
CREATE TABLE [User].[PushSent]
(
[PushSentId] INT NOT NULL PRIMARY KEY IDENTITY,
[MatchingToken] UNIQUEIDENTIFIER NOT NULL,
[PushDeviceId] INT NOT NULL,
[Category] INT NOT NULL,
[MonitoringId] INT NULL,
[ConnectionRequestTargetId] INT NULL,
[ThresholdForNextAlert] DATETIME2(2) NULL,
[Scheduled] DATETIME2(2) NOT NULL DEFAULT SYSUTCDATETIME(),
[Sent] DATETIME2(2) NULL,
CONSTRAINT [FK_PushSent_ConnectionRequestTargetId_to_ConnectionRequest_Target]
FOREIGN KEY ([ConnectionRequestTargetId])
REFERENCES [User].[ConnectionRequest]([Target]), -- This doesn't work
-- Other FK's and check constraints removed for brevity
);
The ConnectionRequest
table contains a history of the connection requests and their status updates. I normalized the target
of the connection request into its own table so the target details wouldn't be duplicated with each status update. For example:
-- ConnectionRequest table sample data
ConnectionRequestId | Requestor | Target | Status | Created
1 1 1 1 2015-10-3 10:31:23.93 -- Request Pending
2 2 2 1 2015-10-3 10:31:25.89 -- Request Pending
3 1 3 1 2015-10-3 11:45:12.49 -- Request Pending
4 1 1 3 2015-10-3 12:45:12.49 -- Request Accepted
5 1 4 1 2015-10-3 12:45:12.49 -- Request Pending
-- ConnectionRequestTarget table sample data
ConnectionRequestTargetId | InternalUserId | ReferralId | RequestCreated
1 3 NULL 2015-10-3 10:31:23.93 -- Internal
2 3 NULL 2015-10-3 10:31:25.89 -- Internal
3 NULL 1 2015-10-3 11:45:12.49 -- External
4 4 2 2015-10-3 12:45:12.49 -- Updated
As you can see connection status updates are handled by inserting a new row into the ConnectionRequest
table. The (Target, Status)
UNIQUE
constraint in the ConnectionRequest
table is there to ensure a Target
doesn't get multiple entries for the same status. Furthermore a new ConnectionRequestTarget
is generated for each request even if the user being targeted is the same. The InternalUserId
column in the ConnectionRequestTarget
table can be updated if the target wasn't a user, and then creates an account.
This worked great until a requirement came in to periodically prompt users with a push notification if they haven't acted(accept/deny) on a connection request. Due to that request I want to put a FK into the PushSent
table which will let me track the number of pushes sent to a target.
I can't set the FK to the Target
column in the ConnectionRequest
table because it is non-unique by design. I know I can put the Status
flag into the PushSent
table with the Target
, or handle this with a UDF and a check constraint, but those options feel a bit dirty. I could also specify a FK to the ConnectionRequestTarget
table directly from the PushSent
table but that could open the door for associating pushes to an orphaned target.
In truth it gets a bit more complicated than this because a Target
of a ConnectionRequest
can be a current member, or a potential member, but the core requirement to track the count of pushes/contacts to a Target
remains.
Best Answer
It seems to me you have an entity Target against which you may have an active ConnectionRequest. It would appear that the Status of a request will be changed from time to time as the request is processed. In that case, that field should not be made part of any key or unique constraint. Only stable (unchanging) fields should be part of a key. In this case, it would appear that the Created field would be perfect.
However, I don't see why you need to create a unique constraint at all. You already assign a unique PK to each request, why don't you have ConnectionRequestTargetId refer to ConnectionRequest( ConnectionRequestID )? This would safely tie each PushSent to one specific request.