Adding a nullable foreign key - sql

I have two tables built like this (this is just a simplified and non-proprietary example):
Person Table
-----------
p_Id, f_name, l_name
Job Table
----------
job_Id, job_desc
I want to add a foreign key column, Persons.job_Id, that can be nullable that references Job.job_Id (the PK) The reason is, the job may not be known in advance, so it could be null. Having an "Other" is not an option.
I had this so far but I'm getting "could not create constraint".
ALTER TABLE dbo.Person
ADD job_Id INT FOREIGN KEY (job_Id) REFERENCES dbo.Job(job_Id)

Try it in two steps:
ALTER TABLE dbo.Person ADD job_Id INT NULL;
ALTER TABLE dbo.Person ADD CONSTRAINT FL_JOB
FOREIGN KEY (job_Id) REFERENCES dbo.Job(job_Id);

Try it like this, WITH NOCHECK:
ALTER TABLE dbo.Person ADD job_Id INT NULL;
ALTER TABLE dbo.Person WITH NOCHECK ADD CONSTRAINT FL_JOB
FOREIGN KEY (job_Id) REFERENCES dbo.Job(job_Id);

Below is my solution with creating foreign key programmatically.
TestTable1 has substitute of FK that is either NULL or matches record in TestTable2.
TestTable2 has standard FK in TestTable1.
CREATE Table TestTable1 (ID1 int IDENTITY UNIQUE, ID2 int NULL);
GO
CREATE Table TestTable2 (ID2 int IDENTITY UNIQUE, ID1 int NOT NULL foreign key references TestTable1(ID1));
GO
CREATE procedure CreateTestRecord1 #ID2 int null AS
begin
if #iD2 IS NOT NULL AND NOT EXISTS(SELECT * from TestTable2 where ID2 = #ID2)
begin
RAISERROR('Cannot insert TestTable1 record. TestTable2 record with ID %d doesnt exist', 16, 1, #ID2);
return;
end
Insert into TestTable1(ID2) OUTPUT Inserted.ID1 Values(#ID2);
end
GO
CREATE procedure LinkTable1toTable2 #ID1 int, #ID2 int NULL as
begin
if #iD2 IS NOT NULL AND NOT EXISTS(SELECT * from TestTable2 where ID2 = #ID2)
begin
RAISERROR('Cannot update ID2 in TestTable1 record. TestTable2 record with ID %d doesnt exist', 16, 1, #ID2);
return;
end
update TestTable1 Set ID2=#ID2 where ID1=#ID1;
select ##ROWCOUNT;
endGO

Related

How to insert data into another table when the condition is met with trigger

I have these 4 tables:
CREATE TABLE dbo.person
(
personId INT IDENTITY(1,1) NOT NULL,
firstName NVARCHAR(30) NOT NULL,
lastName NVARCHAR(30) NOT NULL,
CONSTRAINT pkPerson PRIMARY KEY (personId),
);
CREATE TABLE dbo.personRegistration
(
person_registrationId INT IDENTITY(1,1) NOT NULL,
personId INT,
firstName NVARCHAR(30) NOT NULL,
lastName NVARCHAR(30) NOT NULL,
confirmed NCHAR(1) DEFAULT 'N' NOT NULL,
CONSTRAINT pkpersonRegistration PRIMARY KEY (person_registrationId),
CONSTRAINT fkpersonRegistration FOREIGN KEY (personId) REFERENCES dbo.person (personId)
CONSTRAINT personConfirmed CHECK (confirmed IN ('Y', 'N'))
);
CREATE TABLE dbo.person_organizationalUnit
(
personId INT NOT NULL,
organizationalUnitId INT NOT NULL,
CONSTRAINT pkorganizationalUnit PRIMARY KEY (personId, organizationalUnitId),
CONSTRAINT fkperson FOREIGN KEY (personId) REFERENCES dbo.person (personId),
CONSTRAINT fkorganizationalUnit FOREIGN KEY (organizationalUnitId) REFERENCES dbo.organizatinalUnit(unicOrgUnitId),
);
CREATE TABLE dbo.organizatinalUnit
(
organizationalUnitId INT IDENTITY(1,1) NOT NULL,
organizationalUnitName NVARCHAR(130) NOT NULL,
CONSTRAINT pkorganizationalUnit PRIMARY KEY (organizationalUnitId)
);
I need to create a trigger which will do that when I add new person in table personRegistration (his personId is set to NULL, and initial value for confirmed is 'N') and when I update personRegistration and set confirmed to 'Y', that person is going to be inserted into table person (value for personId is generated because the personId is an identity column) and the confirmed is going to change it's value to 'Y' and is going to be inserted in table person_organizationalUnit. I have written the trigger but the problem is when I update the personRegistration for more than one person my data double with each update.
CREATE TRIGGER personConfirmed
ON dbo.personRegistration
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
INSERT INTO dbo.person (firstName, lastName)
SELECT
firstName, lastName
FROM
dbo.personRegistration
SET NOCOUNT ON
DECLARE #idPerson int
SELECT #idPerson = personId
FROM dbo.person
INSERT INTO dbo.person_organizationalUnit (personId, organizationalUnitId)
SELECT #idPerson, I.organizationalUnitId
FROM Inserted AS I
JOIN dbo.person p ON p.personId = #idPerson
WHERE confirmed = 'Y';
END
Data for insert:
INSERT INTO dbo.personRegistration (personId, firstName, lastName, confirmed)
VALUES (NULL, 'John', 'Smith', 'N');
Data for update:
UPDATE dbo.personRegistration
SET confirmed = 'Y'
WHERE personRegistrationId = 1;
SQL Server triggers works with sets not single register i did some small changes in your trigger
create TRIGGER dbo.usp_PersonConfirmed ON dbo.personRegistration
AFTER UPDATE
AS
BEGIN
-- create person if not exists
INSERT INTO dbo.person (firstName, lastName)
SELECT firstName, lastName
FROM dbo.personRegistration p
where not exists(select * from dbo.Person where firstName = p.firstName
and lastName = p.lastName)
-- create orgonization unit if person dont exist and confirmed is Y
INSERT INTO dbo.person_organizationalUnit (personId, organizationalUnitId)
SELECT i.personId, I.organizationalUnitId
FROM Inserted AS I
where not exists(select * from dbo.person_organizationalUnit where
personId = i.personId)
and confirmed = 'Y';
-- update orgonization unit if person exist and confirmed is Y
update pou set organizationalUnitId = I.organizationalUnitId
from dbo.person_organizationalUnit pou
inner join Inserted AS I on i.personID = pou.personId
where i.confirmed = 'Y';
END
Your trigger has a fatal flaw: it does not deeal properly with multiple rows. It is also not using the inserted table in the first INSERT, and instead selecting from the whole original table.
So you need to OUTPUT the identity column from the first insert in order to use it in the second.
Because you don't have the identity column yet, you need to join onto firstName and lastName, which I need not say isn't a very good primary key
CREATE OR ALTER TRIGGER personConfirmed
ON dbo.personRegistration
AFTER UPDATE
AS
SET NOCOUNT ON
IF NOT UPDATE(confirmed) OR NOT EXISTS (SELECT 1 FROM inserted)
RETURN; --early bailout
DECLARE #ids TABLE (personId int PRIMARY KEY, firstName nvarchar(100), lastName nvarchar(100));
INSERT INTO dbo.person (firstName, lastName)
OUTPUT inserted.personId, inserted.firstName, inserted.lastName
SELECT
i.firstName,
i.lastName
FROM
inserted i
WHERE i.confirmed = 'Y';
INSERT INTO dbo.person_organizationalUnit (personId, organizationalUnitId)
SELECT ids.personId, i.organizationalUnitId
FROM inserted AS i
JOIN #ids ids ON i.firstName = ids.firstName AND i.lastName = ids.lastName;
Ideally, you have some kind of unique primary key on personRegistration. then your trigger would look like this:
CREATE OR ALTER TRIGGER personConfirmed
ON dbo.personRegistration
AFTER UPDATE
AS
SET NOCOUNT ON
IF NOT UPDATE(confirmed) OR NOT EXISTS (SELECT 1 FROM inserted)
RETURN; --early bailout
DECLARE #ids TABLE (personId int PRIMARY KEY, registrationId int);
MERGE dbo.person p
USING (
SELECT *
FROM inserted i
WHERE i.confirmed = 'Y'
) i
ON 1 = 0 -- never match
WHEN NOT MATCHED THEN
INSERT (firstName, lastName)
VALUES (i.firstName, i.lastName)
OUTPUT inserted.personId, i.organizationalUnitId
INTO #ids (personId, organizationalUnitId)
;
INSERT INTO dbo.person_organizationalUnit (personId, organizationalUnitId)
SELECT ids.personId, i.organizationalUnitId
FROM #ids ids;
We need that funny MERGE because we want to OUTPUT columns that we are not inserting. You can only do this using MERGE.

Check constraint with multiple conditions and other table column referring

I have two table called tbl_1 and tbl_2 with below schema
CREATE TABLE tbl_1(id int, disabled bit, qtype int)
and
CREATE TABLE tbl_2 (qtype int, qname nvarchar(MAX))
i want to add a constraint to the tbl_1 so that if (disabled=0 or disabled is null) qtype must be a number that exists in the tbl_2.qtype column?
I tried creating a function and added a check constraint
CREATE FUNCTION fn_Check_qtype(#qtype INT)
RETURNS int
AS
BEGIN
IF EXISTS(SELECT qtype from tbl_1 where (disabled=0 or disabled is null) and qtype=#qtype)
IF EXISTS (SELECT qtype FROM tbl_2 WHERE qtype is not null and qtype = #qtype)
return 1
return 0
END
Constraint
alter table tbl_1
add constraint ck_qtyppe
check (dbo.fn_Check_qtype(qtype) =1)
But even no non-matching records it is throwing error
The ALTER TABLE statement conflicted with the CHECK constraint
"ck_qtyppe". The conflict occurred in database "TestDB", table
"dbo.tbl_1", column 'qtype'.
But if i am deleting qtype null value from tbl_1 it is working no matter disabled=0, disabled=, disabled = null.
First, a foreign key references should be to a primary key, so let me assume tbl_2 is defined as:
CREATE TABLE tbl_2 (
qtype int primary key,
qname nvarchar(MAX)
);
Then, you can do this without a user-defined function. All you need is a persisted computed column:
CREATE TABLE tbl_1 (
id int,
disabled bit,
qtype int,
qtype_enabled as (case when disabled = 1 then qtype end) persisted,
foreign key (qtype_enabled) references tbl_2 (qtype)
);
Here is a db<>fiddle.

Introducing FOREIGN KEY constraint may cause cycles or multiple cascade paths

I have this sql script to create database in mssql:
CREATE TABLE ENTITY (
ID bigint IDENTITY(1,1) NOT NULL,
NAME nvarchar(255) NOT NULL,
PARENT_ID bigint NULL
)
go
ALTER TABLE ENTITY
ADD CONSTRAINT PK_ENTITY PRIMARY KEY CLUSTERED (ID)
WITH FILLFACTOR = 80
go
ALTER TABLE ENTITY
ADD CONSTRAINT FK_ENTITY FOREIGN KEY (PARENT_ID)
REFERENCES ENTITY (ID) ON DELETE CASCADE ON UPDATE CASCADE
go
By design i'd like to create parent->child relations in single table
So, just some pseudocode examples:
id | name | parent_id
1 Mother null
2 Steve 1
3 Jack 1
I'd like to remove Mother with all DELETE Cascade childs if needed
But after i install this script i receive:
Introducing FOREIGN KEY constraint 'FK_ENTITY' on table 'ENTITY' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Is any solution how i can implement such single table hierarchy correctly?
It seems sql server doesn't let Cascading DELETE with Recursive Foreign Keys, so you can write procedure for your delete action like below :
CREATE PROC DeleteChildWithParent(#ID BIGINT)
AS
;WITH ChildToDelete AS (
SELECT id, CAST(1 AS INT) AS ChildLevel
FROM ENTITY
WHERE id = #ID
UNION ALL
SELECT e.id, C.ChildLevel + 1
FROM ENTITY E
JOIN ChildToDelete C ON e.PARENT_ID = C.ID AND E.PARENT_ID <> E.ID
)
SELECT id, ROW_NUMBER() OVER (ORDER BY ChildLevel DESC) Ord
INTO #ChildToDelete
FROM ChildToDelete
DECLARE #count INT = 1, #max INT = ##ROWCOUNT;
WHILE #count <= #max
BEGIN
DELETE ENTITY WHERE id = (SELECT id FROM #ChildToDelete WHERE Ord = #count);
SET #count = #count + 1;
END;
GO

How to trigger a table to change the value of another table column

I've created three tables.
CREATE TABLE Clients
(
ClientID INT IDENTITY(1,1) PRIMARY KEY,
First_Name VARCHAR(50) NOT NULL,
Last_Name VARCHAR(50) NOT NULL,
)
CREATE TABLE Reservation
(
ReservationID INT IDENTITY(1,1) PRIMARY KEY,
ClientID INT FOREIGN KEY (ClientID) REFERENCES Clients(ClientID),
Reservation_paid VARCHAR(3) DEFAULT 'NO',
)
CREATE TABLE Payment
(
Payment_ID INT IDENTITY(1,1) PRIMARY KEY,
ClientID INT FOREIGN KEY (ClientID) REFERENCES Clients(ClientID),
ReservationID INT FOREIGN KEY (ReservationID) REFERENCES Reservation(ReservationID),
)
I would like to change the value of the column Reservation_paid to YES at the Reservation table whenever the Client does pay the reservation, and i want to do it automatically with trigger.
Example: If the ClientID at the Reservation table exists at the Payment table automatically the value of the Reservation_paid will set to YES.
Thank you in advance.
CREATE TRIGGER trgAfterInsert ON [dbo].[Payment]
FOR INSERT
AS
declare #ClientID int;
select #ClientID =i.ClientID from inserted i;
if update(ClientID)
UPDATE Reservation set Reservation_paid='Yes' WHERE
ClientID=#ClientID;
--PRINT 'AFTER INSERT trigger fired.'
After Insert Trigger should do something like this
UPDATE R
SET Reservation_paid = 'Yes'
FROM reservation R
WHERE EXISTS (SELECT 1
FROM INSERTED I
WHERE I.clientid = R.clientid
AND I.reservationid = R.reservationid)
CREATE TRIGGER trgAfterInsert ON [dbo].[Payment]
FOR INSERT
AS
declare #ClientID int;
select #ClientID =i.ClientID from inserted i;
insert into Reservation(ClientID,Reservation_paid)
values(#ClientID,'Yes');
--PRINT 'AFTER INSERT trigger fired.'
GO
Write a trigger that will work on table Reservation after any insert or update on ClientId column of table Payment. Then match the ClientID with ClientID column of Reservation table and update the corresponding Reservation_paid to YES.
Edit:
The trigger will be like this
CREATE TRIGGER `UpdateReservation_paid` AFTER INSERT OR UPDATE ON `Payment`
FOR EACH ROW BEGIN
AS
begin
update Reservation
SET Reservation_paid='YES'
Where NEW.ClientID = Reservation.ClientID
and NEW.ReservationID = Reservation.ReservationID
end

How to insert row in table with foreign key to itself?

I have table that has foreign key for itself. Column parentid is foreign key and it cannot be NULL.
if I doINSERT INTO mytable(name) VALUES('name'), so it says that can't insert NULL to parentid. BUT, what value I can set to it if no row was inserted yet?!
How I can write script that will add row to this table?
Thank you
Remove the NOT NULL constraint, as it is an inappropriate constraint. If you do not have a ParentId then the value is NULL and should be allowed. Creating a dummy row just to have a dummy parentid creates unnecessary dependencies.
A trick: Have a dummy row with a dummy key, say 99999. Insert with this as the FK, and then change the FK to its real value. And do it in a transaction.
Disable the FK in charge.
Then do the insert
Then do an update with the new (generated?) PK-ID into the Self-FK-Field
Then Enable the FK back.
LIke so:
ALTER TABLE [Client] NOCHECK CONSTRAINT [FK_Client_MainClient]
INSERT INTO Client VALUES ...
#ClientID = SCOPE_IDENTITY()
IF #IsMainClient=1
BEGIN
UPDATE [Client]
SET MainClientID = #ClientID
WHERE ClientID = #ClientID
END
ALTER TABLE [Relatie] WITH CHECK CHECK CONSTRAINT [FK_Relatie_Relatie]
How to make a dummy row with both id and parentid equal to -1:
CREATE TABLE mytable(
id int NOT NULL IDENTITY,
parentid int NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (parentid) REFERENCES mytable(id)
) ;
SET IDENTITY_INSERT mytable ON ; <-- this allows you to insert the
INSERT INTO mytable(id, parentid) <-- auto incremented identity field
VALUES (-1, -1);
SET IDENTITY_INSERT mytable OFF ;
SELECT * FROM mytable ;
| id | parentid |
| -1 | -1 |
If you have many data from other tables that you want to transfer into this table, you can set the IDENTITY_INSERT variable to ON, insert the data and then set it to OFF again.
But as others said, it might be better to just remove the NOT NULL constraint from the parentid field.
You can alter the column to allow null then set the fk to the new identity and enable not null again.
This should work, though maybe there is a better way
CREATE TABLE mytable
(
id int IDENTITY(1,1) primary key,
name varchar(50) not null,
parentid int not null
)
go
alter table mytable
add constraint FK_mytable_parentid FOREIGN KEY ( parentid ) references mytable(id)
ALTER TABLE mytable alter column parentid int null;
insert into mytable(name)
select 'test'
update mytable
set parentid = SCOPE_IDENTITY()
where id = SCOPE_IDENTITY()
ALTER TABLE mytable alter column parentid int not null;
select * from mytable
drop table mytable
From what I understood you already have id before inserting and you can't insert it because identity field isn't letting you to.
Like you mentioned in your comment:
in 1 table I have the rows 34 'name1'
34, 35 'name2' 34 (id,name,parentid)
and I want to copy them to other table
First table
create table Table1
(
id int not null primary key,
name varchar(20) not null,
parentId int not null
)
insert Table1
values
(34, 'name1', 34),
(35, 'name2', 34)
Second table:
create table Table2
(
id int identity(1, 1) primary key,
name varchar(20) not null,
parentId int not null foreign key references Table2(id)
)
Copying data from the first table to the second one:
set identity_insert Table2 on
insert Table2(id, name, parentId)
select *
from Table1
set identity_insert Table2 on
[Update]
If the second table already has values then:
alter table Table2
add oldId int null
alter table Table2
alter column parentId int null
go
insert Table2(name, oldId)
select name, id
from Table1
update tt3
set parentId = tt2.id
from Table2 tt3
join Table1 tt1 on
tt1.id = tt3.oldId
join Table2 tt2 on
tt1.parentId = tt2.oldId
alter table Table2
drop column oldId
alter table Table2
alter column parentId int not null
Don't reference an IDENTITY column as a self-referencing foreign key. Use an alternative key of the table instead. I guess you are using IDENTITY as a surrogate key but every table ought to have a natural key as well, so the IDENTITY column shouldn't be the only key of your table.