Question about ON DELETE SET DEFAULT. Does the default value need to exist in the referenced table? - sql

Let's say I have the following table:
CREATE TABLE Products
(
ProdID INT PRIMARY KEY IDENTITY(100,5),
ProdName VARCHAR(20)
)
Then I insert some rows:
INSERT INTO Products VALUES ('Coat Rack') --Will be given a ProdID of 100
INSERT INTO Products VALUES ('Coffee Table') --Will be given a ProdID of 105
Then I create another table called Orders that has a FK constraint:
CREATE TABLE Orders
(
OrderID INT PRIMARY KEY IDENTITY(800,2),
ProductID INT DEFAULT 0,
CONSTRAINT fk_ProdID FOREIGN KEY(ProductID) REFERENCES Products(ProdID) ON DELETE SET DEFAULT
)
Notice the ProductID column has a default value of 0, and a FK constraint that specifies the ON DELETE SET DEFAULT setting.
Then insert one row:
INSERT INTO Orders VALUES (105) --Row references the "Coffee Table" product.
If I try to delete the product "Coffee Table" from the Products table, I get a message saying the product can't be deleted because it's referenced in the Orders table. I understand it is, but I was expecting the FK constraint to simply allow the row to be deleted, then put 0 in the referencing row. 0 is of course the default value for the referencing column (ProductID), and the FK constraint specifies ON DELETE SET DEFAULT.
So with ON DELETE SET DEFAULT, does the default value still need to exist in the referenced table?
Seems a bit odd to me if that's the case. One would probably want to create a "dummy" row in the referenced table, and set the default value equal to whatever ID is used for that dummy row. We'd do this so that if we delete a product, any referencing rows would point to that dummy product instead of an ACTUAL product

According to the documentation
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-table-table-constraint-transact-sql?view=sql-server-ver15
What it does not explain very well indeed is that the default value MUST EXIST in the parent table. If not, you got the error of constraint violation.
A way to show you this based on your example
CREATE TABLE Products
(
ProdID INT PRIMARY KEY IDENTITY(100,5),
ProdName VARCHAR(20)
)
CREATE TABLE Orders
(
OrderID INT PRIMARY KEY IDENTITY(800,2),
ProductID INT DEFAULT 0,
CONSTRAINT fk_ProdID FOREIGN KEY(ProductID) REFERENCES Products(ProdID)
ON DELETE SET DEFAULT
)
INSERT INTO Products VALUES ('Coat Rack') --Will be given a ProdID of 100
INSERT INTO Products VALUES ('Coffee Table') --Will be given a ProdID of 105
SET IDENTITY_INSERT Products ON; -- Enable to insert default dummy product
INSERT INTO Products (ProdID, ProdName) VALUES ( 0 , 'Dummy') -- Insert dummy product
INSERT INTO Orders VALUES (105) --Row references the "Coffee Table" product.
DELETE FROM Orders where ProductID = 105
A complete demo of you code in dbfiddle
db<>fiddle

Related

how to create a Foreign-Key constraint to a subset of the rows of a table?

I have a reference table, say OrderType that collects different types of orders:
CREATE TABLE IF NOT EXISTS OrderType (name VARCHAR);
ALTER TABLE OrderType ADD PRIMARY KEY (name);
INSERT INTO OrderType(name) VALUES('sale-order-type-1');
INSERT INTO OrderType(name) VALUES('sale-order-type-2');
INSERT INTO OrderType(name) VALUES('buy-order-type-1');
INSERT INTO OrderType(name) VALUES('buy-order-type-2');
I wish to create a FK constraint from another table, say SaleInformation, pointing to that table (OrderType). However, I am trying to express that not all rows of OrderType are eligible for the purposes of that FK (it should only be sale-related order types).
I thought about creating a view of table OrderType with just the right kind of rows (view SaleOrderType) and adding a FK constraint to that view, but PostgreSQL balks at that with:
ERROR: referenced relation "SaleOrderType" is not a table
So it seems I am unable to create a FK constraint to a view (why?). Am I only left with the option of creating a redundant table to hold the sale-related order types? The alternative would be to simply allow the FK to point to the original table, but then I am not really expressing the constraint as strictly as I would like to.
I think your schema should be something like this
create table order_nature (
nature_id int primary key,
description text
);
insert into order_nature (nature_id, description)
values (1, 'sale'), (2, 'buy')
;
create table order_type (
type_id int primary key,
description text
);
insert into order_type (type_id, description)
values (1, 'type 1'), (2, 'type 2')
;
create table order_nature_type (
nature_id int references order_nature (nature_id),
type_id int references order_type (type_id),
primary key (nature_id, type_id)
);
insert into order_nature_type (nature_id, type_id)
values (1, 1), (1, 2), (2, 1), (2, 2)
;
create table sale_information (
nature_id int default 1 check (nature_id = 1),
type_id int,
foreign key (nature_id, type_id) references order_nature_type (nature_id, type_id)
);
If the foreign key clause would also accept an expression the sale information could omit the nature_id column
create table sale_information (
type_id int,
foreign key (1, type_id) references order_nature_type (nature_id, type_id)
);
Notice the 1 in the foreign key
You could use an FK to OrderType to ensure referential integrity and a separate CHECK constraint to limit the order types.
If your OrderType values really are that structured then a simple CHECK like this would suffice:
check (c ~ '^sale-order-type-')
where c is order type column in SaleInformation
If the types aren't structured that way in reality, then you could add some sort of type flag to OrderType (say a boolean is_sales column), write a function which uses that flag to determine if an order type is a sales order:
create or replace function is_sales_order_type(text ot) returns boolean as $$
select exists (select 1 from OrderType where name = ot and is_sales);
$$ language sql
and then use that in your CHECK:
check(is_sales_order_type(c))
You don't of course have to use a boolean is_sales flag, you could have more structure than that, is_sales is just for illustrative purposes.

Insert with many Anto Number from Data Table to DataBase

I have 3 data tables to update database
Invoice table, primary key is InvoiceNo
InvoiceProduct table, primary key is InvoiceProductNo and foreign key InvoiceNo
InvoiceProductExp table, primary key is InvoiceProductExpNo and foreign keys are InvoiceNo and InvoiceProductNo
Facts:
One InvoiceNo has many InvoiceProductNo
one InvoiceProductNo has many InvoiceProductExpNo
3 Data Tables data entry would be for example is
Invoice (InvoiceNo,...)
(0001,...)
InvoiceProduct (InvoiceProductNo, InvoiceNo)
(1,0001,...)
(2,0001,...)
InvoiceProductExp (InvoiceProductExpNo,InvoiceProductNo,InvoiceNo)
(1,1,0001,...)
(2,1,0001,...)
(3,2,0001,...)
(4,2,0001,...)
The problem is I liked to use SQL Server generated Identity column for all primary keys of 3 tables
How can I prepare for insert statements?
Insert Into InvoiceProductExp values (auto_number, ?, ?)
How can I get InvoiceProductNo to insert into InvoiceProductExp table since InvoiceProductNo is auto number?
You're looking for SCOPE_IDENTITY()
DECLARE #InvoiceNo INT
DECLARE #InvoiceProductNo INT
INSERT INTO Invoice ([Date])
VALUES (GETDATE())
SELECT #InvoiceNo = SCOPE_IDENTITY()
INSERT INTO InvoiceProduct([InvoiceNo])
VALUES (#InvoiceNo)
SELECT #InvoiceProductNo = SCOPE_IDENTITY()
INSERT INTO InvoiceProductExp ([InvoiceProductNo], [InvoiceNo])
VALUES (#InvoiceProductNo, #InvoiceNo)
Here is SQLFiddle demo
If your primary keys are of identity column type then you don't have to insert a value into the primary key column. The identity column will auto populate when the row is committed to the database.
Does this resolve your question?
Why must manually? You can set your ID as automatically auto increment.
When you create table:
UserID INT IDENTITY(1,1) NOT NULL
sample (create the table)
CREATE TABLE dbo.Tool(
UserID INT IDENTITY NOT NULL PRIMARY KEY,
Name VARCHAR(40) NOT NULL
)
Inserting values
INSERT INTO dbo.Tool(Name) VALUES ('Person 1')
INSERT INTO dbo.Tool(Name) VALUES ('Person 2')

SQL Server, self-referential data, how do I add a constraint for this

Imagine I have the following structure:
DECLARE #Products TABLE (
MemberId INT,
ProductId INT,
GlobalProductId INT,
PRIMARY KEY (MemberId, ProductId));
INSERT INTO #Products VALUES (1, 1, NULL);--this is my "global product"
INSERT INTO #Products VALUES (2, 1, NULL);--this is okay
INSERT INTO #Products VALUES (2, 2, 1);--this is okay
INSERT INTO #Products VALUES (2, 3, 2);--this should fail
SELECT * FROM #Products;
The rule I want to enforce is that MemberId = 1 holds global products and all other MemberIds hold normal products. A set of normal products can be linked to a single global product.
So I want the ability for a Member's Product to be linked to a Global Product, i.e. there would be a foreign key constraint that if the GlobalProductId isn't NULL then there should exist a ProductId that matches the GlobalProductId where the MemberId = 1.
In my example above I have one global product with a ProductId = 1. Then I create three normal products:
the first has no global product;
the second is linked to the single global product I created earlier (then I could link further products to the same global product);
the third should fail as I have linked it to a global product that doesn't exist, i.e. this script will return nothing:
SELECT * FROM #Products WHERE MemberId = 1 AND ProductId = 2;
I can see that the simplest solution would be to create a new table to hold nothing but Global Products. The problem with this approach is that I have a whole set of routines to load, update, delete data from the Product table and a second set of routines to perform calculations, etc. from the same table. If I were to introduce a new "Global Products" table then I would have to duplicate dozens of UDFs to achieve this and my code would become much more complicated.
Add a computed column that's fixed as 1 and then add a foreign key:
CREATE TABLE Products (
MemberId INT,
ProductId INT,
GlobalProductId INT,
PRIMARY KEY (MemberId, ProductId),
GlobalMemberId AS 1 PERSISTED,
FOREIGN KEY (GlobalMemberId,GlobalProductID)
references Products (MemberId,ProductID)
);
INSERT INTO Products VALUES (1, 1, NULL);--this is my "global product"
INSERT INTO Products VALUES (2, 1, NULL);--this is okay
INSERT INTO Products VALUES (2, 2, 1);--this is okay
INSERT INTO Products VALUES (2, 3, 2);--this should fail
SELECT * FROM Products;
This produces these results:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY SAME TABLE constraint "FK__Products__7775B2CE". The conflict occurred in database "abc", table "dbo.Products".
The statement has been terminated.
MemberId ProductId GlobalProductId GlobalMemberId
----------- ----------- --------------- --------------
1 1 NULL 1
2 1 NULL 1
2 2 1 1
Why not just add a CHECK constraint:
ALTER TABLE Products ADD CONSTRAINT CHK_ColumnD_GlobalProductId
CHECK (GlobalProductId IS NULL AND MemberId = 1
OR GlobalProductId IS NOT NULL AND MemberId != 1);
and a FOREIGN KEY:
ALTER TABLE Products ADD CONSTRAINT fk_SelfProducts
FOREIGN KEY (GlobalProductId )
REFERENCES Products (ProductId)

How can I insert into tables with relations?

I have only done databases without relations, but now I need to do something more serious and correct.
Here is my database design:
Kunde = Customer
Vare = Product
Ordre = Order (Read: I want to make an order)
VareGruppe = ehm..type? (Read: Car, chair, closet etc.)
VareOrdre = Product_Orders
Here is my SQL (SQLite) schema:
CREATE TABLE Post (
Postnr INTEGER NOT NULL PRIMARY KEY,
Bynavn VARCHAR(50) NOT NULL
);
CREATE TABLE Kunde (
CPR INTEGER NOT NULL PRIMARY KEY,
Navn VARCHAR(50) NOT NULL,
Tlf INTEGER NOT NULL,
Adresse VARCHAR(50) NOT NULL,
Postnr INTEGER NOT NULL
CONSTRAINT fk_postnr_post REFERENCES Post(Postnr)
);
CREATE TABLE Varegruppe (
VGnr INTEGER PRIMARY KEY,
Typenavn VARCHAR(50) NOT NULL
);
CREATE TABLE Vare (
Vnr INTEGER PRIMARY KEY,
Navn VARCHAR(50) NOT NULL,
Pris DEC NOT NULL,
Beholdning INTEGER NOT NULL,
VGnr INTEGER NOT NULL
CONSTRAINT fk_varegruppevgnr_vgnr REFERENCES Varegruppe(VGnr)
);
CREATE TABLE Ordre (
Onr INTEGER PRIMARY KEY,
CPR INTEGER NOT NULL
CONSTRAINT fk_kundecpr_cpr REFERENCES Kunde(CPR),
Dato DATETIME NOT NULL,
SamletPris DEC NOT NULL
);
CREATE TABLE VareOrdre (
VareOrdreID INTEGER PRIMARY KEY,
Onr INTEGER NOT NULL
CONSTRAINT fk_ordrenr_onr REFERENCES Ordre(Onr),
Vnr INTEGER NOT NULL
CONSTRAINT fk_varevnr_vnr REFERENCES Vare(Vnr),
Antal INTEGER NOT NULL
);
It should work correctly.
But I am confused about Product_Orders.
How do I create an order? For example, 2 products using SQL INSERT INTO?
I can get nothing to work.
So far:
Only when I manually insert products and data into Product_Orders and then add that data to Orders = which makes it complete. Or the other way around (create an order in with 1 SQL, then manually inserting products into Product_orders - 1 SQL for each entry)
You should first create an order and then insert products in the table Product_Orders. This is necessary because you need an actual order with an id to associate it with the table Product_Orders.
You always should create a record in the foreign-key table before being able to create one in your current table. That way you should create a "Post", customer, type, product, order and product_order.
Try this ...
first you have to insert a customer
insert into kunde values(1, 'navn', 1, 'adresse', 1)
then you insert a type
insert into VareGruppe values(1, 'Type1')
then you insert a product
insert into vare values(1, 'product1', '10.0', 1, 1)
then you add an order
insert into ordre values(1, 1, '20090101', '10.0')
then you insert a register to the product_orders table
insert into VareOrdre values (1, 1, 1, 1)
I think this is it. :-)
As the primary keys are autoincrement, don't add them to the insert and specify the columns like this
insert into vare(Nav, Pris, Beholdning, VGnr) values('product1', '10.0', 1, 1)
Use Select ##identity to see the onr value
I think you already have the hang of what needs to happen. But what I think you are getting at is how to ensure data integrity.
This is where Transactions become important.
http://www.sqlteam.com/article/introduction-to-transactions
Is it the SalesPrice (I'm guessing that's what SamletPris means) that's causing the issue? I can see that being a problem here. One common design solution is to have 2 tables: Order and OrderLine. The Order is a header table - it will have the foreign key relationship to the Customer table, and any other 'top level' data. The OrderLine table has FK relationships to the Order table and to the Product table, along with quantity, unit price, etc. that are unique to an order's line item. Now, to get the sales price for an order, you sum the (unit price * quantity) of the OrderLine table for that order. Storing the SalesPrice for a whole order is likely to cause big issues down the line.
A note just in case this is MySQL: If you're using MyISAM, the MySQL server ignores the foreign keys completely. You have to set the engine to InnoDB if you want any kind of integrity actually enforced on the database end instead of just in your logic. This isn't your question but it is something to be aware of.
fbinder got the question right :)

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