SQL Server 2005: Nullable Foreign Key Constraint - sql

I have a foreign key constraint between tables Sessions and Users. Specifically, Sessions.UID = Users.ID. Sometimes, I want Sessions.UID to be null. Can this be allowed? Any time I try to do this, I get an FK Constraint Violation.
Specifically, I'm inserting a row into Sessions via LINQ. I set the Session.User = null; and I get this error:
An attempt was made to remove a relationship between a User and a Session. However, one of the relationship's foreign keys (Session.UID) cannot be set to null.
However, when I remove the line that nulls the User property, I get this error on my SubmitChanges line:
Value cannot be null.
Parameter name: cons
None of my tables have a field called 'cons', nor is it in my 5,500-line DataContext.designer.cs file, nor is it in the QuickWatch for any of the related objects, so I have no idea what 'cons' is.
In the Database, Session.UID is a nullable int field and User.ID is a non-nullable int. I want to record sessions that may or may not have a UID, and I'd rather do it without disabling constraint on that FK relationship. Is there a way to do this?

I seemed to remember creating a nullable FK before, so I whipped up a quick test. As you can see below, it is definitely doable (tested on MSSQL 2005).
Script the relevant parts of your tables and constraints and post them so we can troubleshoot further.
CREATE DATABASE [NullableFKTest]
GO
USE [NullableFKTest]
GO
CREATE TABLE OneTable
(
OneId [int] NOT NULL,
CONSTRAINT [PK_OneTable] PRIMARY KEY CLUSTERED
(
[OneId] ASC
)
)
CREATE TABLE ManyTable (ManyId [int] IDENTITY(1,1) NOT NULL, OneId [int] NULL)
GO
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_ManyTable_OneTable]') AND parent_object_id = OBJECT_ID(N'[dbo].[ManyTable]') )
ALTER TABLE [dbo].[ManyTable] WITH CHECK ADD CONSTRAINT [FK_ManyTable_OneTable] FOREIGN KEY([OneId])
REFERENCES [dbo].[OneTable] ([OneId])
GO
--let's get a value in here
insert into OneTable(OneId) values(1)
select* from OneTable
--let's try creating a valid relationship to the FK table OneTable
insert into ManyTable(OneId) values (1) --fine
--now, let's try NULL
insert into ManyTable(OneId) values (NULL) --also fine
--how about a non-existent OneTable entry?
insert into ManyTable(OneId) values (5) --BOOM! - FK violation
select* from ManyTable
--1, 1
--2, NULL
--cleanup
ALTER TABLE ManyTable DROP CONSTRAINT FK_ManyTable_OneTable
GO
drop TABLE OneTable
GO
drop TABLE ManyTable
GO
USE [Master]
GO
DROP DATABASE NullableFKTest

Related

The INSERT statement conflicted with the FOREIGN KEY constraint problem

I have the script below, which gives me an error:
"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.PlanShiftAssignments_dbo.User_UserId". The conflict occurred in database "SWS", table "dbo.User", column 'Id'.
The statement has been terminated."
As you can see, in WHERE clause I check if UserId exists in dbo.User. What are the other possible reasons of the error?
UPDATED: I am also want to know what row from select statement causes the error. Any advices on debugging this query will be appreciated. I am using MS SQL Server Management Studio.
CREATE TABLE [dbo].[PlanShiftAssignments] (
[PlanShiftId] [uniqueidentifier] NOT NULL,
[Status] [int] NOT NULL,
[UserId] [int],
CONSTRAINT [PK_dbo.PlanShiftAssignments] PRIMARY KEY ([PlanShiftId])
)
CREATE INDEX [IX_PlanShiftId] ON [dbo].[PlanShiftAssignments]([PlanShiftId])
CREATE INDEX [IX_UserId] ON [dbo].[PlanShiftAssignments]([UserId])
ALTER TABLE [dbo].[PlanShiftAssignments] ADD CONSTRAINT [FK_dbo.PlanShiftAssignments_dbo.PlanShifts_PlanShiftId] FOREIGN KEY ([PlanShiftId]) REFERENCES [dbo].[PlanShifts] ([Id])
ALTER TABLE [dbo].[PlanShiftAssignments] ADD CONSTRAINT [FK_dbo.PlanShiftAssignments_dbo.User_UserId] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
insert into dbo.PlanShiftAssignments
select ps.Id as PlanShiftId, ISNULL(ps.AssigneeId, psi.UserId) as UserId, ISNULL(psi.[Status], 1) as [Status] from dbo.PlanShifts ps
left join
dbo.PlanShiftInvitations psi
on ps.Id = psi.PlanShiftId
where (psi.UserId is not null and psi.UserId IN (select Id from dbo.[User]))
or (ps.AssigneeId is not null and ps.AssigneeId IN (select Id from dbo.[User]))
Make sure that you always include the target's column list on each INSERT statement.
insert into dbo.PlanShiftAssignments (
PlanShiftId,
UserId,
Status)
SELECT
ps.Id as PlanShiftId,
ISNULL(ps.AssigneeId, psi.UserId) as UserId,
ISNULL(psi.[Status], 1) as [Status]
...
Your table is created with the order PlanShiftId, Status, UserId and the column order from your current SELECT is PlanShiftId, UserId, Status, hence the confusion.
You have a strange data model, if UserId and AssigneeId do not already refer to User in the underlying tables.
In any case, your where clause is
where (psi.UserId is not null and psi.UserId IN (select Id from dbo.[User])) or
(ps.AssigneeId is not null and ps.AssigneeId IN (select Id from dbo.[User]))
This leaves open the possibility that psi.UserId matches but ps.AssigneeId does not.
To ensure that the logic matches, use the same expression as in the select:
where coalesce(ps.AssigneeId, psi.UserId) in (select Id from dbo.[User])
Could it be for the fact you've specified an OR in your WHERE clause, therefore either the AssigneeId or UserId is in the User table but not the other and therefore invalidates the FK constraint.

How to force Microsoft Database Project generate ALTER statement for primary key constraint instead of creating temp table?

I have following script for LoginLogo table:
CREATE TABLE [LoginLogo] (
[LoginLogoId] INT IDENTITY (1, 1) NOT NULL,
[LoginId] INT NOT NULL,
[LogoNm] NVARCHAR(255) NULL,
CONSTRAINT [PK_LoginLogo_LoginLogoId] PRIMARY KEY CLUSTERED ([LoginId] ASC),
CONSTRAINT [FK_LoginLogo_LoginId] FOREIGN KEY ([LoginId])
REFERENCES [Login] ([LoginId])
);
GO
CREATE NONCLUSTERED INDEX [IF_LoginLogo_LoginId]
ON [LoginLogo]([LoginId] ASC)
ON [INDX];
I need to change Primary Key Constraint, so I've just changed one line, please see below the change:
CONSTRAINT [PK_LoginLogo_LoginLogoId] PRIMARY KEY CLUSTERED ([LoginLogoId] ASC),
Database project perfectly build changed code, but when it generates database update statement it generates temp table instead of simple ALTER statement. See below generated script:
CREATE TABLE [tmp_ms_xx_LoginLogo] (
[LoginLogoId] INT IDENTITY (1, 1) NOT NULL,
[LoginId] INT NOT NULL,
[LogoNm] NVARCHAR (255) NULL,
CONSTRAINT [tmp_ms_xx_constraint_PK_LoginLogo_LoginLogoId1]
PRIMARY KEY CLUSTERED ([LoginLogoId] ASC)
);
IF EXISTS (SELECT TOP 1 1
FROM [apps].[LoginLogo])
BEGIN
SET IDENTITY_INSERT [apps].[tmp_ms_xx_LoginLogo] ON;
INSERT INTO [apps].[tmp_ms_xx_LoginLogo] ([LoginLogoId], [LoginId], [LogoNm])
SELECT [LoginLogoId],
[LoginId],
[LogoNm],
FROM [LoginLogo]
ORDER BY [LoginLogoId] ASC;
SET IDENTITY_INSERT [tmp_ms_xx_LoginLogo] OFF;
END
DROP TABLE [LoginLogo];
EXECUTE sp_rename N'[tmp_ms_xx_LoginLogo]', N'LoginLogo';
EXECUTE sp_rename N'[tmp_ms_xx_constraint_PK_LoginLogo_LoginLogoId1]',
N'PK_LoginLogo_LoginLogoId', N'OBJECT';
Is it possible to tell Database project to generate ALTER statement instead of creating temp table? How can I force Microsoft Database Project to do that?
Bearing in mind that if you change the clustered index of a table, the table will be rebuilt regardless of whether the script does ALTER TABLE or the SSDT-generated stuff with temp tables, the usual way to solve these problems is to do the ALTER ahead of time
Meaning, you need a script, often referred to as a pre-pre-deploy script (pre-deploy won't work, as it is run post-comparison) that makes the expensive change, so that when the comparison is run the change has already occurred, and hence doesn't get repeated by the dacpac deployment.
This script needs to be run as part of your deployment, before you do any of the sqlpackage stuff. You can specify the change as alter table in this script.
In this particular instance, where the table is going to be rebuilt either way, I can't see it making a great deal of difference to the overall deployment time.

CHECK CONSTRAINT on multiple columns

I use SQL Server 2008
I use a CHECK CONSTRAINT on multiple columns in the same table to try to validate data input.
I receive an error:
Column CHECK constraint for column
'AAAA' references another column,
table 'XXXX'.
CHECK CONSTRAINT does not work in this way.
Any other way to implement this on a single table without using FK?
Thanks
Here an example of my code
CREATE TABLE dbo.Test
(
EffectiveStartDate dateTime2(2) NOT NULL,
EffectiveEndDate dateTime2(2) NOT NULL
CONSTRAINT CK_CmsSponsoredContents_EffectiveEndDate CHECK (EffectiveEndDate > EffectiveStartDate),
);
Yes, define the CHECK CONSTRAINT at the table level
CREATE TABLE foo (
bar int NOT NULL,
fred varchar(50) NOT NULL,
CONSTRAINT CK_foo_stuff CHECK (bar = 1 AND fred ='fish')
)
You are declaring it inline as a column constraint
...
fred varchar(50) NOT NULL CONSTRAINT CK_foo_fred CHECK (...)
...
Edit, easier to post than describe. Fixed your commas.
CREATE TABLE dbo.Test
(
EffectiveStartDate dateTime2(2) NOT NULL,
EffectiveEndDate dateTime2(2) NOT NULL, --need comma
CONSTRAINT CK_CmsSponsoredContents_EffectiveEndDate CHECK (EffectiveEndDate > EffectiveStartDate) --no comma
);
Of course, the question remains are you using a CHECK constraint where it should be an FK constraint...?
Check constraints can refer to a single column or to the whole record.
Use this syntax for record-level constraints:
ALTER TABLE MyTable
ADD CONSTRAINT MyCheck
CHECK (...your check expression...)
You can simply apply your validation in a trigger on the table especially that either way the operation will be rolled back if the check failed.
I found it more useful for CONSTRAINT using case statements.
ALTER TABLE dbo.ProductStock
ADD
CONSTRAINT CHK_Cost_Sales
CHECK ( CASE WHEN (IS_NOT_FOR_SALE=0 and SAL_CPU <= SAL_PRICE) THEN 1
WHEN (IS_NOT_FOR_SALE=1 ) THEN 1 ELSE 0 END =1 )

In SQL Server 2005, how do I set a column of integers to ensure values are greater than 0?

This is probably a simple answer but I can't find it. I have a table with a column of integers and I want to ensure that when a row is inserted that the value in this column is greater than zero. I could do this on the code side but thought it would be best to enforce it on the table.
Thanks!
I was in error with my last comment all is good now.
You can use a check constraint on the column. IIRC the syntax for this looks like:
create table foo (
[...]
,Foobar int not null check (Foobar > 0)
[...]
)
As the poster below says (thanks Constantin), you should create the check constraint outside the table definition and give it a meaningful name so it is obvious which column it applies to.
alter table foo
add constraint Foobar_NonNegative
check (Foobar > 0)
You can get out the text of check constraints from the system data dictionary in sys.check_constraints:
select name
,description
from sys.check_constraints
where name = 'Foobar_NonNegative'
Create a database constraint:
ALTER TABLE Table1 ADD CONSTRAINT Constraint1 CHECK (YourCol > 0)
You can have pretty sophisticated constraints, too, involving multiple columns. For example:
ALTER TABLE Table1 ADD CONSTRAINT Constraint2 CHECK (StartDate<EndDate OR EndDate IS NULL)
I believe you want to add a CONSTRAINT to the table field:
ALTER TABLE tableName WITH NOCHECK
ADD CONSTRAINT constraintName CHECK (columnName > 0)
That optional NOCHECK is used to keep the constraint from being applied to existing rows of data (which could contain invalid data) & to allow the constraint to be added.
Add a CHECK constraint when creating your table
CREATE TABLE Test(
[ID] [int] NOT NULL,
[MyCol] [int] NOT NULL CHECK (MyCol > 1)
)
you can alter your table and add new constraint like bellow.
BEGIN TRANSACTION
GO
ALTER TABLE dbo.table1 ADD CONSTRAINT
CK_table1_field1 CHECK (field1>0)
GO
ALTER TABLE dbo.table1 SET (LOCK_ESCALATION = TABLE)
GO
COMMIT

Constraint for only one record marked as default

How could I set a constraint on a table so that only one of the records has its isDefault bit field set to 1?
The constraint is not table scope, but one default per set of rows, specified by a FormID.
Use a unique filtered index
On SQL Server 2008 or higher you can simply use a unique filtered index
CREATE UNIQUE INDEX IX_TableName_FormID_isDefault
ON TableName(FormID)
WHERE isDefault = 1
Where the table is
CREATE TABLE TableName(
FormID INT NOT NULL,
isDefault BIT NOT NULL
)
For example if you try to insert many rows with the same FormID and isDefault set to 1 you will have this error:
Cannot insert duplicate key row in object 'dbo.TableName' with unique
index 'IX_TableName_FormID_isDefault'. The duplicate key value is (1).
Source: http://technet.microsoft.com/en-us/library/cc280372.aspx
Here's a modification of Damien_The_Unbeliever's solution that allows one default per FormID.
CREATE VIEW form_defaults
AS
SELECT FormID
FROM whatever
WHERE isDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix_form_defaults on form_defaults (FormID)
GO
But the serious relational folks will tell you this information should just be in another table.
CREATE TABLE form
FormID int NOT NULL PRIMARY KEY
DefaultWhateverID int FOREIGN KEY REFERENCES Whatever(ID)
From a normalization perspective, this would be an inefficient way of storing a single fact.
I would opt to hold this information at a higher level, by storing (in a different table) a foreign key to the identifier of the row which is considered to be the default.
CREATE TABLE [dbo].[Foo](
[Id] [int] NOT NULL,
CONSTRAINT [PK_Foo] PRIMARY KEY CLUSTERED
(
[Id] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[DefaultSettings](
[DefaultFoo] [int] NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[DefaultSettings] WITH CHECK ADD CONSTRAINT [FK_DefaultSettings_Foo] FOREIGN KEY([DefaultFoo])
REFERENCES [dbo].[Foo] ([Id])
GO
ALTER TABLE [dbo].[DefaultSettings] CHECK CONSTRAINT [FK_DefaultSettings_Foo]
GO
You could use an insert/update trigger.
Within the trigger after an insert or update, if the count of rows with isDefault = 1 is more than 1, then rollback the transaction.
CREATE VIEW vOnlyOneDefault
AS
SELECT 1 as Lock
FROM <underlying table>
WHERE Default = 1
GO
CREATE UNIQUE CLUSTERED INDEX IX_vOnlyOneDefault on vOnlyOneDefault (Lock)
GO
You'll need to have the right ANSI settings turned on for this.
I don't know about SQLServer.But if it supports Function-Based Indexes like in Oracle, I hope this can be translated, if not, sorry.
You can do an index like this on suposed that default value is 1234, the column is DEFAULT_COLUMN and ID_COLUMN is the primary key:
CREATE
UNIQUE
INDEX only_one_default
ON my_table
( DECODE(DEFAULT_COLUMN, 1234, -1, ID_COLUMN) )
This DDL creates an unique index indexing -1 if the value of DEFAULT_COLUMN is 1234 and ID_COLUMN in any other case. Then, if two columns have DEFAULT_COLUMN value, it raises an exception.
The question implies to me that you have a primary table that has some child records and one of those child records will be the default record. Using address and a separate default table here is an example of how to make that happen using third normal form. Of course I don't know if it's valuable to answer something that is so old but it struck my fancy.
--drop table dev.defaultAddress;
--drop table dev.addresses;
--drop table dev.people;
CREATE TABLE [dev].[people](
[Id] [int] identity primary key,
name char(20)
)
GO
CREATE TABLE [dev].[Addresses](
id int identity primary key,
peopleId int foreign key references dev.people(id),
address varchar(100)
) ON [PRIMARY]
GO
CREATE TABLE [dev].[defaultAddress](
id int identity primary key,
peopleId int foreign key references dev.people(id),
addressesId int foreign key references dev.addresses(id))
go
create unique index defaultAddress on dev.defaultAddress (peopleId)
go
create unique index idx_addr_id_person on dev.addresses(peopleid,id);
go
ALTER TABLE dev.defaultAddress
ADD CONSTRAINT FK_Def_People_Address
FOREIGN KEY(peopleID, addressesID)
REFERENCES dev.Addresses(peopleId, id)
go
insert into dev.people (name)
select 'Bill' union
select 'John' union
select 'Harry'
insert into dev.Addresses (peopleid, address)
select 1, '123 someplace' union
select 1,'work place' union
select 2,'home address' union
select 3,'some address'
insert into dev.defaultaddress (peopleId, addressesid)
select 1,1 union
select 2,3
-- so two home addresses are default now
-- try adding another default address to Bill and you get an error
select * from dev.people
join dev.addresses on people.id = addresses.peopleid
left join dev.defaultAddress on defaultAddress.peopleid = people.id and defaultaddress.addressesid = addresses.id
insert into dev.defaultaddress (peopleId, addressesId)
select 1,2
GO
You could do it through an instead of trigger, or if you want it as a constraint create a constraint that references a function that checks for a row that has the default set to 1
EDIT oops, needs to be <=
Create table mytable(id1 int, defaultX bit not null default(0))
go
create Function dbo.fx_DefaultExists()
returns int as
Begin
Declare #Ret int
Set #ret = 0
Select #ret = count(1) from mytable
Where defaultX = 1
Return #ret
End
GO
Alter table mytable add
CONSTRAINT [CHK_DEFAULT_SET] CHECK
(([dbo].fx_DefaultExists()<=(1)))
GO
Insert into mytable (id1, defaultX) values (1,1)
Insert into mytable (id1, defaultX) values (2,1)
This is a fairly complex process that cannot be handled through a simple constraint.
We do this through a trigger. However before you write the trigger you need to be able to answer several things:
do we want to fail the insert if a default exists, change it to 0 instead of 1 or change the existing default to 0 and leave this one as 1?
what do we want to do if the default record is deleted and other non default records are still there? Do we make one the default, if so how do we determine which one?
You will also need to be very, very careful to make the trigger handle multiple row processing. For instance a client might decide that all of the records of a particular type should be the default. You wouldn't change a million records one at a time, so this trigger needs to be able to handle that. It also needs to handle that without looping or the use of a cursor (you really don't want the type of transaction discussed above to take hours locking up the table the whole time).
You also need a very extensive tesing scenario for this trigger before it goes live. You need to test:
adding a record with no default and it is the first record for that customer
adding a record with a default and it is the first record for that customer
adding a record with no default and it is the not the first record for that customer
adding a record with a default and it is the not the first record for that customer
Updating a record to have the default when no other record has it (assuming you don't require one record to always be set as the deafault)
Updating a record to remove the default
Deleting the record with the deafult
Deleting a record without the default
Performing a mass insert with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record inserts
Performing a mass update with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record updates
Performing a mass delete with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record deletes
#Andy Jones gave an answer above closest to mine, but bearing in mind the Rule of Three, I placed the logic directly in the stored proc that updates this table. This was my simple solution. If I need to update the table from elsewhere, I will move the logic to a trigger. The one default rule applies to each set of records specified by a FormID and a ConfigID:
ALTER proc [dbo].[cpForm_UpdateLinkedReport]
#reportLinkId int,
#defaultYN bit,
#linkName nvarchar(150)
as
if #defaultYN = 1
begin
declare #formId int, #configId int
select #formId = FormID, #configId = ConfigID from csReportLink where ReportLinkID = #reportLinkId
update csReportLink set DefaultYN = 0 where isnull(ConfigID, #configId) = #configId and FormID = #formId
end
update
csReportLink
set
DefaultYN = #defaultYN,
LinkName = #linkName
where
ReportLinkID = #reportLinkId