Putting pre-update values into another table with a trigger - sql

I've created a table called ProductsAudit that is meant to hold values from the MyGuitarShop Products table after they have been updated with a trigger, but I can't seem to get it to work. I'm doing this in SQL Server.
So what I need to happen is when Products is updated, it stores the old value in to the ProductsAudit table.
CREATE TABLE ProductsAudit
(
AuditID int NOT NULL,
CategoryID int,
ProductCode varchar(10) NOT NULL,
ProductName varchar(255) NOT NULL,
ListPrice money NOT NULL,
DiscountPercent money NOT NULL,
DateUpdated datetime
PRIMARY KEY (AuditID)
FOREIGN KEY (CategoryID) REFERENCES Categories(CategoryID)
)
CREATE TRIGGER Products_UPDATE2
ON Products
AFTER UPDATE
AS
BEGIN;
ROLLBACK TRAN
INSERT INTO ProductsAudit
SELECT CategoryID, ProductCode, ProductName, ListPrice, DiscountPercent
FROM Deleted
WHERE CategoryID = (SELECT CategoryID FROM Inserted);
PRINT 'Old data sent to ProductsAudit'
END;
UPDATE Products
SET ListPrice = 79.43
WHERE ProductID = 3;

CREATE TABLE ProductsAudit
(
AuditID int NOT NULL IDENTITY(1,1),
CategoryID int,
ProductCode varchar(10) NOT NULL,
ProductName varchar(255) NOT NULL,
ListPrice money NOT NULL,
DiscountPercent money NOT NULL,
DateUpdated datetime DEFAULT (GETDATE()),
PRIMARY KEY (AuditID),
FOREIGN KEY (CategoryID) REFERENCES Categories(CategoryID)
)
CREATE TRIGGER Products_UPDATE2
ON Products
AFTER UPDATE
AS
BEGIN;
INSERT INTO ProductsAudit (CategoryID, ProductCode, ProductName, ListPrice, DiscountPercent, DateUpdated)
SELECT
d.CategoryID
,d.ProductCode
,d.ProductName
,d.ListPrice
,d.DiscountPercent
,DateUpdated = GETDATE() -- may consider adding default to the table to handle this part
FROM
deleted d
LEFT JOIN inserted i
ON d.CategoryID= i.CategoryID --is this the Product PrimaryKey Column?
WHERE
ISNULL(d.CategoryID,-1) <> ISNULL(i.CategoryID,-1)
OR ISNULL(d.ProductCode,'') <> ISNULL(i.ProductCode,'')
OR ISNULL(d.ProductName,'') <> ISNULL(i.ProductName,'')
OR ISNULL(d.ListPrice,-1) <> ISNULL(i.ListPrice,-1)
OR ISNULL(d.DiscountPercent,-1000) <> ISNULL(i.DiscountPercent,-1000)
PRINT 'Old data sent to ProductsAudit'
END;
UPDATE Products
SET ListPrice = 79.43
WHERE ProductID = 3;
So you seem to have a few issues going on, and I will see if I can walk you through them and 1 way of fixing. First, you say you want ProductAudit to hold values "AFTER" they have been updated but your code and the term Audit suggests you want to store the OLD values (deleted) into an Audit Table not the new values so that is the assumption I worked from.
2 main issues with the code you showed.
ROLLBACK TRAN - this is sort of like an undo button that would roll back all statements in the transactions which could be more than just the update you are wanting to track. This would mean your data wouldn't actually get updated.
(SELECT CategoryId FROM Inserted) - Triggers are evaluated on a set based operation NOT a scalar value so the table inserted would have more than 1 CateogryId if more than 1 row is updated. So it would have picked the last row in the data set and only that row would have been inserted into your Audit Table.
Note you can use the deleted and the inserted tables to get to the information you want. technically you only need the deleted if you want to store a record no matter what. I am showing how to use both to detect if there was actually a change to the record and then only log if there was one.

Related

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

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

SQL sever constraint data

I have 2 table Product and Supplier
create table Product(
ProductCode int not null primary key,
Name varchar(50) not null ,
PurchasePrice numeric(20,3) not null,
SellPrice numeric(20,3) not null ,
Type varchar(50) not null,
SupplierCode int not null
)
go
create table Supplier(
SupplierCode int not null primary key,
SupplierName varchar(50) not null ,
Address varchar(50) not null
)
I want : A product of Samsung must be television, mobile or tablet.
Help me.
database enter image description here
I want "SupplierCode<>4" because Supplier supply 'food'
You can't achieve it this way do something like this in check constraint because value depends on different tables.
The most straightforward way would be to create a trigger. This one is after insert. It just deletes rows that are not acceptable. You can experiment and make it instead of insert instead
CREATE TRIGGER insertProduct
ON Sales.Product
AFTER INSERT, UPDATE
AS
delete from inserted a
join Supplier b on a.SupplierCode = b.SupplierCode
where
(b.Supplier = 'Samsung' and a.Type not in ('phone', whatever you need...))
or (b.Supplier = 'different' and a.Type not in (...))
--here you put any additional logic for different suppliers
if ##ROWCOUNT > 0
RAISERROR ('Couldn't insert into table', 16, 10);
GO
Depending on your case what you can also do is make inserting to the tables available only via stored procedures. Then put the checks in the stored procedure instead.
If I understand you correct, you want to define for each suppliers what kind of products are allowed in your tables.
I think you need a table that defines what producttypes are allowed for what Suppliers, and than you can force this in a trigger or in your client.
First you need a table to define the kind of products
table ProductType (ID, Name)
this holds info like
(1, 'Television'), (2, 'Tablet'), (3, 'Mobi'), (4, 'Food'), and so on...
Then replace the field Type by ProductTypeID in your Product table
Now you can have a table that defines what product types each supplier may have
table AllowedProducts (ID, SupplierCode, ProductTypeID)
and finally you can check this in your client, or in a trigger if you want to keep this rule in your database
When inserting data you can check if the ProductTypeID of the selected Product is present in table AllowedProducts for the selected ´Supplier` and if not reject the insert.

Why do I keep getting this error message when I try to insert something into a table

Here is the question that I have to answer:
Create a trigger named Products_INSERT that inserts the current date for the DateAdded column of the Products table if the value for that column is null.
Test this trigger with an appropriate INSERT statement.
Here is the code that I have:
CREATE TRIGGER Products_INSERT
ON Products
AFTER INSERT
AS
UPDATE Products
SET DateAdded = GETDATE()
WHERE DateAdded IS NULL OR
DateAdded IN (SELECT DateAdded FROM inserted);
Here is my insert statement:
INSERT INTO Products
VALUES (4, 'LK-5300', 'Likeable Keyboard 5300',
'This keyboard is so cool, you just might flip!',
699.99, 30.00, NULL)
And here is the error I keep getting:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "FK__Products__Catego__145C0A3F". The conflict occurred in database "MyGuitarShop", table "dbo.Categories", column 'CategoryID'.
The statement has been terminated.
I know that the error has something to do with the foreign key but I'm not entirely sure. Any help would be appreciated.
EDIT:
CREATE TABLE Products (
ProductID INT PRIMARY KEY IDENTITY,
CategoryID INT REFERENCES Categories (CategoryID),
ProductCode VARCHAR(10) NOT NULL UNIQUE,
ProductName VARCHAR(255) NOT NULL,
Description TEXT NOT NULL,
ListPrice MONEY NOT NULL,
DiscountPercent MONEY NOT NULL DEFAULT 0.00,
DateAdded DATETIME DEFAULT NULL
);
Here is the Products table
You don't need to include the productID because its auto generated
Thank's #Turophile here a what I ended up doing to my INSERT statement after I read your post:
INSERT INTO Products (CategoryID, ProductCode, ProductName, Description, ListPrice,DiscountPercent, DateAdded)
VALUES (44444, 'LK-5300', 'Likeable Keyboard 5300',
'This keyboard is so cool, you just might flip!',
699.99, 30.00, NULL)
Your INSERT should be:
INSERT INTO Products
( CategoryID,
ProductCode,
ProductName,
Description,
ListPrice,
DiscountPercent,
DateAdded
)
VALUES (4, 'LK-5300', 'Likeable Keyboard 5300',
'This keyboard is so cool, you just might flip!',
699.99, 30.00, NULL)
Your INSERT was missing a value to go into the ProductID column, since it is auto-generated because it is defined as IDENTITY but to get that to work, you need to name the columns, leaving out ProductID.

How I can get an auto incremented value

I have here a table that corresponds to the orders of the customers. I use AUTO_INCREMENT to determine the ID of the order. I have this SQL code to the orders table:
CREATE TABLE IF NOT EXISTS `orders` (
`order_id` int(11) NOT NULL AUTO_INCREMENT,
`customer_id` int(11) NOT NULL,
`customer_name` varchar(500) NOT NULL,
`order_total_price` decimal(20, 2) NOT NULL,
`order_date` varchar(100) NOT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB
What I need is to insert each of the products of that order in another table with a Foreign Key order_id to specify what order that products belongs to. The SQL code for the purchased_products table is:
CREATE TABLE IF NOT EXISTS `purchased_products` (
`order_id` int (11) NOT NULL,
FOREIGN KEY (`order_id`) REFERENCES orders(`order_id`),
`product_name` varchar(500) NOT NULL,
`product_price` decimal(20, 2) NOT NULL,
`product_quantity` int(11) NOT NULL,
PRIMARY KEY (`order_id`)
)
When the user buy something, I use this to insert the data in the orders table:
INSERT INTO orders (customer_id, customer_name, order_total_price, order_date)
VALUES ('{$customer_id}', '{$customer['customer_name']}', '{$order_total_price}', '{$order_date}')";
And here is my problem. I need to insert in the purchased_products table the products with the Order ID generated:
INSERT INTO purchased_products (order_id, product_name, product_price, product_quantity)
VALUES ('*/The ID of the order need to goes here*/', '{$product['product_name']}', '{$product['product_price']}', '{$product['quantity']}')";
This is giving me a headache. I'm not really knowing how to do it. This should be done by a different way? How do I associate the order ID to the products belonging to it?
use function last_insert_id(). it will give you value that was auto-incremented as last one before call to it.
You can get the get the last inserted primary key value by using ##IDENTITY
Here's the MSDN article: https://msdn.microsoft.com/en-us/library/ms187342.aspx
USE AdventureWorks2012;
GO
--Display the value of LocationID in the last row in the table.
SELECT MAX(LocationID) FROM Production.Location;
GO
INSERT INTO Production.Location (Name, CostRate, Availability, ModifiedDate)
VALUES ('Damaged Goods', 5, 2.5, GETDATE());
GO
SELECT ##IDENTITY AS 'Identity';
GO
--Display the value of LocationID of the newly inserted row.
SELECT MAX(LocationID) FROM Production.Location;
GO
I would also recommend wrapping the statement in a TRANSACTION so that if any errors occur you can rollback.
As others have commented it depends on the RDBMS. In Oracle you typically use sequences. You create and store the sequence on the database and can use it on an INSERT by doing sequencename.nextval().
Sequences let you control starting values, increment/decrement size, caching and a lot more.
I did it by using PDO lastInsertId() to get the ID of last inserted order:
$sql = "INSERT INTO orders (customer_id, customer_name, order_total_price, order_date)
VALUES ('{$customer_id}', '{$customer['customer_name']}', '{$order_total_price}', '{$order_date}')";
$query = $connection->prepare($sql);
$query->execute();
$respective_order_id = $connection->lastInsertId();
And then:
INSERT INTO purchased_products (order_id, product_name, product_price, product_quantity)
VALUES ('{$respective_order_id}', '{$product['product_name']}', '{$product['product_price']}', '{$product['quantity']}')";
Thanks for all who tried to help! They put me in the right way!
you can use SCOPE_IDENTITY() to retrieve the last identity you inserted within the current sql session.
here is another question with a great description of all the differences:
identity scope Question

SQL Trigger not working correctly

here are the 2 tables i have, i want to implement an trigger that customer cannot have more than 5 accounts from a one bank, but can have more than 5 in total.
CREATE TABLE ACCOUNT(
ACCOUNT_NO VARCHAR(20) NOT NULL,
BALANCE REAL,
BANK_CODE VARCHAR(20),
BRANCH_NO VARCHAR(25),
ACCOUNT_CODE VARCHAR(20),
PRIMARY KEY(ACCOUNT_NO),
);
CREATE TABLE ACCOUNT_CUSTOMER(
CUS_NO VARCHAR(20) NOT NULL,
ACCOUNT_NO VARCHAR(20) NOT NULL,
PRIMARY KEY(CUS_NO,ACCOUNT_NO),
FOREIGN KEY(ACCOUNT_NO) REFERENCES ACCOUNT(ACCOUNT_NO),
);
heres the trigger i wrote but i can't create more than 5 accounts in total because it checks for all the accounts in all the banks rather than a single bank.
CREATE TRIGGER TRIGGER1
ON ACCOUNT_CUSTOMER
FOR INSERT,UPDATE
AS BEGIN
DECLARE #COUNT INT
DECLARE #CUS_NO VARCHAR(20)
SELECT #COUNT=COUNT(AC.ACCOUNT_NO)
FROM INSERTED I,ACCOUNT_CUSTOMER AC
WHERE I.CUS_NO=AC.CUS_NO
GROUP BY(AC.CUS_NO)
IF #COUNT>5
ROLLBACK TRANSACTION
END
THE PROBLEM IS WITHIN THE GROUPBY FUNCTION AS I GUESS.
this is easy to implement with constraints:
CREATE TABLE ACCOUNT(
ACCOUNT_NO VARCHAR(20) NOT NULL,
BALANCE REAL,
BANK_CODE VARCHAR(20),
BRANCH_NO VARCHAR(25),
ACCOUNT_CODE VARCHAR(20),
PRIMARY KEY(ACCOUNT_NO),
UNIQUE(ACCOUNT_NO,BANK_CODE)
);
CREATE TABLE ACCOUNT_CUSTOMER(
CUS_NO VARCHAR(20) NOT NULL,
ACCOUNT_NO VARCHAR(20) NOT NULL,
BANK_CODE VARCHAR(20),
NUMBER_FOR_BANK INT NOT NULL CHECK(NUMBER_FOR_BANK BETWEEN 1 AND 5),
PRIMARY KEY(CUS_NO,ACCOUNT_NO),
UNIQUE(CUS_NO,BANK_CODE,NUMBER_FOR_BANK),
FOREIGN KEY(ACCOUNT_NO, BANK_CODE) REFERENCES ACCOUNT(ACCOUNT_NO, BANK_CODE),
);
Edit: sometimes triggers do not fire. Only trusted constraints 100% guarantee data integrity.
To insert, I would use Numbers table:
INSERT INTO ACCOUNT_CUSTOMER(
CUS_NO,
ACCOUNT_NO,
BANK_CODE,
NUMBER_FOR_BANK
)
SELECT TOP 1 #CUS_NO,
#ACCOUNT_NO,
#BANK_CODE,
NUMBER
FROM dbo.Numbers WHERE NUMBER BETWEEN 1 AND 5
AND NOT EXISTS(SELECT * FROM ACCOUNT_CUSTOMER WHERE CUS_NO=#CUS_NO AND BANK_CODE=#BANK_CODE)
I would use a trigger to prohibit modifications of BANK_CODE.
I would try something like this:
Replace this part of your trigger
SELECT #COUNT=COUNT(AC.ACCOUNT_NO)
FROM INSERTED I,ACCOUNT_CUSTOMER AC
WHERE I.CUS_NO=AC.CUS_NO
GROUP BY(AC.CUS_NO)
IF #COUNT>5
ROLLBACK TRANSACTION
with this:
IF EXISTS (
SELECT COUNT(a.ACCOUNT_NO)
FROM INSERTED i
JOIN ACCOUNT a ON i.ACCOUNT_NO = a.ACCOUNT_NO
JOIN ACCOUNT_CUSTOMER c ON i.CUS_NO = c.CUS_NO
GROUP BY c.CUS_NO, a.BANK_CODE
HAVING COUNT(a.ACCOUNT_NO) >= 5
)
ROLLBACK TRANSACTION
Also consider that the INSERTED table may have multiple records in it. If those records are for more than one customer and any of the customers causes this trigger to rollback the transaction, then the updates for those customers that did not violate your rule will not be applied. This may never happen (if your application never updates records for more than one customer at a time), or may be the intended behavior.
Try this instead of the current query in your trigger. I think that this might work.
My syntax might be a bit off but you get the general idea.
SELECT #COUNT=MAX(COUNT(AC.ACCOUNT_NO))
FROM INSERTED I
INNER JOIN ACCOUNT_CUSTOMER AC ON I.CUS_NO=AC.CUS_NO
INNER JOIN ACCOUNT A ON AC.ACCOUNT_NO = A.ACCOUNT_NO
GROUP BY(AC.CUS_NO, A.BANK_CODE)
The trouble with your query is that you are only searching by unique customer identifier.
Your query must search a count of a unique customer AND bank identifier together. I'll leave the exact query to you, but here's what you want in pseudocode:
SELECT COUNT(customer_id)
FROM table_name
WHERE customer_id = customer_id_to_validate
AND bank_id = bank_id_to_validate
This will return how many times a customer + bank combination exist. That's the limit you want.
Thanks for the answers, after going through all, i came up with this solution. I inserted a nested query taht will give me the bankcode and by that code i get the count
CREATE TRIGGER TRIGGER1
ON ACCOUNT_CUSTOMER
FOR INSERT,UPDATE
AS BEGIN
DECLARE #COUNT INT
DECLARE #CUS_NO VARCHAR(20)
SELECT #COUNT=COUNT(*)
FROM ACCOUNT_CUSTOMER AC, ACCOUNT A
WHERE A.ACCOUNT_NO=AC.ACCOUNT_NO AND A.BANK_CODE=
(SELECT A.BANK_CODE
FROM DIT09C_0293_ACCOUNT A, INSERTED I
WHERE A.ACCOUNT_NO=I.ACCOUNT_NO
)
IF #COUNT>5
ROLLBACK TRANSACTION
END