How to describe user-input values in trigger - Oracle PL/SQL - plsql

I am learning PL/SQL and have attempted to create a trigger where once a particular condition has been met in the fact table, data will be routed to the appropriate dimension table.
There is a fact table
BHM_FACT_TABLE
Four dimension tables
BHM_EMPLOYEES
BHM_ACCOUNTS
BHM_SUBSCRIBERS
BHM_EXPENSES
In the fact table, there is an "S_Number" column that serves as the primary key for the fact table. There is also a "Code" column that lists either A, B, C, or D, depending on whether the record lists an employee, an account, a subscriber, or an expense (this is a very basic, rudimentary table, by the way).
The fact table only contains the following columns:
S_Number
Code
Employees
Accounts
Subscribers
Expenses.
The dimension tables contain additional columns to hold data about the employees, accounts, subscribers, and expenses (you can see the columns in details in the trigger below).
What I want the trigger to do is route the data that the user inputs to the appropriate dimension table, based on what is entered in the fact table - i.e., once a row is entered with a code of 'A', which means that this record is of an employee - then that will prompt the rest of the values input to go to the BHM_EMPLOYEES table. And so on and so forth. So, this is what I came up with:
CREATE OR replace TRIGGER bhm_test
AFTER INSERT ON bhm_fact_table
FOR EACH ROW
DECLARE
v_code VARCHAR2 (1);
BEGIN
SELECT code
INTO v_code
FROM bhm_fact_table;
IF v_code = 'A' THEN
INSERT INTO bhm_employees
(s_number,
code,
employeename,
employeeaddress,
employeessn,
employeephonenumber,
employeesalary)
VALUES ( :NEW.s_number,
:NEW.code,
employeename,
employeeaddress,
employeessn,
employeephonenumber,
employeesalary );
ELSIF v_code = 'B' THEN
INSERT INTO bhm_accounts
(s_number,
code,
accountname,
address,
taxid,
phonenumber,
invoicetotal)
VALUES ( :NEW.s_number,
:NEW.code,
accountname,
address,
taxid,
phonenumber,
invoicetotal );
ELSIF v_code = 'C' THEN
INSERT INTO bhm_subscribers
(s_number,
code,
subscribername,
subscriberaddress,
subscriberphone,
subscribertype,
subscriberpaid)
VALUES ( :NEW.s_number,
:NEW.code,
subscribername,
subscriberaddress,
subscriberphone,
subscribertype,
subscriberpaid );
ELSIF v_code = 'D' THEN
INSERT INTO bhm_expenses
(s_number,
code,
expensesname,
expensesamt)
VALUES ( :NEW.s_number,
:NEW.code,
expensesname,
expensesamt );
END IF;
END;
For this I am getting the error
ORA-00984: Column Not Allowed Here
for each last column listed for VALUES (employeesalary, invoicetotal, subscriberpaid, and expensesamt). I took away the bind variables for the columns that you see without them; I originally had them by all of the values, but got the "Bad Bind Variables" error.
So my first question is, what would I put in the VALUES clause, if I don't know that information yet and it won't be stored (and thus available to reference) from the fact table?
My second question is - am I even going remotely in the right direction with this trigger?

Well, you're trying to fetch from the same table you're inserting in, so at the very least you'll run into a Mutating table/trigger error.
You don't need to SELECT the code, it's accessible via :NEW.CODE in your trigger.
The other problem is that you're trying to insert into the various tables without referencing the source -
INSERT INTO bhm_accounts
(s_number,
code,
accountname,
address,
taxid,
phonenumber,
invoicetotal)
VALUES ( :NEW.s_number,
:NEW.code,
accountname,
address,
taxid,
phonenumber,
invoicetotal );
This, for instance makes no sense because you're trying to refer the same column as the source of data that you're trying to insert into.
If the columns are available in BHM_FACT_TABLE, then you can refer them using :NEW.COLUMN_NAME modifier and insert them into the subsequent tables.
But since you mention that these columns are not present in BHM_FACT_TABLE, and that they are user entered, then a trigger is a wrong approach to this. Triggers are useful when you have the data stored/available readily beforehand.
Instead of using triggers, a better way is to use a stored procedure which will insert the data into the various tables based on the columns.

Related

Need to delete/insert that corresponding row in that table, as there is duplicate data coming up- PLSQL

I need a help in getting the PLSQL procedure to : Insert/Delete the rows of a table , because as I used Update functionality getting duplicates for that particular Sequence ID field.
So for a particular sequence ID row, whenever I insert the data, it should be the latest in that table.
The last sentence you wrote suggests that you have to
delete row(s) whose ID equals that particular "sequence ID" value
then insert a new row
If you expected some code to be written, you should have posted some more info (CREATE TABLE and INSERT INTO sample data, as well as the way you manipulate it by inserting new row, showing what you expect to happen with old one(s)). It is difficult to write code based on unknown data model.
A guess...
INSERT INTO schema_name.table_name(
primary_key_column
, other_column
)
VALUES(
(SELECT max(primary_key_column)+1 FROM schema_name.table_name),
, 'other_value'
);
COMMIT;
This is the procedure I am using:
https://drive.google.com/file/d/1eGbxSppjexpICKh6pzuW0ZzckVxA6BB0/view?usp=sharing
My requirement when we need to insert the new data , the previous data should be deleted for the corresponding ID.
In the above procedure I am updating the data.

Efficient insertion of row and foreign table row if it does not exist

Similar to this question and this solution for PostgreSQL (in particular "INSERT missing FK rows at the same time"):
Suppose I am making an address book with a "Groups" table and a "Contact" table. When I create a new Contact, I may want to place them into a Group at the same time. So I could do:
INSERT INTO Contact VALUES (
"Bob",
(SELECT group_id FROM Groups WHERE name = "Friends")
)
But what if the "Friends" Group doesn't exist yet? Can we insert this new Group efficiently?
The obvious thing is to do a SELECT to test if the Group exists already; if not do an INSERT. Then do an INSERT into Contacts with the sub-SELECT above.
Or I can constrain Group.name to be UNIQUE, do an INSERT OR IGNORE, then INSERT into Contacts with the sub-SELECT.
I can also keep my own cache of which Groups exist, but that seems like I'm duplicating functionality of the database in the first place.
My guess is that there is no way to do this in one query, since INSERT does not return anything and cannot be used in a subquery. Is that intuition correct? What is the best practice here?
My guess is that there is no way to do this in one query, since INSERT
does not return anything and cannot be used in a subquery. Is that
intuition correct?
You could use a Trigger and a little modification of the tables and then you could do it with a single query.
For example consider the folowing
Purely for convenience of producing the demo:-
DROP TRIGGER IF EXISTS add_group_if_not_exists;
DROP TABLE IF EXISTS contact;
DROP TABLE IF EXISTS groups;
One-time setup SQL :-
CREATE TABLE IF NOT EXISTS groups (id INTEGER PRIMARY KEY, group_name TEXT UNIQUE);
INSERT INTO groups VALUES(-1,'NOTASSIGNED');
CREATE TABLE IF NOT EXISTS contact (id INTEGER PRIMARY KEY, contact TEXT, group_to_use TEXT, group_reference TEXT DEFAULT -1 REFERENCES groups(id));
CREATE TRIGGER IF NOT EXISTS add_group_if_not_exists
AFTER INSERT ON contact
BEGIN
INSERT OR IGNORE INTO groups (group_name) VALUES(new.group_to_use);
UPDATE contact SET group_reference = (SELECT id FROM groups WHERE group_name = new.group_to_use), group_to_use = NULL WHERE id = new.id;
END;
SQL that would be used on an ongoing basis :-
INSERT INTO contact (contact,group_to_use) VALUES
('Fred','Friends'),
('Mary','Family'),
('Ivan','Enemies'),
('Sue','Work colleagues'),
('Arthur','Fellow Rulers'),
('Amy','Work colleagues'),
('Henry','Fellow Rulers'),
('Canute','Fellow Ruler')
;
The number of values and the actual values would vary.
SQL Just for demonstration of the result
SELECT * FROM groups;
SELECT contact,group_name FROM contact JOIN groups ON group_reference = groups.id;
Results
This results in :-
1) The groups (noting that the group "NOTASSIGNED", is intrinsic to the working of the above and hence added initially) :-
have to be careful regard mistakes like (Fellow Ruler instead of Fellow Rulers)
-1 used because it would not be a normal value automatically generated.
2) The contacts with the respective group :-
Efficient insertion
That could likely be debated from here to eternity so I leave it for the fence sitters/destroyers to decide :). However, some considerations:-
It works and appears to do what is wanted.
It's a little wasteful due to the additional wasted column.
It tries to minimise the waste by changing the column to an empty string (NULL may be even more efficient, but for some can be confusing)
There will obviously be an overhead BUT in comparison to the alternatives probably negligible (perhaps important if you were extracting every Facebook user) but if it's user input driven likely irrelevant.
What is the best practice here?
Fences again. :)
Note Hopefully obvious, but the DROP statements are purely for convenience and that all other SQL up until the INSERT is run once
to setup the tables and triggers in preparation for the single INSERT
that adds a group if necessary.

Alternative to using subquery inside CHECK constraint?

I am trying to build a simple hotel room check-in database as a learning exercise.
CREATE TABLE HotelReservations
(
roomNum INTEGER NOT NULL,
arrival DATE NOT NULL,
departure DATE NOT NULL,
guestName CHAR(30) NOT NULL,
CONSTRAINT timeTraveler CHECK (arrival < departure) /* stops time travelers*/
/* CONSTRAINT multipleReservations CHECK (my question is about this) */
PRIMARY KEY (roomNum, arrival)
);
I am having trouble specifying a constraint that doesn't allow inserting a new reservation for a room that has not yet been vacated. For example (below), guest 'B' checks into room 123 before 'A' checks out.
INSERT INTO HotelStays(roomNum, arrival, departure, guestName)
VALUES
(123, date("2017-02-02"), date("2017-02-06"), 'A'),
(123, date("2017-02-04"), date("2017-02-08"), 'B');
This shouldn't be allowed but I am unsure how to write this constraint. My first attempt was to write a subquery in check, but I had trouble figuring out the proper subquery because I don't know how to access the 'roomNum' value of a new insert to perform the subquery with. I then also figured out that most SQL systems don't even allow subquerying inside of check.
So how am I supposed to write this constraint? I read some about triggers which seem like it might solve this problem, but is that really the only way to do it? Or am I just dense and missing an obvious way to write the constraint?
The documentation indeed says:
The expression of a CHECK constraint may not contain a subquery.
While it would be possible to create a user-defined function that goes back to the database and queries the table, the only reasonable way to implement this constraint is with a trigger.
There is a special mechanism to access the new row inside the trigger:
Both the WHEN clause and the trigger actions may access elements of the row being inserted, deleted or updated using references of the form "NEW.column-name" and "OLD.column-name", where column-name is the name of a column from the table that the trigger is associated with.
CREATE TRIGGER multiple_reservations_check
BEFORE INSERT ON HotelReservations
BEGIN
SELECT RAISE(FAIL, "reservations overlap")
FROM HotelReservations
WHERE roomNum = NEW.roomNum
AND departure > NEW.arrival
AND arrival < NEW.departure;
END;

PL/SQL insert based on conditions

I would appreciate all help I can get. I'm learning PL/SQL and have stumbled on a problem so please help me find an appropriate way of handling this situation :)
I'm running Oracle 11gR2
My schema:
CREATE TABLE "ENTRY"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
CREATE TABLE "VIEW"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
CREATE TABLE "REJECTED"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
My sample data:
insert into entry (type, transaction, owner) values (11111, 11111, 11111);
insert into entry (type, transaction, owner) values (22222, 22222, 22222);
Now for the puzzling part, I've wrote this procedure that should copy the values from the ENTRY table to VIEW table if a record does not exist for specific (transaction AND owner) combination. If such a combination exists in the VIEW table that record should then go to the REJECTED table. This procedure does that but on multiple runs of the procedure I get more and more entries in the REJECTED table so my question is how to limit inserts in the REJECTED table - if a record already exists in the REJECTED table then do nothing.
create or replace PROCEDURE COPY AS
v_owner_entry ENTRY.owner%TYPE;
v_transaction_entry ENTRY.transaction%TYPE;
v_owner VIEW.owner%TYPE;
v_transaction VIEW.transaction%TYPE;
begin
begin
select e.owner, e.transaction, v.owner, v.transaction
into v_owner_entry, v_transaction_entry, v_owner, v_transaction
from entry e, view v
where e.owner = v.owner
and e.transaction = v.transaction;
EXCEPTION
when too_many_rows
then
insert into REJECTED
(
TYPE,
TRANSACTION,
OWNER
)
SELECT
s1.TYPE,
s1.TRANSACTION,
s1.OWNER
FROM ENTRY s1;
when no_data_found
THEN
insert into VIEW
(
TYPE,
TRANSACTION,
OWNER
)
SELECT
s.TYPE,
s.TRANSACTION,
s.OWNER
FROM ENTRY s;
end;
end;
Any suggestions guys? :)
Cheers!
UPDATE
Sorry if the original post wasn't clear enough -
The procedure should replicate data (on a daily basis) from DB1 to DB2 and insert into VIEW or REJECTED depending on the conditions. Here is a photo, maybe it would be clearer:
I think Dmitry was trying to suggest using MERGE in the too_many_rows case of your exception handler. So you've already done the SELECT up front and determined that the Entry row appears in your View table and so it raises the exception too_many_rows.
The problem is that you don't know which records have thrown the exception (assuming your Entry table has more than one row easy time this procedure is called). So I think your idea of using the exception section to determine that you have too many rows was elegant, but insufficient for your needs.
As a journeyman programmer, instead of trying to come up with something terribly elegant, I'd use more brute force.
Something more like:
BEGIN
FOR entry_cur IN
(select e.owner, e.transaction, SUM(NVL2(v.owner, 1, 0)) rec_count
from entry e, view v
where e.owner = v.owner(+)
and e.transaction = v.transaction(+)
GROUP BY e.owner, e.transaction)
LOOP
CASE WHEN rec_count > 0
THEN INSERT INTO view
ELSE MERGE INTO rejected r
ON (r.transaction = entry_cur.transaction
AND r.owner = entry_cur.owner)
WHEN NOT MATCHED THEN INSERT blah blah blah
;
END LOOP;
END;
The HAVING COUNT(*) > 1 will
No exceptions thrown. The loop gives you the correct record you don't want to insert into View. BTW, I couldn't get over that you used keywords for object names - view, transaction, etc. You enquoted the table names on the CREATE TABLE "VIEW" statement, which gets around the fact that these are keywords, but you didn't when you referenced them later, so I'm surprised the compiler didn't reject the code. I think that's a recipe for disaster because it makes debugging so much harder. I"m just hoping that you did this for the example here, not in PL/SQL.
Personally, I've had trouble using the MERGE statement where it didn't seem to work consistently, but that was an Oracle version long ago and probably my own ignorance in how it should work.
Use MERGE statement:
merge into REJECTED r
using ENTRY e
on (r.type = e.type and
r.transaction = e.transaction and
r.owner = e.owner)
when not matched then insert (type, transaction, owner)
values (e.type, e.transaction, e.owner)
This query will insert into table REJECTED only combinations of (type, transaction, owner) from table ENTRY that are not present there yet.
You're trying to code yourself out of a quandary you've modeled yourself into.
A table should contain your entity. There should not be a table of entities in one state, another table for entities in another state, and yet another table for entities in a different state altogether. You're seeing the kind of problems this can lead to.
The state can be an attribute (field or column) of the one table. Or normalized to a state table but still only one entity table. When an entity changes states, this is accomplished by an update, not a move from one table to another.

Update if exists, else insert new record, all based on checkboxes

I'm having some issues using checkboxes in combination with an insert/update statement. As it stands, I have a report of available credit cards, with a checkbox next to each row. The user can select as many as they like to approve, then hit a submit button to have their profile updated. This is where I am stuck.
I would like to have a single PSQL process that will update the user profile table based on whether or not said credit card is there. If it doesn't exist, then we insert all relevant information. If it already is there, all I want to do is update that record by changing 'Approved_flag' to 'Y'. I've written this code chunk, which inserts new records and it works fine:
FOR i in 1..APEX_APPLICATION.G_F01.count
LOOP
INSERT INTO ls_credit_cards(credit_card_id, created_by, created_on, card_id, user_id, approved_flag)
VALUES (apex_application.g_f01(i), :F125_USER_ID,sysdate, :P58_CARDS, :P58_USER, 'Y');
END LOOP;
I understand that ORACLE doesn't support the usual if/else commands, so I've researched this a bit and found that I should probably be using the MERGE command, but everything I've seen makes it use two tables. All I'm using is one, with all data being taken from the report/check boxes so I'm kind of at a loss here. Can I still use the MERGE command in this instance, or is there something else that would serve my purposes better?
You can use MERGE. You just need to select your data from `DUAL'
MERGE INTO ls_credit_cards dest
USING( SELECT apex_application.g_f01(i) credit_card_id,
:F125_USER_ID created_by,
sysdate created_on,
:P58_CARDS card_id,
:P58_USER user_id,
'Y' approved_flag
FROM dual) src
ON( dest.credit_card_id = src.credit_card_id )
WHEN MATCHED
THEN
UPDATE SET dest.approved_flag = src.approved_flag
WHEN NOT MATCHED
THEN
INSERT( credit_card_id,
created_by,
created_on,
card_id,
user_id,
approved_flag )
VALUES( src.credit_card_id,
src.created_by,
src.created_on,
src.card_id,
src.user_id,
src.approved_flag );

Resources