How to keep track of historical changes to reference table? - sql

This is a very common thing in web applications. If I have a user table and I want to keep track of all the changes made to the user table, I can use a database insert and update triggers to save those changes in the user_history table.
But what if I have user_products table, where I have user_id , product_id, cost. When I add a user in the system lets say I have two products associated with that user. So my user_products table will have two rows for that user.
user_id product_id cost
1 10 1000
2 20 2000
Now if I go to the edit user page and delete product 1 , add product 3 , change cost for product 2 from 2000 to 3000.
so normally I delete all records for user_id 1 from user_product table and then do a new insert for the new products.
So its not a regular update but a delete and then insert. Hence I am not able to keep track of history changes.
Ideally I would want to know that I deleted product 1 , added product 3 , changed cost for product 2 from 2000 to 3000.
EDIT 1:-
I am not doing a update. I am doing a delete and then insert. So I am deleting record with product id 2 and cost 2000. And then again inserting record with prod id 2 but with cost 3000. So technically its delete and insert but logically only cost is changed from 2000 to 3000. If i check while executing both queries it will say i deleted product with id 2 and and then added product with id 2 which are same. But I want to be able to see that the cost has chnaged from 2000 to 3000

One option would be to create a user_product_history table that is populated by triggers on user_product and then define a trigger that transforms the old 'delete' row in the history table into an update if the row is subsequently inserted.
CREATE TABLE user_product_history (
user_id number,
product_id number,
cost number,
operation_type varchar2(1),
operation_date date
);
CREATE TRIGGER trg_user_product_history
AFTER INSERT OR UPDATE OR DELETE ON user_product
FOR EACH ROW
DECLARE
l_cnt integer;
BEGIN
IF( deleting )
THEN
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :old.user_id, :old.product_id, :old.cost, 'D', sysdate );
ELSIF( updating )
THEN
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :new.user_id, :new.product_id, :new.cost, 'U', sysdate );
ELSIF( inserting )
THEN
select count(*)
into l_cnt
from user_product_history
where operation_type = 'D'
and user_id = :new.user_id
and product_id = :new.product_id;
if( l_cnt > 0 )
then
update user_product_history
set operation_type = 'U',
operation_date = sysdate,
cost = :new.cost
where operation_type = 'D'
and user_id = :new.user_id
and product_id = :new.product_id;
else
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :new.user_id, :new.product_id, :new.cost, 'I', sysdate );
end if;
END IF;
END;
From an efficiency standpoint, however, doing deletes and inserts rather than updates is going to mean that you're putting far more load on your database than is necessary. You'll do substantially more I/O than necessary. And you'll end up with much more complicated code for handling changes. You'll almost certainly be better served figuring out what has changed and then just updating those rows.

I don't know if there is a "standard" way or simple one but for my experience, you have to do it yourself...
Of course, you don't need to code a specific method for each table but a generic method that handles all update SQL, something smart according your tables structure.
Example:
You can define a history table that will have:
id - table name - table record id - list of fields to update - list of values updated BEFORE - list of values updated AFTER
Then, into your code, you should have an abstract class that will handle the SQL queries: on update, you call the method that will parse the SQL query and insert a record into this table, even you have to query a SELECT to retrieve the data BEFORE the updates (the overhead is minimum because the SELECT uses only few resources and you can use the DB server cache).
Finally, when you display the information relative to a record, let's say the user, you can add the code to display the history on this table AND for this user id.
Hope it will help you.

Related

How to use trigger to update column for newly inserted tuple

I have a table BillRecords
bill_id buy_amt sell_amt profit
----------------------------------------------------
0 200 300 NULL
1 1000 1200 NULL
Let's say I want to insert following record
INSERT INTO BillRecords(bill_id, buy_amt, sell_amt)
VALUES (2, 2000, 2500)
I'm expecting that as soon as I insert record, trigger should only update profit column for newly inserted record (i.e. WHERE bill_id = 2) and not for all the rows in profit column. What should I do?
Note: I'm using SQL Server
No need for a trigger to do what you want. It is much simpler, and more efficient, to use a computed column:
create table billrecords (
bill_id int primary key,
buy_amt int,
sell_amt int,
profit as (buy_amt - sell_amt)
);
Or if you want to add it to an already-existing table:
alter table billrecords
add profit as (buy_amt - sell_amt);
This gives you an always up-to-date value, that you can access just like any other column. For better performance, you can use option persisted, so the value column is physically stored, and recomputed only when needed instead of every time it is accessed.
Since you only want to update the profit amount of the transaction having bill_id = 2, you can use the below after trigger this in combination with a derived column "profit" which fetches the profit amount.
CREATE TRIGGER trigger_name
ON billrecords
After INSERT
AS
IF EXISTS (SELECT * from inserted)
begin
update m
set m.profit = m.sell_amt - m.bill_amt, m.bill_amt = null, m.sell_amt = null
from billrecords m join inserted i
on i.bill_id = m.bill_id
where i.bill_id = 2
end;

Add data to trigger from the last insert

I am faced with the following situation:
I created a trigger which reacts on insert to the third table. When I insert any data (for example 1 1 2), last number should be subtracted from the Amount of stock column from cell, which has necessary ID Product (as it's shown on picture). But how can I understand which row was the last added? I thought firstly to do it by select, but it seems impossible. And now I think that it's possible to do it with the help of cursor, but it doesn't seem as the best variant. Is there a better variant how can I do it?
Here's my code of trigger, but it only subtracts 1 from the 1st product each time, unfortunately:
CREATE TRIGGER AmountInsert ON Amount
AFTER INSERT
AS
BEGIN
UPDATE Product
SET Amount_On_Stock =
(SELECT Amount_On_Stock FROM Product
WHERE ID_Product = 1) - 1
WHERE ID_Product = 1
END
The first thing you need to understand is that in a trigger in SQL Server you are provided with an inserted pseudo-table and a deleted pseudo-table. You use these tables to determine what changes have occurred.
I think the following trigger accomplishes what you are looking for - the comments explain the logic.
CREATE TRIGGER dbo.AmountInsert ON dbo.Amount
AFTER INSERT
AS
BEGIN
set nocount on;
update P set
-- Adjust the stock level by the amount of the latest insert
Amount_On_Stock = coalesce(Amount_On_Stock) - I.Amount
from dbo.Product P
inner join (
-- We need to group by ID_Product in case the same product appears in the insert multiple times
select ID_Product, sum(Amount) Amount
from Inserted
group by ID_Product
-- No need to update is net change is zero
having sum(Amount) <> 0
) I
on I.ID_Product = P.ID_Product;
END

Inserting multiple records in database table using PK from another table

I have DB2 table "organization" which holds organizations data including the following columns
organization_id (PK), name, description
Some organizations are deleted so lot of "organization_id" (i.e. rows) doesn't exist anymore so it is not continuous like 1,2,3,4,5... but more like 1, 2, 5, 7, 11,12,21....
Then there is another table "title" with some other data, and there is organization_id from organization table in it as FK.
Now there is some data which I have to insert for all organizations, some title it is going to be shown for all of them in web app.
In total there is approximately 3000 records to be added.
If I would do it one by one it would look like this:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
VALUES
(
'This is new title',
XXXX,
CURRENT TIMESTAMP,
1,
1,
1
);
where XXXX represent "organization_id" which I should get from table "organization" so that insert do it only for existing organization_id.
So only "organization_id" is changing matching to "organization_id" from table "organization".
What would be best way to do it?
I checked several similar qustions but none of them seems to be equal to this?
SQL Server 2008 Insert with WHILE LOOP
While loop answer interates over continuous IDs, other answer also assumes that ID is autoincremented.
Same here:
How to use a SQL for loop to insert rows into database?
Not sure about this one (as question itself is not quite clear)
Inserting a multiple records in a table with while loop
Any advice on this? How should I do it?
If you seriously want a row for every organization record in Title with the exact same data something like this should work:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
;
you shouldn't need the column aliases in the select but I am including for readability and good measure.
https://www.ibm.com/support/knowledgecenter/ssw_i5_54/sqlp/rbafymultrow.htm
and for good measure in case you process errors out or whatever... you can also do something like this to only insert a record in title if that organization_id and title does not exist.
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
LEFT JOIN Title t
ON o.organization_id = t.organization_id
AND t.name = 'This is new title'
WHERE
t.organization_id IS NULL
;

Why can't I use SELECT ... FOR UPDATE with aggregate functions?

I have an application where I find a Sum() of a database column for a set of records and later use that sum in a separate query, similar to the following (made up tables, but the idea is the same):
SELECT Sum(cost)
INTO v_cost_total
FROM materials
WHERE material_id >=0
AND material_id <= 10;
[a little bit of interim work]
SELECT material_id, cost/v_cost_total
INTO v_material_id_collection, v_pct_collection
FROM materials
WHERE material_id >=0
AND material_id <= 10
FOR UPDATE;
However, in theory someone could update the cost column on the materials table between the two queries, in which case the calculated percents will be off.
Ideally, I would just use a FOR UPDATE clause on the first query, but when I try that, I get an error:
ORA-01786: FOR UPDATE of this query expression is not allowed
Now, the work-around isn't the problem - just do an extra query to lock the rows before finding the Sum(), but that query would serve no other purpose than locking the tables. While this particular example is not time consuming, the extra query could cause a performance hit in certain situations, and it's not as clean, so I'd like to avoid having to do that.
Does anyone know of a particular reason why this is not allowed? In my head, the FOR UPDATE clause should just lock the rows that match the WHERE clause - I don't see why it matters what we are doing with those rows.
EDIT: It looks like SELECT ... FOR UPDATE can be used with analytic functions, as suggested by David Aldridge below. Here's the test script I used to prove this works.
SET serveroutput ON;
CREATE TABLE materials (
material_id NUMBER(10,0),
cost NUMBER(10,2)
);
ALTER TABLE materials ADD PRIMARY KEY (material_id);
INSERT INTO materials VALUES (1,10);
INSERT INTO materials VALUES (2,30);
INSERT INTO materials VALUES (3,90);
<<LOCAL>>
DECLARE
l_material_id materials.material_id%TYPE;
l_cost materials.cost%TYPE;
l_total_cost materials.cost%TYPE;
CURSOR test IS
SELECT material_id,
cost,
Sum(cost) OVER () total_cost
FROM materials
WHERE material_id BETWEEN 1 AND 3
FOR UPDATE OF cost;
BEGIN
OPEN test;
FETCH test INTO l_material_id, l_cost, l_total_cost;
Dbms_Output.put_line(l_material_id||' '||l_cost||' '||l_total_cost);
FETCH test INTO l_material_id, l_cost, l_total_cost;
Dbms_Output.put_line(l_material_id||' '||l_cost||' '||l_total_cost);
FETCH test INTO l_material_id, l_cost, l_total_cost;
Dbms_Output.put_line(l_material_id||' '||l_cost||' '||l_total_cost);
END LOCAL;
/
Which gives the output:
1 10 130
2 30 130
3 90 130
The syntax select . . . for update locks records in a table to prepare for an update. When you do an aggregation, the result set no longer refers to the original rows.
In other words, there are no records in the database to update. There is just a temporary result set.
You might try something like:
<<LOCAL>>
declare
material_id materials.material_id%Type;
cost materials.cost%Type;
total_cost materials.cost%Type;
begin
select material_id,
cost,
sum(cost) over () total_cost
into local.material_id,
local.cost,
local.total_cost
from materials
where material_id between 1 and 3
for update of cost;
...
end local;
The first row gives you the total cost, but it selects all the rows and in theory they could be locked.
I don't know if this is allowed, mind you -- be interesting to hear whether it is.
For example, there is product table with id, name and stock as shown below.
product table:
id
name
stock
1
Apple
3
2
Orange
5
3
Lemon
8
Then, both 2 queries below can run sum() and SELECT FOR UPDATE together:
SELECT sum(stock) FROM (SELECT * FROM product FOR UPDATE) AS result;
WITH result AS (SELECT * FROM product FOR UPDATE) SELECT sum(stock) FROM result;
Output:
sum
-----
16
(1 row)
For that, you can use the WITH command.
Exemple:
WITH result AS (
-- your select
) SELECT * FROM result GROUP BY material_id;
Is your problem "However, in theory someone could update the cost column on the materials table between the two queries, in which case the calculated percents will be off."?
In that case , probably you can simply use a inner query as:
SELECT material_id, cost/(SELECT Sum(cost)
FROM materials
WHERE material_id >=0
AND material_id <= 10)
INTO v_material_id_collection, v_pct_collection
FROM materials
WHERE material_id >=0
AND material_id <= 10;
Why do you want to lock a table? Other applications might fail if they try to update that table during that time right?

Oracle Triggers Update based on orderlines fulfilled how to avoid mutation error

for a typical products & shipping Database I am exploring the best way to run a trigger that:
When an order line is set to 'Complete', a trigger is ran that:
Looks for any other order lines for that order.
If all other order lines for that order are also 'Complete'
Update the order header table to complete.
For clatiry: The order header table would store the overall oder total, and the orderLines table stores each product of the order.
SO far, the trigger is written as such:
CREATE OR REPLACE TRIGGER orderComplete
after update ON orderline
for each row
WHEN (new.orderline_fulfilled = 'Y')
DECLARE count NUMBER := 5;
ordersNotDone NUMBER;
BEGIN
SELECT COUNT(Orderline_fulfilled) INTO ordersNotDone
FROM orderHeader
JOIN orderline ON
orderHeader.Order_id = orderLine.Orderline_order
WHERE Order_id = :old.orderline_order
AND orderline_fulfilled = 'Y';
IF ordersNotDone = 0
THEN
UPDATE orderHeader
SET completed = SYSDATE
WHERE orderId = :old.orderline_order;
ENDIF;
END;
This above causes the mutation error, when updating the orderline row.
Enforcing integrity with a trigger is inherently problematic because the RDBMS read consistency mode allows multiple changes simultaneously that cannot see each others' result.
A better solution might be to avoid denormalising the data, and rely on detecting the presence of an incomplete order line to identify incomplete orders. As this would be the minority of cases it can be optimised with a function-based index along the lines of:
create index my_index on orderline(case orderline_complete when 'NO' then orderid else null end)
This will index only the values of orderline where orderline_complete is 'NO', so if there are only 100 such rows in the table then the index will only contain 100 entries.
Identifying incomplete orders is then a matter only of a full or fast full index scan of a very compact index with a query:
select distinct
case orderline_complete when 'NO' then orderid else null end orderid
from
orderline
where
case orderline_complete when 'NO' then orderid else null end is not null;
If you are using 11g then check about compound triggers, example: http://www.toadworld.com/KNOWLEDGE/KnowledgeXpertforOracle/tabid/648/TopicID/TRGC1/Default.aspx
The simplest answer is to use a slightly different type of trigger which is triggered not after a row is updated but after the table is updated. This will not suffer from this problem.
So do something like:
CREATE or REPLACE TRIGGER trigger_name
AFTER INSERT ON orderline --note no for each row
BEGIN
--loop over all orders which contain no unfulfilled orders
FOR lrec IN (SELECT order_id FROM orderline group by order_id where not exists (select 1 from orderline where orderline_fulfilled = 'Y'))
LOOP
-- do stuff to order id because this are complete
END LOOP;
END;
So here we may have completed multiple orders on the insert so the trigger needs to cope with this. Sorry I do not have an oracle instance to play with at home. Hope this helps