I have the following tables: replies and messages. I would like to imitate Facebook's behavior: when a message is deleted, all related replies are deleted as well. My tables look like this:
REPLIES
messageId replyId
6b61d107-dff3-4374-a3a2-75ac7478a2f2 865c873d-0210-482a-b8bd-371c4f07f0cf
MESSAGES
id body
865c873d-0210-482a-b8bd-371c4f07f0cf this is the reply
6b61d107-dff3-4374-a3a2-75ac7478a2f2 this is the message
I have created one first trigger, which works, which deletes the related lines in Replies when a message is deleted. I now would like to create another trigger which would delete the related message every time a line in Replies is deleted. Right now the replies become messages of their own, which doesn't make sense. Here's the second trigger:
CREATE TRIGGER TRG_DEL_MESSAGES
ON Replies
FOR DELETE
AS
DELETE FROM Messages WHERE id = (SELECT replyId FROM DELETED)
Which brings the following error when I try to delete something:
Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).
Apparently there's an infinite loop going and I think it is because the DELETED table is filled by data from the first trigger (which provoked the second). But really I'm not sure and I'd appreciate some help. Thanks!
Will something like this work for you? Remove the recursion by just using a single trigger on Messages, that calculates all of the related messages up front, so you should get at most one nested call? It deletes all the related messages from both messages and replies
CREATE TRIGGER TRG_DEL_REPLIES
ON [Messages]
FOR DELETE
AS
BEGIN
DECLARE #Related TABLE (MessageId uniqueidentifier)
--get all related messages so that we don't recurse
BEGIN
WITH AllReplies (MessageId)
AS
(
--Anchor
SELECT D.MessageId
FROM Deleted D
UNION ALL
--Recurse
SELECT R.ReplyId
FROM AllReplies AR
JOIN Replies R
ON AR.MessageId = R.MessageId
)
INSERT INTO #Related
SELECT *
FROM AllReplies
END
--delete the replies
DELETE R
FROM Replies R
JOIN #Related REL
ON R.MessageId = REL.MessageId
--delete the messages
DELETE M
FROM [Messages] M
JOIN #Related REL
ON REL.MessageId = M.MessageId
LEFT
JOIN DELETED D
ON REL.MessageId = D.MessageId
WHERE D.MessageId IS NULL
END
To translate this into a stored proc as you wanted, i would do this, rather than a loop that does multiple separate deletes:
CREATE PROCEDURE DeleteMessageWithReplies(#messageId uniqueidentifier)
AS
BEGIN
DECLARE #Related TABLE (MessageId uniqueidentifier)
--get all related messages
BEGIN
WITH AllReplies (MessageId)
AS
(
--Anchor
SELECT #messageId
UNION ALL
--Recurse
SELECT R.ReplyId
FROM AllReplies AR
JOIN Replies R
ON AR.MessageId = R.MessageId
)
INSERT INTO #Related
SELECT *
FROM AllReplies
END
--delete the replies
DELETE R
FROM Replies R
JOIN #Related REL
ON R.MessageId = REL.MessageId
--delete the messages that haven't already been deleted
DELETE M
FROM [Messages] M
JOIN #Related REL
ON REL.MessageId = M.MessageId
END
Thanks for your help Fergus, I appreciate it. However, as #Ben pointed, a stored procedure is easier and simpler to code. This is what I've just written, it could probably be improved but at least it works.
EXEC('CREATE PROCEDURE deleteMessageWithReplies(#messageId uniqueidentifier)
AS
BEGIN
DECLARE #repliesCount int
SELECT #repliesCount = (SELECT COUNT(*) FROM Replies WHERE messageId=#messageId)
DECLARE #cpt int
SET #cpt = 0
DELETE FROM Messages WHERE id = #messageId
WHILE #cpt < #repliesCount
BEGIN
DECLARE #replyId uniqueidentifier
SELECT #replyId = (SELECT TOP 1 replyId FROM Replies WHERE messageId=#messageId)
DELETE FROM Replies WHERE replyId = #replyId
DELETE FROM Messages WHERE id=#replyId
SET #cpt = #cpt+1
END
END')
I would recommend either generating all the deletes at once in a trigger, as Fergus Bown suggested, or moving your delete logic to a stored procedure call. In our application, we use the stored procedure approach for all CRUD operations (create, read, update, delete).
The downside is that a rookie support DBA might goof if they use SQL to delete a single Reply without deleting all the other Messages associated with it. But such a DBA ought to know to use the stored procedures instead (or get the SQL right in the first place).
Related
I am brand new to SQL, and I am learning on an SQLite editor. So I create a couple of very simple tables. This code is straight from Linkedin learning "SQL essential training", and I am using the recommended SQLite editor.
CREATE TABLE widgetInventory(
id INTEGER PRIMARY KEY,
description TEXT,
onhand INTEGER NOT NULL);
CREATE TABLE widgetSales(
id INTEGER PRIMARY KEY,
inv_id INTEGER,
quan INTEGER,
price INTEGER);
Then I update widgetInventory with some data:
INSERT INTO widgetInventory (description, onhand) VALUES ('rock', 25);
INSERT INTO widgetInventory (description, onhand) VALUES ('paper', 25);
INSERT INTO widgetInventory (description, onhand) VALUES ('scissors', 25);
Next, I want to update the widgetSales table with a sale, and update the widgetInventory table to record the reduction of onhand.
BEGIN TRANSACTION;
INSERT INTO widgetSales (inv_id, quan, price) VALUES (1,5,500);
UPDATE widgetInventory SET onhand = (onhand-5) WHERE id = 1;
END TRANSACTION;
I am not understanding why this gives me an error when I run it, as it is exactly as it is in the lesson.
[06:18:04] Error while executing SQL query on database 'test': cannot start a transaction within a transaction
But, I can run the INSERT and UPDATE lines separately, and they do what I want them to do.
Apparently, running - END TRANSACTION; - before running the entire transaction appears to work.
I think that somehow, SQL thinks that a transaction is already occurring. Though, I'm not sure where exactly. So to stop it, you have to end the transaction first before proceeding with the course.
In the SQLite Editor, you may have to delete or comment out all of the code before and after these two transactions.
BEGIN TRANSACTION;
INSERT INTO widgetSales ( inv_id, quan, price ) VALUES ( 1, 5, 500 );
UPDATE widgetInventory SET onhand = ( onhand - 5 ) WHERE id = 1;
END TRANSACTION;
BEGIN TRANSACTION;
INSERT INTO widgetInventory ( description, onhand ) VALUES ( 'toy', 25 );
ROLLBACK;
Otherwise it won't execute the transaction.
Other than that, there is probably an error written in somewhere. Copying and pasting in the .txt file didn't give me that transaction error and could execute the transaction normally.
Just had this same error and my issue was I only highlighted the first line so SQLLite started the transaction but didn't run it fully. All I did was run end transaction, highlight the whole block of code and run that and it worked fine. Must be some syntax issue in Lite that doesn't run the full block itself.
while executing SQL query on database 'test': cannot start a
transaction within a transaction
means a transaction already exists. It may happen if someone forgets to select the END TRANSACTION; statement.
If you face this issue just select END TRANSACTION once and run. With this it will end the active transaction and then you can run any of the existing transaction.
For the particular case of following the Linkedin learning "SQL essential training" course, I have figured out to fix it by running (f9) the "BEGIN TRANSACTION", "...TRANSACTION CONTENTS..." and "END TRANSACTION" statements separately, not all the statements at the same time.
So,
First select the "BEGIN TRANSACTION;" and run it by pressing f9.
Then select the contents of the transactions (I think you can include also the "END TRANSACTION;" part) and run it.
I have searched extensively on this and I have found a lot of people asking the question but no answers that included code examples to help me understand.
I'd like to write a transaction (in sql using the command line sqlite3 interface) that performs several update statements, and if any of them fail for any reason, rolls back the transaction. The default behaviour appears to be to roll-back the statement that failed but commit the others.
This tutorial appears to advise that it's sufficient to add begin; and rollback; before and after the statements, but that's not true because I've tried it with deliberate errors and the non-error statements were definitely committed (which I don't want).
This example really confuses me because the two interlocutors seem to give conflicting advice at the end - one says that you need to write error handling (without giving any examples) whereas the other says that no error handling is needed.
My MWE is as follows:
create table if not exists accounts (
id integer primary key not null,
balance decimal not null default 0
);
insert into accounts (id, balance) values (1,200),(2,300);
begin transaction;
update accounts set balance = field1 - 100 where id = 1;
update accounts set balance = field1 + 100 where id = 2;
update accounts set foo = 23; //Deliberate error
commit;
The idea is that none of these changes should be committed.
The sqlite3 command-line shell is intended to be used interactively, so it allows you to continue after an error.
To abort on the first error instead, use the -bail option:
sqlite3 -bail my.db < mwe.sql
If you are executing line by line, then the idea is that you first run these commands:
create table if not exists accounts (
id integer primary key not null,
balance decimal not null default 0
);
insert into accounts (id, balance) values (1,200),(2,300);
begin transaction;
update accounts set balance = field1 - 100 where id = 1;
update accounts set balance = field1 + 100 where id = 2;
update accounts set foo = 23; //Deliberate error
At this point, if you have no errors, you run the commit:
commit;
All the updates should be visible if you open a second connection and query the table.
On another hand if you got an error, instead of committing you rollback:
rollback;
All the updates should be rolled back;
If you are doing it programatically in java you would enclose the updates in a try - catch block, and commit at the end of the try, or rollback inside the catch.
I am testing the trigger named, "tulockout" listed below, with this alter statement..."alter user testuser account lock;" to see if the trigger log a record of what happened in table, "log_table_changes".
However, certain values are not accurately logging into the table, "log_table_changes". To be specific v_dusr.start_dt is returning NULL when the trigger, "tulockout" fires off after I execute "alter user testuser account lock;" statement.
I am not certain as to why. Can you please assist?
How can I fix this issue? Thanks.
create or replace trigger tulockout
after alter on schema
declare
cursor v_abc is
select du.username, max(us.start_dt)
from dba_users du, user_session us, users_info ui
where ui.db_user_name = du.username
and ui.db_user_name = us.user_name
and ui.db_user_name = ora_login_user;
v_dusr v_abc%ROWTYPE;
begin
if(ora_sysevent = 'ALTER' and v_dusr.username = ora_dict_obj_name and
v_dusr.account_status = 'LOCKED') then
insert into log_table_changes(username,
lastlogin_date,
notes,
execute_date,
script_name
)
values(
v_dusr.username,
v_dusr.start_dt,
ora_dict_obj_type||', '||
ora_dict_obj_name||' has been locked out.',
sysdate,
ora_sysevent
);
end;
You are declaring a cursor, and a record based on that; but you don't ever execute the cursor query or populate the variable.
Your cursor query is currently missing a group-by clause so will error when run, because of the aggregate function. You don't really need to include the user name in the select list though, as you already know that value. You are, though, later referring to the v_duser.account_status field, which doesn't exist in your cursor query/rowtype, so you need to add (and group by) that too.
The trigger also needs to be at database, not schema, level; and unless you intend to record who performed the alter command, you don't ned to refer to ora_login_user - looking that user's status up doesn't seem very helpful.
You don't really need a cursor at all; a select-into would do, something like (assuming there will always be a row returned from the joins to your user_session and users_info tables; which implies they store the username in the same case as dba_users does - although I'm not sure why you are joining to users_info at all?):
create or replace trigger tulockout
after alter on database
declare
v_start_dt user_session.start_dt%TYPE;
v_account_status dba_users.account_status%TYPE;
begin
select du.account_status, max(us.start_dt)
into v_account_status, v_start_dt
from dba_users du
join user_session us on us.db_user_name = du.username
-- join not needed?
-- join users_info ui on ui.db_user_name = us.user_name
where du.username = ora_dict_obj_name
group by du.account_status;
if(ora_sysevent = 'ALTER' and ora_dict_obj_type = 'USER'
and v_account_status = 'LOCKED') then
insert ...
and then use those date and status variables and ora_dict_obj_name(the user that was altered) in the insert.
I've also switched to modern join syntax, and tweaked the conditions a bit.
Untested, but should give you the idea.
You could make it even easier by doing a single insert ... select against those tables, removing the need for local variables.
I'm trying to create a function that compares 2 collections with each other. Let me explain what I'm trying to do first: I have a table for accounts and a table with teachers. I want to use this function to see if there are any accounts that aren't linked to a teacher and delete them. I have most of the function done, but I can't seem to figure out how to delete the account that isn't linked from the database. Does anyone have any ideas? Here's my code:
declare
type type_coll_accnr is table of account.account_id%type;
type type_coll_teachernr is table of teacher.teacher_id%type;
t_teachernr type_coll_teachernr;
t_accnr type_coll_accnr;
begin
select account_account_id
bulk collect into t_teachernr
from teacher;
select account_id
bulk collect into t_accnr
from account
where acces = 'Teacher';
for i_counter IN 1 .. t_teachernr.count
loop
if t_teachernr(i_counter) member of t_accnr then
dbms_output.put_line(t_accnr(i_counter));
else
delete from account where account_id = t_accnr(i_counter);
-- It should delete the account here, but I have no clue how.
end if;
end loop;
end;
I had to translate all of this, so please let me know if I missed something. I'm also pretty new to PL/SQL, so I know this might look like a stupid question!
Ok, I was right - loop should be on t_accnr list. And it should checks if current value from t_accnr is member of t_teachernr.
I create tables and checked it.
t_accnr is large, but it is not a problem
declare
type type_coll_accnr is table of account.account_id%type;
type type_coll_teachernr is table of teacher.teacher_id%type;
t_teachernr type_coll_teachernr;
t_accnr type_coll_accnr;
begin
select account_account_id
bulk collect into t_teachernr
from teacher;
select account_id
bulk collect into t_accnr
from account where acces = 'Teacher';
for i_counter IN 1 .. t_accnr.count
loop
if t_accnr(i_counter) member of t_teachernr then
dbms_output.put_line(t_accnr(i_counter));
else
dbms_output.put_line('delete from account where account_id ='|| t_accnr(i_counter));
delete from account where account_id = t_accnr(i_counter);
-- It should delete the account here, but I have no clue how.
end if;
end loop;
end;
Hope it helps
I have the following error:
ORA-04091: table SYSTEM.ORDINE is mutating, trigger/function may not
see it
On this trigger PL/SQL:
create or replace trigger T_Ordine
after insert on Ordine
for each row
DECLARE
conta number := 0;
t_o exception;
BEGIN
select count(*) into conta
from Cliente join Ordine on Cliente.ID_Cliente = Ordine.ID_CLiente
where Cliente.C_CF_Rivenditore <> Ordine.CF_Rivenditore;
if conta > 0 then
raise t_o;
end if;
EXCEPTION
when t_o then
raise_application_error(-20002,'Un rivenditore non puo ricevere ordini da un cliente non suo');
end;
/
I think that the error caused from the modification of table Ordine in the join with table Cliente.
Your trigger is a little odd.
You've declare it for each row, yet you never use :new to access any of the inserted values.
As far as I can see, there are two ways to fix your trigger:
Make the trigger a statement-level trigger, so that it runs once after the ordine table is inserted into, regardless of how many rows are inserted. To do this, simply delete the line for each row.
Adjust the trigger to only check the inserted order, rather than every order in the table. To do this, replace the SQL query you use to find conta with the following:
select count(*) into conta
from Cliente
where Cliente.ID_Cliente = :new.ID_CLiente
and Cliente.C_CF_Rivenditore <> :new.CF_Rivenditore;
Note that we are no longer querying the Ordine table - the details of the row that has just been inserted are available as :new.column_name. This gets around the ORA-04091 error.
I would recommend the second approach. The query you use to find conta currently searches the whole of the Ordine table, and as your application gains more and more orders, this trigger gets slower and slower as the query searches through more and more data. Also, you probably don't want your application to refuse to take any orders from anyone if it happens that there's one order somewhere in the system where the client's Rivenditore doesn't match the order's Rivenditore.
Incidentally, there's not a lot of point raising the exception t_o, catching it and raising an alternative exception. Just raise the second exception straight away, i.e.:
if conta > 0 then
raise_application_error(-20002,'Un rivenditore non puo ricevere ordini da un cliente non suo');
end if;
Since I am italian, I am a little advantaged in understanding what you are trying to do:
"Ordine" is the table of orders (like product orders).
"rivenditore" means "seller".
"cliente" means customer.
in the "customer" table there is a field (C_CF_Rivenditore) that imposes the seller that should be used for orders issued by the customer.
the "orders" table contains a reference to the customer and a reference to the seller receiving the order.
you simply want to make it impossible to insert an order for a seller different from the one designated for each customer (this is what your error message says), but you don't know how to use :new or :old, so you wrote the test that way (which isn't at all the best method of doing it, since you are re-checking all the orders in the table every time a new order is inserted).
This is what you really want to write:
create or replace trigger T_Ordine
after insert on Ordine
for each row
DECLARE
rivenditore_del_cliente Cliente.C_CF_Rivenditore%type;
BEGIN
select Cliente.C_CF_Rivenditore
into rivenditore_del_cliente
from Cliente
where Cliente.ID_Cliente = :new.ID_CLiente
if rivenditore_del_cliente <> :new.CF_Rivenditore then
raise raise_application_error(-20002,
'Un rivenditore non puo ricevere ordini da un cliente non suo');
end if;
end;
the above trigger might need to be extended with some further checks if some of these are true:
id_cliente is not the primary key of "cliente"
ordine.id_cliente is not mandatory
there isn't a foreign key constraint that ensures that ordine.id_cliente is a valid ID_cliente of the clienti table.