How to avoid no data exception? - database

I am working on a project and I have a database schema written in Oracle db for a library. I have some issues when trying to build a function which should provide some recommendations for the user.
The recommendations should be given based on the last books genres some user had borrowed in the past month and the most read books of all users. Every time I try to run the function I get a no data error even if I have values in the tables.
CREATE OR REPLACE FUNCTION getGenre(bookISBN IN VARCHAR2) RETURN VARCHAR2
AS
v_bookGenre VARCHAR2(100);
BEGIN
select genre into v_bookGenre from (select genre from books where bookISBN=isbn);
return v_bookGenre;
END getGenre;
/
CREATE OR REPLACE TYPE t IS TABLE OF varchar2(200);
/
create or replace type typeRecom is VARRAY(200) of VARCHAR2(200);
/
CREATE OR REPLACE FUNCTION topRecommandations(idUser IN VARCHAR2)
RETURN typeRecom
AS
lastGenres t :=t();
readedBooks t:=t();
topTen typeRecom;
v_i NUMBER := 1;
v_j NUMBER := 1;
verifDateBook NUMBER:=0;
verifDateLoan NUMBER:=0;
BEGIN
select count(*) into verifDateBook from books;
if(verifDateBook = 0) then
raise no_data_found;
end if;
select count(*) into verifDateLoan from loans;
if(verifDateLoan = 0) then
raise no_data_found;
end if;
lastGenres.extend();
for i in (select genre from books join loans on books.isbn=loans.bookId where loanDate>add_months(sysdate,-1) and idUser=loans.regNo order by genre desc) loop
DBMS_OUTPUT.PUT_LINE(i.genre);
lastGenres.extend();
lastGenres(lastGenres.count) := i.genre;
end loop;
/*select title into bookTitles from book join loan on book.isbn=loan.regNo;*/
readedBooks.extend();
for i in (select bookId from loans group by bookId order by count(bookId) desc) loop
readedBooks.extend();
readedBooks(readedBooks.count) := i.bookId;
end loop;
--select bookId into readedBooks from loan order by count(bookId) desc;
for v_i IN 1..50 LOOP
for v_j in 1..50 LOOP
DBMS_OUTPUT.PUT_LINE(lastGenres(v_j));
DBMS_OUTPUT.PUT_LINE(getGenre(readedBooks(v_i)));
if(lastGenres(v_j)=getGenre(readedBooks(v_i))) then
topTen(v_i) := getGenre(readedBooks(v_j));
end if;
EXIT WHEN topTen.count=10;
END LOOP;
EXIT WHEN topTen.count=10;
end LOOP;
for v_cont in 1..10 LOOP
--if(topTen.count <= 10) then
DBMS_OUTPUT.PUT_LINE(topTen(v_cont));
--end if;
end loop;
return topTen;
EXCEPTION
WHEN no_data_found THEN
DBMS_OUTPUT.PUT_LINE('Nu exista valori.');
END topRecommandations;
Expected result: a list with 10 recommended books.
Actual result: no data found error.

Unless I'm wrong, the only place that might return NO_DATA_FOUND is the getGenre function, as it has a SELECT some value into a variable, and no exceptions are handled. Other SELECTs use aggregates (which will return 0 if there's nothing there).
Therefore, fix that function, e.g.
CREATE OR REPLACE FUNCTION getGenre(bookISBN IN VARCHAR2)
RETURN VARCHAR2
AS
v_bookGenre VARCHAR2(100);
BEGIN
select genre
into v_bookGenre
from books where bookISBN = isbn;
return v_bookGenre;
EXCEPTION
when no_data_found then
return null;
END getGenre;
By the way, why did you use an inline view in that function? Why didn't you simply return genre from the table itself (like I did)?

Related

how to print out the data from stored procedure in oracle

As my assignment needs me to use stored procedure, i wanted to print out the data in the table, but it only shows (PL/SQL procedure successfully completed.) but none of the data print out.Below is my code:
CREATE OR REPLACE PROCEDURE showMenu(Merchant_N IN Varchar2)
AS
Food_Name Varchar2(30) := null;
BEGIN
SELECT Food_Name
INTO Food_Name
From Food , Merchant
Where Merchant_Name = Merchant_N AND
Merchant.Merchant_ID = Food.Merchant_ID;
dbms_output.enable;
dbms_output.put_line('The food is');
dbms_output.put_line(Food_Name);
END;
EXEC showMenu(‘KFC’)
For your second question - processing multiple rows:
CREATE OR REPLACE PROCEDURE showMenu(Merchant_N IN Varchar2)
AS
Food_Name Varchar2(30) := null;
BEGIN
for x in (SELECT Food_Name
-- INTO Food_Name not needed for cursor loops
From Food , Merchant
Where Merchant_Name = Merchant_N AND
Merchant.Merchant_ID = Food.Merchant_ID)
loop
-- dbms_output.enable; < not needed
dbms_output.put_line('The food is');
dbms_output.put_line(x.Food_Name); --
end loop;
END;
Do you really want two lines each? How about
loop
dbms_output.put_line('The food is '|| x.Food_Name);
end loop;
And you really should use ANSI join syntax:
From Food
join Merchant on Food.Merchant_ID = Merchant.Merchant_ID
Where Merchant_Name = Merchant_N
set serveroutput on
set the serveroutput to on then execute the proc again then you should see an output

Oracle PL/SQL Assign each value from a cursor(from function) to another cursor one by one

I have a function called GET_CLIENT_IN_SED(return sys_refcursor), it gives me a list of id numbers(single column). Now, in a procedure, I am trying to loop through each (one by one) of that values and use it for calling a second procedure (it needs a client id parameter).
PROCEDURE GET_ORDINARY_CLIENT;
PROCEDURE GET_ORDINARY_CLIENT_BY_SED
( sed_in IN varchar2, client_sed OUT SYS_REFCURSOR )
IS
ordinary_clients sys_refcursor;
BEGIN
ordinary_clients := GET_CLIENT_IN_SED(sed_in);
for item in ordinary_clients loop
client_sed := client_sed + ordinary_clients(i);
end loop;
END;
As far i could understand you need to do something like :
Function:
This function would take input as number and return a refcursor. Similar to your requirement.
CREATE OR REPLACE FUNCTION get_num_sysrefcur (num IN NUMBER)
RETURN SYS_REFCURSOR
AS
my_cursor SYS_REFCURSOR;
BEGIN
--
OPEN my_cursor FOR
WITH ntable
AS (SELECT 1 ID, 111 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 2 ID, 222 AGT, 'ABC' DESCRIP FROM DUAL
UNION ALL
SELECT 1 ID, 333 AGT, 'ABC' DESCRIP FROM DUAL)
SELECT AGT FROM ntable WHERE ID = num;
RETURN my_cursor;
END;
/
Block ( In your case Procedure )
-- This anonymous block will loop through the records return from the sys_refcursor. Similiar to you need where you want the second procedure to use the value of sys_refcursor and loop it(You can create procedure in place of this anonymous block).
DECLARE
a NUMBER := 1;
TYPE ta IS TABLE OF NUMBER
INDEX BY PLS_INTEGER;
b ta;
x SYS_REFCURSOR;
BEGIN
x := get_num_sysrefcur (a);
fetch x bulk collect into b;
for i in 1..b.count
loop
-- Displaying the result of the ref_cursor.
DBMS_OUTPUT.put_line (b(i));
end loop;
END;
To loop through a ref cursor is not like looping through an array or table which explains why your FOR...LOOP is not working.
In short, instead of a collection, the ref_cursor is more of a "pointer" or an "iterator" over a collection. In this other question you will find a quite clear example of iterating through a ref_cursor using FETCH.
How to use record to loop a ref cursor?
An example with your data would look like this :
PROCEDURE GET_ORDINARY_CLIENT_BY_SED(sed_in IN VARCHAR2,
client_sed OUT SYS_REFCURSOR) IS
ordinary_clients SYS_REFCURSOR;
clt NUMBER; -- assuming your cursor contains strictly numbers
BEGIN
ordinary_clients := GET_CLIENT_IN_SED(sed_in);
LOOP
FETCH ordinary_clients
INTO clt;
EXIT WHEN ordinary_clients%NOTFOUND;
dbms_output.put_line(clt);
-- do some other things here with your number
END LOOP;
END;

Atomic Transaction with nested BEFORE INSERT/UPDATE Triggers

Currently i am implementing a procedure, which creates a couple of rows in some related tables out of a template. So my Procedure consists of a SAVEPOINT followed by some INSERT statements on different tables, and a Cursor to insert some more rows to other tables while referencing on the newly created primary keys.
Each of those tables has an BEFORE INSERT/UPDATE trigger defined which has the purpose to:
Get a new primary key from a sequencer if it is not defined in the INSERT statement (there are cases where I need to set the Primary key explicitely to reference it later on in the same transaction)
Set some default values if they are NULL
Set auditing fields (last_change_date, last_change_user, etc..)
The transaction fails with ORA-04091: table is mutating, trigger/function may not see it
I am understanding, that I could Workaround this, by declaring PRAGMA AUTONOMOUS TRANSACTION in each Trigger, but my Transaction would not be atomic any more then, as it is the requirement that all those datasets should be created/inserted as a whole or None of them.
So what am I doing wrong in the design of my database?
UPDATE: This is the Code of the trigger
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
IF :new.id is NULL or :new.id = 0 THEN
SELECT SEQ_AUFTRAG.nextval into :new.id from dual;
END IF;
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer FROM AUFTRAG WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
--DEFAULT Values
IF :new.BETR_GRENZWERTE_RELEVANT is NULL THEN
SELECT 0 INTO :new.BETR_GRENZWERTE_RELEVANT FROM dual;
END IF;
IF :new.DOKUMENTE_ABGELEGT is NULL THEN
SELECT 0 INTO :new.DOKUMENTE_ABGELEGT FROM dual;
END IF;
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
SELECT 1 INTO :new.EXT_ORG FROM dual;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
You can write it more compact like this:
CREATE TRIGGER TRG_AUFTRAG_B_IU
BEFORE INSERT OR UPDATE
ON AUFTRAG
FOR EACH ROW
BEGIN
IF INSERTING THEN
:new.id = NVL(NULLIF(:new.id, 0), SEQ_AUFTRAG.nextval);
--DEFAULT Values
:new.BETR_GRENZWERTE_RELEVANT := NVL(:new.BETR_GRENZWERTE_RELEVANT, 0);
:new.DOKUMENTE_ABGELEGT := NVL(:new.DOKUMENTE_ABGELEGT, 0);
IF :new.EXT_ORG is NULL or :new.EXT_ORG < 1 THEN
:new.EXT_ORG := 1;
END IF;
:new.ERSTELLT_VON := nvl(:new.ERSTELLT_VON,user);
:new.ERSTELLT_DATUM := nvl(:new.ERSTELLT_DATUM,sysdate);
END IF;
:new.GEAENDERT_VON := user;
:new.GEAENDERT_DATUM := sysdate;
END;
Only "problem" is this part
IF :new.nummer is NULL or :new.nummer = 0 THEN
SELECT nvl(MAX(NUMMER),0)+1 INTO :new.nummer
FROM AUFTRAG
WHERE EXTRACT(YEAR from DATUM) = EXTRACT(YEAR from :new.DATUM);
END IF;
This one you should put into your procedure or in a statement trigger (i.e. without FOR EACH ROW clause) like this:
CREATE TRIGGER TRG_AUFTRAG_B_A
AFTER INSERT ON AUFTRAG
BEGIN
UPDATE
(SELECT ID, NUMMER,
ROW_NUMBER() OVER (PARTITION BY EXTRACT(YEAR from DATUM) ORDER BY ID) as N
FROM AUFTRAG)
SET NUMMER = N
WHERE NUMMER IS NULL;
END;

PL/SQL How to convert a record to an assoc array

I am trying to fetch data from a table(employee) with cursor and save to asso. array. However, fetching data with cursor to a record is more straight-forward and it is troublesome to convert a record to an assoc array(arr). Code below is what I am trying to fetch data to a assoc array and improvement is needed. Or any approaches other than cursor? Thank you.
DECLARE
TYPE AssoArray IS TABLE OF varchar2(30) INDEX BY varchar2(30);
arr AssoArray;
table_rec employee%rowtype;
CURSOR cur IS
SELECT * FROM employee;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr('col1') := table_rec.col1;
arr('col2') := table_rec.col2;
arr('col3') := table_rec.col3;
...
arr('col50') := table_rec.col50;
-- end of section
-- do sth
END LOOP;
END;
Could you explain what do you want to get. But I see in your code arr AssoArray will contain only one last fetched row and will be rewrite each cycle. Are you realy need it? My suggestion is you want to get some rows of table as associated array. If its true, you may create the array as table of rowtype and (for example) indexed it by id(if you ID-s is integer).
TYPE AssoArray IS TABLE OF employee%rowtype INDEX BY PLS_INTEGER;
For example
create table tmp_table_01 as select * from all_objects where rownum < 11;
DECLARE
TYPE AssoArray IS TABLE OF tmp_table_01%rowtype INDEX BY varchar2(30);
arr AssoArray;
table_rec tmp_table_01%rowtype;
CURSOR cur IS
SELECT * FROM all_objects where rownum < 20;
BEGIN
OPEN cur;
LOOP
FETCH cur into table_rec;
EXIT WHEN cur%notfound;
-- how to improve the code in the section below,
arr(table_rec.object_name) := table_rec;
-- end of section
dbms_output.put_line(table_rec.object_name ||' '||arr(table_rec.object_name).object_id );
-- do sth
END LOOP;
END;
EDIT:
If you want to make comraisons by table structure you may use Dynamic SQL. NExt code get data from table TMP_TABLE_01 sort it by object_id, compare neighbour rows, and return count of difference row.
DECLARE
l_sql varchar2(32767);
TYPE t_cols_type IS table of varchar2(30);
l_cols t_cols_type ;
l_result number;
BEGIN
SELECT c.COLUMN_NAME
BULK COLLECt INTO l_cols
FROM user_tab_cols c
WHERE c.TABLE_NAME = 'TMP_TABLE_01';
l_sql := 'WITH s AS (SELECT t.*, row_number() OVER (ORDER BY object_id) AS rn FROM TMP_TABLE_01 t)
SELECT count(*)
FROM s
JOIN s sub_s ON s.rn = sub_s.rn - 1
where 1=0 ';
FOR i IN 1 .. l_cols.last LOOP
l_sql := l_sql || ' OR decode(s.'||l_cols(i)||',sub_s.'||l_cols(i)||',1,0) = 0';
END LOOP;
dbms_output.put_line(l_sql);
EXECUTE IMMEDIATE l_sql
INTO l_result ;
dbms_output.put_line('count of conseuence different rows ' ||l_result);
END;

Converting Cursor from PLSQL into TSQL

i m migrating my following plsql function from oracle into mssql 2008 but dont know how to convert cursor in a while loop.Could u help?
CREATE OR REPLACE function f_genel_iskonto (p_ID_MUSTERI_SIRKET in number, p_BILGI_TIP in NUMBER)
return number
is
v_iskonto number;
begin
v_iskonto:=null;
for c in (
SELECT an.mt_iskonto_oran, an.aktif, an.id_anlasma
FROM lu_anlasma an
WHERE an.id_musteri_sirket = p_ID_MUSTERI_SIRKET AND an.id_durum = 9
AND (TRUNC (SYSDATE) BETWEEN an.baslangic AND an.bitis)
ORDER BY an.baslangic DESC
) loop
if p_BILGI_TIP=1 then
v_iskonto:=c.mt_iskonto_oran;
end if;
if p_BILGI_TIP=2 then
v_iskonto:=c.aktif;
end if;
if p_BILGI_TIP=3 then
v_iskonto:=c.id_anlasma;
end if;
exit;
end loop;
return v_iskonto;
exception
when others then
return null;
end;
You don't have to convert a cursor/loop in this case as it only looks at the first record.
create function f_genel_iskonto (#p_ID_MUSTERI_SIRKET int, #p_BILGI_TIP int)
returns int
as
begin
declare #result int
SELECT top 1
#result = case #p_BILGI_TIP
when 1 then an.mt_iskonto_oran
when 2 then an.aktif
when 3 then an.id_anlasma
end
FROM lu_anlasma an
WHERE an.id_musteri_sirket = #p_ID_MUSTERI_SIRKET AND an.id_durum = 9
AND (current_timestamp BETWEEN an.baslangic AND an.bitis)
ORDER BY an.baslangic DESC
return #result
end
And you will probably need to tweak the date comparison to do what you want.

Resources