How do you write a procedure which shows that one field's value cannot be higher than another field's value, in terms of numbers. Say. an employee'a salary can't be higher than his manager's salary. I've never done one before
There is no declarative way to enforce business rules like this in SQL. So it has to be done with code. There are a number of gotchas, not the least of which is identifying all the scenarios where the rule needs to be enforced..
Here are the scenarios:
When we insert an employee we need to check whether their salary is greater than 90% of their manager's salary.
When we update an employee's salary we need to check that it still isn't greater than 90% of their manager's salary.
When we update a manager's salary we need to check that it is still greater than 110% of all their subordinates' salaries.
If we insert records simultaneously for a manager and their subordinates (say using INSERT ALL) we need to make sure that rule is still enforced.
If we move an employee from one manager to another we need to make sure that rule is still enforced.
Here are the things which make all this harder:
Enforcing these rules involves selecting from the table we are manipulating so we cannot use BEFORE ... FOR EACH ROW triggers, due to the ORA-04088: mutating tables exceptions.
Also, selecting from the table means we cannot run in multi-user mode, because of read consistency (otherwise session #1 could go ahead with a pay increase to an employee oblivious to the fact that session #2 is currently applying a pay decrease to that employee's manager).
So, for all those reasons, the only way to enforce such business rules is to use an API; build a stored procedure and never let any process have naked DML access to the table.
The following chunk o' code enforces the rule just when updating an employee's salary. Points of interest include:
it has user-defined exceptions to identify rule violations. Really these should be defined in a package specification, so other program units can reference them.
the use of SELECT ... FOR UPDATE to lock the rows of interest.
the use of COMMIT and ROLLBACK to release the locks. In a real implementation this might be handled differently (i.e. by the calling program).
create or replace procedure change_emp_sal
( p_eno in emp.empno%type
, p_new_sal in emp.sal%type )
is
type emp_nt is table of emp%rowtype;
l_emp emp%rowtype;
l_mgr emp%rowtype;
l_subords emp_nt;
l_idx pls_integer;
x_mgr_not_paid_enough exception;
pragma exception_init(x_mgr_not_paid_enough, -20000);
x_sub_paid_too_much exception;
pragma exception_init(x_sub_paid_too_much, -20001);
begin
-- lock the employee record
select * into l_emp
from emp
where empno = p_eno
for update of sal;
-- lock their manager's record (if they have one)
if l_emp.mgr is not null
then
select * into l_mgr
from emp
where empno = l_emp.mgr
for update;
end if;
-- lock their subordinates' records
select * bulk collect into l_subords
from emp
where mgr = p_eno
for update;
-- compare against manager's salary
if l_mgr.sal is not null
and l_mgr.sal < ( p_new_sal * 1.1 )
then
raise x_mgr_not_paid_enough;
end if;
-- compare against subordinates' salaries
for i in 1..l_subords.count()
loop
if l_subords(i).sal > ( p_new_sal * 0.9 )
then
l_idx := i;
raise x_sub_paid_too_much;
end if;
end loop;
-- no exceptions raised so we can go ahead
update emp
set sal = p_new_sal
where empno = p_eno;
-- commit to free the locks
commit;
exception
when x_mgr_not_paid_enough then
dbms_output.put_line ('Error! manager only earns '||l_mgr.sal);
rollback;
raise;
when x_sub_paid_too_much then
dbms_output.put_line ('Error! subordinate earns '||l_subords(l_idx).sal);
rollback;
raise;
end change_emp_sal;
/
Here are the four employees of Deptarment 50:
SQL> select e.empno, e.ename, e.sal, m.ename as mgr_name, m.empno as mgr_no
2 from emp e join emp m on (e.mgr = m.empno)
3 where e.deptno = 50
4 order by sal asc
5 /
EMPNO ENAME SAL MGR_NAME MGR_NO
---------- ---------- ---------- ---------- ----------
8060 VERREYNNE 2850 FEUERSTEIN 8061
8085 TRICHLER 3500 FEUERSTEIN 8061
8100 PODER 3750 FEUERSTEIN 8061
8061 FEUERSTEIN 4750 SCHNEIDER 7839
SQL>
Let's try to give Billy a big raise, which should fail...
SQL> exec change_emp_sal (8060, 4500)
Error! manager only earns 4750
BEGIN change_emp_sal (8060, 4500); END;
*
ERROR at line 1:
ORA-20000:
ORA-06512: at "APC.CHANGE_EMP_SAL", line 67
ORA-06512: at line 1
SQL>
Okay, let's give Billy a smaller raise, which should succeed...
SQL> exec change_emp_sal (8060, 4000)
PL/SQL procedure successfully completed.
SQL>
Now let's try to give Steven a swingeing pay cut, which should fail...
SQL> exec change_emp_sal (8061, 3500)
Error! subordinate earns 3500
BEGIN change_emp_sal (8061, 3500); END;
*
ERROR at line 1:
ORA-20001:
ORA-06512: at "APC.CHANGE_EMP_SAL", line 71
ORA-06512: at line 1
SQL>
So let's give Steven a token pay cut, which should succeed ...
SQL> exec change_emp_sal (8061, 4500)
PL/SQL procedure successfully completed.
SQL>
Here is the new pay structure...
SQL> select e.empno, e.ename, e.sal, m.ename as mgr_name, m.empno as mgr_no
2 from emp e join emp m on (e.mgr = m.empno)
3 where e.deptno = 50
4 order by sal asc
5 /
EMPNO ENAME SAL MGR_NAME MGR_NO
---------- ---------- ---------- ---------- ----------
8085 TRICHLER 3500 FEUERSTEIN 8061
8100 PODER 3750 FEUERSTEIN 8061
8060 VERREYNNE 4000 FEUERSTEIN 8061
8061 FEUERSTEIN 4500 SCHNEIDER 7839
SQL>
So it works, as far as it goes. It only handles two of the five scenarios. Refactoring the code to satisfy the other three is left as an exercise for the reader.
Related
"ORA-04091: table JOSEP.EMP is mutating, trigger/function may not see it"
I have to show a message with the old and the new salary, and the code of the employed (emp_no) in it, and I'm not able to do it.
create or replace trigger emp_AU
after update of salario
on emp for each row
declare
v_emp_no emp.emp_no%type;
begin
select emp_no into v_emp_no FROM emp;
insert into auditaemple VALUES ((select count(*) from auditaemple)+1, 'El salario del empleado '||v_emp_no||'antes era de '||:old.salario||' y ahora será '||:new.salario, sysdate);
end emp_AU;
Doing it like this gives the "ORA-04091:" error. If I eliminate v_emp_no, I won't get the message, but I need to show the code of the employed. What I'm I doing wrong.
Thank in advance.
Reason which caused the mutating table error is in selecting data from the table when it is in the middle of a transaction - you're updating it, and - at the same time - selecting from it. As you can't do that (well, you could, there are workarounds, but you shouldn't), Oracle won't let you.
There's no need to select emp_no; you already have it - reference it with :new pseudorecord. Besides, the way you put it, you'd get TOO-MANY-ROWS error as there's no WHERE clause which would restrict resultset to a single row.
Don't use count + 1 (nor max + 1 or similar "technique"), especially if you're about to populate a column which is supposed to be unique. As long as it'll work in a single-user environment, it'll fail (sooner or later) in a multi-user one. Use a sequence (or, if your database supports it, an identity column).
Here's a working example of how you might have done that.
First, test case:
SQL> create table temp as select empno emp_no, sal salario
2 from emp where deptno = 10;
Table created.
SQL> create table auditaemple (id number, text varchar2(100), datum date);
Table created.
SQL> create sequence seqa;
Sequence created.
Trigger:
SQL> create or replace trigger trg_bu_emp
2 before update of salario on temp
3 for each row
4 begin
5 insert into auditaemple (id, text, datum)
6 values (seqa.nextval,
7 'El salario del empleado '||:new.emp_no||' antes era de '||
8 :old.salario||' y ahora será '||:new.salario, sysdate);
9 end;
10 /
Trigger created.
Testing:
SQL> select * from temp;
EMP_NO SALARIO
---------- ----------
7782 2450
7839 5000
7934 1300
SQL> update temp set salario = 9000 where emp_no = 7839;
1 row updated.
SQL> select * From auditaemple;
ID TEXT DATUM
---------- ---------------------------------------- ----------------
1 El salario del empleado 7839 antes era d 11.04.2019 21:47
e 5000 y ahora será 9000
SQL>
what is wrong? everything seems fine. but....
CREATE OR REPLACE TRIGGER salary_change
BEFORE UPDATE
OF emp_salary
ON employee
FOR EACH ROW
WHEN (((NEW.emp_salary-OLD.emp_salary)/OLD.emp_salary)>0.2)
DECLARE
limit NUMBER(7);
BEGIN
limit:=:OLD.emp_salary*1.2;
RAISE_APPLICATION_ERROR (-20000,'rule violated. cannot increase beyond : '|| limit);
END;
I have errors:
ERROR at line 3:
ORA-20000: rule violated. cannot increase beyond : 3360
ORA-06512: at "SYSTEM.SALARY_CHANGE", line 5
ORA-04088: error during execution of trigger 'SYSTEM.SALARY_CHANGE'
The ORA-06512 just tells you what line in your code caused the error. In this case it's line #5 in the trigger, which apparently corresponds to the RAISE_APPLICATION_ERROR call.
So, apparently ((NEW.emp_salary - OLD.emp_salary) / OLD.emp_salary is greater than 0.2. Given that the limit value from the message is 3360, this would mean that OLD.EMP_SALARY was 2800, and NEW.EMP_SALARY was greater than 3360.
Best of luck
USING RAISE_APPLICATION_ERROR ALSO GIVES ERROR ORA-06512 AND POSSIBLY ORA-04088
CREATE OR REPLACE TRIGGER hr.salary_change
before UPDATE
OF salary
ON hr.employees
FOR EACH ROW
WHEN (((NEW.salary-OLD.salary)/OLD.salary)>0.2)
DECLARE
limit NUMBER(8,2);
salary_high exception;
pragma exception_init(salary_high ,-20001);
BEGIN
limit:=:OLD.salary*1.2;
RAISE_APPLICATION_ERROR (-20001,' rule violated. cannot increase beyond :'||to_char(limit));
exception
when salary_high then
:NEW.salary:=:OLD.salary;
dbms_output.put_line(dbms_utility.format_error_stack);
END;
SQL> SET SERVEROUT ON
SQL> select salary from hr.employees where employee_id=198;
SALARY
----------
3900
SQL> update hr.employees set salary=5900 where employee_id=198;
ORA-20001: rule violated. cannot increase beyond :4680
1 row updated.
SQL> select salary from hr.employees where employee_id=198;
SALARY
----------
3900
SQL>
I want to Write a PROCEDURE that will first print the Employee Number and Salary of an employee (i.e. 7839). Then it will increase the salary of an employee 7839 (this will be employee number in the table employee) as per following conditions:
Condition-1: If experience is more than 10 years, increase salary by 20%.
Condition-2: If experience is greater than 5 years, increase salary by 10%.
Condition-3: All others will get an increase of 5% in the salary.
The program will print the Employee Number and salary before and after the increase i tried the following steps but not sure how accurate is it..
I need to convert it to a PROCEDURE code.
please advise
DECLARE
veno emp.empno%type:=&veno;
vsal emp.sal%type;
vexp number;
BEGIN
select empno,sal,trunc(to_char(months_between(sysdate,hiredate)/12))into veno,vsal,vexp from emp where empno=veno;
DBMS_OUTPUT.PUT_LINE('before update:' ||chr(10)||veno||chr(10)||vsal);
if vexp>=10 then
update emp set sal=sal+(sal*.20) where empno=veno;
select sal into vsal from emp where empno=veno;
DBMS_OUTPUT.PUT_LINE('after update:' ||chr(10)||vsal);
elsif vexp>=5 then
update emp set sal=sal+(sal*.10) where empno=veno;
select sal into vsal from emp where empno=veno;
DBMS_OUTPUT.PUT_LINE('after update:' ||chr(10)||vsal);
else
update emp set sal=sal+(sal*.05) where empno=veno;
select sal into vsal from emp where empno=veno;
DBMS_OUTPUT.PUT_LINE('after update:' ||chr(10)||vsal);
end if;
END;
/
All you need to change is the DECLARE (indicating the start of an anonymous block) to CREATE PROCEDURE, with the variable you're currently setting via a substitution variable as a formal argument; so instead of:
DECLARE
veno emp.empno%type:=&veno;
vsal emp.sal%type;
vexp number;
BEGIN
...
END;
/
Make it:
CREATE OR REPLACE PROCEDURE my_proc (veno IN emp.empno%type)
AS
vsal emp.sal%type;
vexp number;
BEGIN
...
END;
/
You can then call that from an anonymous block, or in SQL*Plus or SQL Developer with the execute shorthand:
set serveroutput on
execute my_proc(&veno);
This example is still using a substitution variable so you'll be promoted for the value to use, but you can pass a number directly too.
Read more about creating procedures and the types of parameters.
You could simplify the code quite a bit to reduce repetition and requerying; look up case expressions and the returning clause. But that's not directly relevant.
I have asked this question before but I did not get any help.
I want to get the count of rows in two different table given an attribute.
This is my code .
Instead of fetching the total count where the condition holds, I am getting the whole count of the table
create or replace PROCEDURE p1( suburb IN varchar2 )
as
person_count NUMBER;
property_count NUMBER;
BEGIN
SELECT count(*) INTO person_count
FROM person p WHERE p.suburb = suburb ;
SELECT count(*) INTO property_count
FROM property pp WHERE pp.suburb = suburb ;
dbms_output.put_line('Number of People :'|| person_count);
dbms_output.put_line('Number of property :'|| property_count);
END;
/
Is there any other way to do this so that i can retrieve the real total count of people in that SUBURB
Some datas from PERSON TABLE
PEID FIRSTNAME LASTNAME
---------- -------------------- --------------------
STREET SUBURB POST TELEPHONE
---------------------------------------- -------------------- ---- ------------
30 Robert Williams
1/326 Coogee Bay Rd. Coogee 2034 9665-0211
32 Lily Roy
66 Alison Rd. Randwick 2031 9398-0605
34 Jack Hilfgott
17 Flood St. Bondi 2026 9387-0573
SOME DATA from PROPERTY TABLE
PNO STREET SUBURB POST
---------- ---------------------------------------- -------------------- ----
FIRST_LIS TYPE PEID
--------- -------------------- ----------
48 66 Alison Rd. Randwick 2031
12-MAR-11 Commercial 8
49 1420 Arden St. Clovelly 2031
27-JUN-10 Commercial 82
50 340 Beach St. Clovelly 2031
05-MAY-11 Commercial 38
Sorry for the way the table is looking .
This is the value I get when I run the above script.
SQL> exec p1('Randwick')
Number of People :50
Number of property :33
I changed the PROCEDURE ,this is what I get .
SQL> create or replace PROCEDURE p1( location varchar2 )
IS
person_count NUMBER;
property_count NUMBER;
BEGIN
SELECT count(p.peid) INTO person_count
FROM person p WHERE p.suburb = location ;
SELECT count(pp.pno) INTO property_count
FROM property pp WHERE pp.suburb = location ;
dbms_output.put_line('Number of People :'|| person_count);
dbms_output.put_line('Number of property :'|| property_count);
END;
/
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Procedure created.
SQL> exec p1('KINGSFORD')
Number of People :0
Number of property :0
PL/SQL procedure successfully completed.
SQL>
SQL>
SQL> exec p1('Randwick')
Number of People :0
Number of property :0
PL/SQL procedure successfully completed.
SQL>
The solution suppose to be this
SQL> exec p1('randwick');
Number of People: 7
Number of Property: 2
You named the variable the same as the field. In the query, suburb is first sought in the scope of the query, and it matches the field suburb even though it doesn't use the pp table alias.
So you're actually comparing the field with itself, therefore getting all records (where suburb is NOT NULL, that is). The procedure parameter isn't used in the query at all.
The solution: change the name of the procedure parameter.
To prevent errors like this, I always use P_ as a prefix for procedure/function parameters and V_ as a prefix for local variables. This way, they never mingle with field names.
Although I agree that the cause of the problem is a namespace issue between SQL and PL/SQL, in that the SQL engine has "captured" the name of the PL/SQL variable, I don't believe that changing the name of the parameter is the best approach. If you do this then you doom every developer to start prefixing every parameter name with "p_" or some other useless appendage, and to make sure that they never create a column with a P_ prefix.
If you look through the PL/SQL Supplied Packages documentation you see very few, if any, cases where Oracle themselves do this, although they have in the past done irritatingly inconsistent things like refer to table_name as "tabname".
A more robust approach is to prefix the variable name with the pl/sql procedure name when referencing it in SQL statements:
SELECT count(*)
INTO person_count
FROM person p WHERE p.suburb = p1.suburb ;
In your case you clearly wouldn't name your procedure "P1" so in fact you'd have something like:
SELECT count(*)
INTO person_count
FROM person p WHERE p.suburb = count_suburb_objects.suburb ;
Your code is now immune to variable name capture -- as a bonus your text editor might highlight all the instances where you've used a variable name in a SQL statement when you double-click on the procedure name.
First, create indices for case-insensitive search:
CREATE INDEX idx_person_suburb_u ON person(upper(suburb))
/
CREATE INDEX idx_property_suburb_u ON property(upper(suburb))
/
Second, use prefixes for procedure parameters and local variables:
CREATE OR REPLACE PROCEDURE p1(p_location VARCHAR2)
IS
v_person_count NUMBER;
v_property_count NUMBER;
v_location VARCHAR2(32767);
BEGIN
IF p_location IS NOT NULL THEN
v_location := upper(p_location);
SELECT count(*) INTO v_person_count
FROM person WHERE upper(suburb) = v_location ;
SELECT count(*) INTO v_property_count
FROM property WHERE upper(suburb) = v_location ;
ELSE
SELECT count(*) INTO v_person_count
FROM person WHERE upper(suburb) IS NULL;
SELECT count(*) INTO v_property_count
FROM property WHERE upper(suburb) IS NULL;
END IF;
dbms_output.put_line('Number of People :' || v_person_count);
dbms_output.put_line('Number of Property :' || v_property_count);
END;
/
I have been using Oracle(10g.2) as a PHP programmer for almost 3 years, but when I gave an assignment, I have tried to use the ref cursors and collection types for the first time. And I
've searched the web, when I faced with problems, and this ora-00932 error really overwhelmed me. I need help from an old hand.
Here is what I've been tackling with,
I want to select rows from a table and put them in a ref cursor, and then with using record type, gather them within an associative array. And again from this associative array, make a ref cursor. Don't ask me why, I am writing such a complicated code, because I need it for more complex assignment. I might be sound confusing to you, thus let me show you my codes.
I have 2 types defined under the types tab in Toad. One of them is an object type:
CREATE OR REPLACE
TYPE R_TYPE AS OBJECT(sqn number,firstname VARCHAR2(30), lastname VARCHAR2(30));
Other one is collection type which is using the object type created above:
CREATE OR REPLACE
TYPE tr_type AS TABLE OF r_type;
Then I create a package:
CREATE OR REPLACE PACKAGE MYPACK_PKG IS
TYPE MY_REF_CURSOR IS REF CURSOR;
PROCEDURE MY_PROC(r_cursor OUT MY_REF_CURSOR);
END MYPACK_PKG;
Package Body:
CREATE OR REPLACE PACKAGE BODY MYPACK_PKG AS
PROCEDURE MY_PROC(r_cursor OUT MY_REF_CURSOR) AS
rcur MYPACK_PKG.MY_REF_CURSOR;
sql_stmt VARCHAR2(1000);
l_rarray tr_type := tr_type();
l_rec r_type;
BEGIN
sql_stmt := 'SELECT 1,e.first_name,e.last_name FROM hr.employees e ';
OPEN rcur FOR sql_stmt;
LOOP
fetch rcur into l_rec;
exit when rcur%notfound;
l_rarray := tr_type( l_rec );
END LOOP;
CLOSE rcur;
--OPEN r_cursor FOR SELECT * FROM TABLE(cast(l_rarray as tr_type) );
END MY_PROC;
END MYPACK_PKG;
I commented out the last line where I open ref cursor. Because it's causing another error when I run the procedure in Toad's SQL Editor, and it is the second question that I will ask.
And lastly I run the code in Toad:
variable r refcursor
declare
r_out MYPACK_PKG.MY_REF_CURSOR;
begin
MYPACK_PKG.MY_PROC(r_out);
:r := r_out;
end;
print :r
There I get the ora-00932 error.
The way you are using the REF CURSOR is uncommon. This would be the standard way of using them:
SQL> CREATE OR REPLACE PACKAGE BODY MYPACK_PKG AS
2 PROCEDURE MY_PROC(r_cursor OUT MY_REF_CURSOR) AS
3 BEGIN
4 OPEN r_cursor FOR SELECT e.empno,e.ENAME,null FROM scott.emp e;
5 END MY_PROC;
6 END MYPACK_PKG;
7 /
Corps de package crÚÚ.
SQL> VARIABLE r REFCURSOR
SQL> BEGIN
2 MYPACK_PKG.MY_PROC(:r);
3 END;
4 /
ProcÚdure PL/SQL terminÚe avec succÞs.
SQL> PRINT :r
EMPNO ENAME N
---------- ---------- -
7369 SMITH
7499 ALLEN
7521 WARD
7566 JONES
7654 MARTIN
[...]
14 ligne(s) sÚlectionnÚe(s).
I'm not sure what you are trying to accomplish here, you're fetching the ref cursor inside the procedure and then returning another ref cursor that will have the same data. I don't think it's necessary to fetch the cursor at all in the procedure. Let the calling app do the fetching (here the fetching is done by the print).
Update: why are you getting the unhelpful error message?
You're using a cursor opened dynamically and I think that's part of the reason you are getting the unhelpful error message. If we use fixed SQL the error message is different:
SQL> CREATE OR REPLACE PACKAGE BODY MYPACK_PKG AS
2 PROCEDURE MY_PROC(r_cursor OUT MY_REF_CURSOR) AS
3 TYPE type_rec IS RECORD (qn number,
4 firstname VARCHAR2(30),
5 lastname VARCHAR2(30));
6 lt_record type_rec; /* Record type */
7 lt_object r_type; /* SQL Object type */
8 BEGIN
9 OPEN r_cursor FOR SELECT e.empno,e.ENAME,null FROM scott.emp e;
10 FETCH r_cursor INTO lt_record; /* This will work */
11 FETCH r_cursor INTO lt_object; /* This won't work in 10.2 */
12 END MY_PROC;
13 END MYPACK_PKG;
14 /
Package body created
SQL> VARIABLE r REFCURSOR
SQL> BEGIN
2 MYPACK_PKG.MY_PROC(:r);
3 END;
4 /
BEGIN
*
ERREUR Ó la ligne 1 :
ORA-06504: PL/SQL: Return types of Result Set variables or query do not match
ORA-06512: at "APPS.MYPACK_PKG", line 11
ORA-06512: at line 2
I outlined that currently in 10.2 you can fetch a cursor into a PLSQL record but not in a SQL Object.
Update: regarding the PLS-00306: wrong number or types of arguments
l_rarray is a NESTED TABLE, it needs to be initialized and then extended to be able to store elements. For example:
SQL> CREATE OR REPLACE PACKAGE BODY MYPACK_PKG AS
2 PROCEDURE MY_PROC(r_cursor OUT MY_REF_CURSOR) AS
3 lr_array tr_type := tr_type(); /* SQL Array */
4 BEGIN
5 FOR cc IN (SELECT e.empno, e.ENAME, NULL lastname
6 FROM scott.emp e) LOOP
7 lr_array.extend;
8 lr_array(lr_array.count) := r_type(cc.empno,
9 cc.ename,
10 cc.lastname);
11 /* Here you can do additional procedural work on lr_array */
12 END LOOP;
13 /* then return the result set */
14 OPEN r_cursor FOR SELECT * FROM TABLE (lr_array);
15 END MY_PROC;
16 END MYPACK_PKG;
17 /
Corps de package crÚÚ.
SQL> print r
SQN FIRSTNAME LASTNAME
---------- ------------------------------ -----------
7369 SMITH
7499 ALLEN
7521 WARD
[...]
14 ligne(s) sÚlectionnÚe(s).
For further reading you can browse the documentation for PL/SQL collections and records.