I would like to use a before-after-pattern approach with PL/SQL (PSEUDO CODE):
Pattern method:
procedure doIt(DO_SOMETHING)
is
l_cnt pls_integer := 1;
begin
loop
begin
DO_SOMETHING;
exit;
exception
when exception changed then
if l_cnt = 2 then
--raise exception...
else
l_cnt := l_cnt + 1;
end if;
end;
end loop;
end;
And execute it like this:
begin
doIt(execute immediate sql_statement using in or out);
end;
As you can see, I would like to use different dynamic sql statements (execute immediate with one or more in and out variables) but always the same before-after-pattern approach.
Have someone an idea how I can solve this problem?
This solution is not so nice, but it works. Perhaps you can start from it and make something better:
I used 2 parameters in the doIt function-
1) the command to execute immediate
2) the arguments as an anydata type
In the execute immediate command I've put all the logic from translating the anydata to some type which I created to wrap the IN OUT parameters.
here is the code:
a type like this should be created for every different command:
create or replace type some_type as object(a number, b number);
/
this is the procedure :
create or replace procedure doIt(aa in varchar2, param IN OUT anydata) is
begin
execute immediate aa using in out param;
end doIt;
/
and this is how I call it (in this example I just selected count(*) from dual into some OUT param):
declare
i number;
prm some_type;
ad anydata;
a number;
b number;
begin
prm := new some_type(a,b);
ad := anydata.convertobject(prm);
doIt('declare prmAd anydata := :0; prm1 some_type; x number; begin x := prmAd.getobject(prm1); select count(*) into prm1.a from dual; :0 := anydata.convertobject(prm1); end;', ad);
i := ad.GetObject(prm);
dbms_output.put_line(prm.a);
end;
basically you can add to the doIt procedure what ever you want, and run with it any command.
I guess you can make things nices- move some of the execute immediate string to the doIt procedure, maybe declare the type better and so on.
I can't find much information on Before/After pattern but looking at what your trying to achieve it could look something like this:
create or replace
procedure doIt(stmt in varchar2, param in varchar2)
is
l_cnt pls_integer := 1;
begin
loop
begin
execute immediate stmt using param;
exit;
exception
when others then
if l_cnt = 2 then
raise;
else
l_cnt := l_cnt + 1;
end if;
end;
end loop;
end;
/
-- run it
exec doIt('insert into my_table (col1) values ( :val1 )', 'richard' );
The problem here is that if you want to pass two parameters then you will have to override 'doIt' eg:
procedure doIt(stmt in varchar2, param1 in varchar2, param2 in varchar2)
...
using param1, param2
Also the other caveat is passing different data types into param. Again you could override the procedure with the correct data type - be warned this could get messy with multiple data types or number of arguments.
Related
I have a variable which contains a name of a stored procedure. I want to execute this procedure with dynamic sql, so I did this :
var_procedure is the variable which contain the name of my stored procedure. firstparameter and secondone are my varchar parameters for the stored procedure.
execute immediate var_procedure||'('''||firstparameter||''','''||secondone||''')';
It's not working and I have this basic error printed :
ORA-00900: invalid SQL statement
Do you have a solution ?
Let try it step by step.
This is your procedure.
create or replace procedure my_proc (a varchar2, b varchar2)
as
begin
dbms_output.put_line ('a= '||a||' b = '||b);
end;
/
This is the way how to call it directly in PL/SQL
begin
my_proc('x','y');
end;
/
This is the Wrong Way how to execute is dynamically.
Why? Concatenating parameters in the statement is a bad practice enabling SQL injection.
declare
a varchar2(5) := 'a';
b varchar2(5) := 'b';
begin
execute immediate 'begin
my_proc('''||a||''','''||b||''');
end;';
end;
/
This is the right way using bind variables:
declare
a varchar2(5) := 'a';
b varchar2(5) := 'b';
begin
execute immediate 'begin
my_proc(:a,:b);
end;' USING a,b;
end;
/
To pass the name of the procedure dynamically you have to concatenate the statement, so take some care to avoid SQL Injection if the parameter is not under your control (minimum is to limit the length to 30 characters).
declare
proc_name varchar2(30) := 'my_proc';
a varchar2(5) := 'a';
b varchar2(5) := 'b';
begin
execute immediate 'begin '||
proc_name||'(:a,:b);
end;' USING a,b;
end;
/
I am trying to pass dbms_sql.number_table from one procedure to another and then using it inside a dynamic plsql block. But the below code throws error as:
Error(6,17): PLS-00306: wrong number or types of arguments in call to '||'
create or replace
procedure proc1( v_in_table_name varchar2,
v_in_column_name varchar2,
v_in dbms_sql.number_table)
as
plsql_block varchar2(4000);
begin
plsql_block:='declare
begin
FORALL INDX IN 1 ..'||v_in.count||' SAVE EXCEPTIONS
UPDATE '||v_in_table_name||'
Set '||v_in_column_name||'=123 WHERE col2='||v_in||'(INDX)||;
end';
execute immediate plsql_block;
end proc1;
You should make two changes:
First(and its case of error) its when you concatinate string with collection.
If you want to send collection into pl/sql block you shoud use the param.
And second you should add ; in the end of dynamic pl\sql:
create or replace
procedure proc1(v_in dbms_sql.number_table)
as
plsql_block varchar2(4000);
begin
plsql_block:='declare
l_collect dbms_sql.number_table := :number_table ;
begin
FORALL INDX IN 1 ..'||v_in.count||' SAVE EXCEPTIONS
UPDATE table_name
Set col1=123 WHERE col2=l_collect(INDX);
end;';
execute immediate plsql_block using v_in;
end proc1;
But after all changes I would like to ask: Are you realy need to use dynamic pl\sql?
There is no need for using a dynamic block. Also i dont think it can work as well. You can use this way;
create or replace
procedure proc1(v_in dbms_sql.number_table)
as
plsql_block varchar2(4000);
l_collect dbms_sql.number_table;
begin
l_collect := v_in;
FORALL INDX IN 1 ..l_collect.count SAVE EXCEPTIONS
UPDATE table_name
Set col1=123
WHERE col2=l_collect(INDX);
commit;
end proc1;
Execution:
SQL> DECLARE
var DBMS_SQL.number_table;
BEGIN
var (1) := 1;
var (2) := 2;
var (3) := 3;
proc1 (var);
END;
/
PL/SQL procedure successfully completed.
EDIT: As per your edit the code becomes like:
create or replace procedure proc1 (v_in_table_name varchar2,
v_in_column_name varchar2,
v_in dbms_sql.number_table)
as
plsql_block varchar2 (4000);
begin
plsql_block := q'[ FORALL INDX IN 1 ..v_in.count SAVE EXCEPTIONS
UPDATE :table_name
Set :col1=123
WHERE col2=v_in(INDX)]';
execute immediate plsql_block using v_in_table_name, v_in_column_name;
commit;
end proc1;
Background
I'm trying to make a re-usable PL/SQL procedure to move data from one
database to another.
For this purpose, I'm using dynamic SQL.
The procedure executes perfectly if I use a REPLACE with placeholders.
However, for security reasons, I want to use bind variables.
Question
How can I make an entire PL/SQL code block dynamic (with bind
variables)? If I use a REPLACE instead of the bind variables, it works
fine.
How to replicate
To replicate this in your database, create the following procedure as it is:
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
l_cursor_limit pls_integer := 500;
l_values_list varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := q'[
declare
l_cur_limit pls_integer := :l_cursor_limit;
cursor c_get_to_be_moved is
select :i_table_name.*, :i_table_name.rowid
from :i_table_name;
type tab_to_be_moved is table of c_get_to_be_moved%rowtype;
l_to_be_moved tab_to_be_moved;
begin
open c_get_to_be_moved;
loop
fetch c_get_to_be_moved
bulk collect into l_to_be_moved limit l_cur_limit;
exit when l_to_be_moved.count = 0;
for i in 1.. l_to_be_moved.count loop
begin
insert into :i_table_name#:i_destination values (:l_values_list);
exception
when others then
dbms_output.put_line(sqlerrm);
l_to_be_moved.delete(i);
end;
end loop;
forall i in 1.. l_to_be_moved.count
delete
from :i_table_name
where rowid = l_to_be_moved(i).rowid;
for i in 1..l_to_be_moved.count loop
if (sql%bulk_rowcount(i) = 0) then
raise_application_error(-20001, 'Could not find ROWID to delete. Rolling back...');
end if;
end loop;
commit;
end loop;
close c_get_to_be_moved;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;]';
execute immediate l_sql using l_cursor_limit, i_table_name, i_destination, l_values_list;
exception
when others then
rollback;
dbms_output.put_line(sqlerrm);
end;
/
And then you can execute the procedure with the following:
begin
move_data('MySchemaName', 'MyTableName', 'MyDatabaseLinkName');
end;
/
Due to many reasons(inability to generate an appropriate execution plan, security checking, etc.) Oracle does not allow identifiers binding (table names, schema names, column names and so on). So if it's really necessary, the only way is to hard code those identifiers after some sort of validation (to prevent SQL injection).
If I understand well, you could try a trick, by using a dynamic SQL inside a dynamic SQL.
setup:
create table tab100 as select level l from dual connect by level <= 100;
create table tab200 as select level l from dual connect by level <= 200;
create table tabDest as select * from tab100 where 1 = 2;
This will not work:
create or replace procedure testBind (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := 'insert into tabDest select * from :tableName';
execute immediate vSQL using pTableName;
end;
But this will do the trick:
create or replace procedure testBind2 (pTableName in varchar2) is
vSQL varchar2(32000);
begin
vSQL := q'[declare
vTab varchar2(30) := :tableName;
vSQL2 varchar2(32000) := 'insert into tabDest select * from ' || vTab;
begin
execute immediate vSQL2;
end;
]';
execute immediate vSQL using pTableName;
end;
I think you can do it simpler.
create or replace procedure move_data(i_schema_name in varchar2, i_table_name in varchar2, i_destination in varchar2) as
l_sql varchar2(32767);
begin
select listagg('l_to_be_moved(i).' || column_name, ', ') within group (order by column_id)
into l_values_list
from all_tab_cols
where owner = i_schema_name and
table_name = i_table_name and
virtual_column = 'NO';
l_sql := 'insert into '||i_destination||'.'||i_table_name||' select * from '||i_schema_name||'.'||i_table_name;
execute immediate l_sql;
end;
If you are concerned about SQL-Injection, have a look at package DBMS_ASSERT. This PL/SQL package provides function to validate properties of input values.
My question is pretty basic but I am complete newbie to stored procedure and need to get around quickly. Any help will be appreciated,
Below is the current stored procedure we have,
PROCEDURE get_something(
type IN VARCHAR2,
value IN VARCHAR2,
i_type OUT VARCHAR2,
i_id OUT VARCHAR2)
AS
TYPE t_array
IS
TABLE OF VARCHAR2(320);
identifers t_array;
column_name VARCHAR2(32);
info_qry VARCHAR2(500);
top_row_qry VARCHAR2(500);
BEGIN
identifers := t_array('ABC');
column_name := get_column_name(type);
info_qry := 'select INS.i_id, INS.model from table1 INS where INS.'||column_name||'='''||value||''' order by INS.version desc';
top_row_qry := 'select * from ('||info_qry||') where rownum<=1';
EXECUTE immediate top_row_qry INTO i_id, i_type;
EXCEPTION
WHEN OTHERS THEN
i_id := NULL;
i_type := NULL;
END get_something;
Now when I execute this procedure, it gives me one record due to the rownum condition.
My requirement is to remove the rownum from the top_row_qry, so result would be multiple rows.
I want to store each field into some variable out of which I will use one of the variable for some comparison in the procedure itself.
So basically i want to store the results which I can later loop over and compare with a list of values.
Also, I need to define the list of values in this procedure itself.
Something like below:
list_of_vals:= t_array('ABC','XYZ');
Can anyone help me on this.
I guess you need to use cursor as OUT parameter for your procedure and later use it for your requirement.
Also for "I need to define the list of values in this procedure itself."
you can use a collection the way I have used in the procedure code (v_array).
You can then use a loop to traverse through multiple values for the v_array.
CREATE ORE REPLACE PROCEDURE get_something(type IN VARCHAR2,
value IN VARCHAR2,
out_param OUT sys_refcursor)
AS
TYPE t_array IS TABLE OF VARCHAR2(320);
v_array t_array:=t_array();
-- identifers t_array;
column_name VARCHAR2(32);
info_qry VARCHAR2(500);
top_row_qry VARCHAR2(500);
BEGIN
--To define multiple values for a collection e.g. ABC, XYZ
v_array.EXTEND(2) ;
v_array(1):='ABC' ;
v_array(2):='XYZ' ;
--identifers := t_array('ABC') ;
column_name := get_column_name(TYPE) ;
info_qry := 'select INS.i_id, INS.model from table1 INS where INS.'||column_name||'='''||VALUE||''' order by INS.version desc';
top_row_qry := 'select * from ('||info_qry||')' ;
OPEN out_param FOR top_row_qry ;
EXCEPTION
WHEN OTHERS THEN
OPEN out_param FOR 'select null, null from dual' ;
END get_something ;
/
--To execute the procedure
DECLARE
lv_type VARCHAR2(200);
lv_id NUMBER;
lv_cur SYS_REFCURSOR;
BEGIN
get_something('param1','param2',lv_cur);
LOOP
FETCH lv_cur INTO lv_id,lv_type;
EXIT WHEN lv_cur%NOTFOUND;
--Do something.....
END LOOP;
END;
I am writing a stored procedure to update data in a table where I will pass a string with the new data (col1='new values', col2='new 2 values'). But when I am compiling my stored procedure , i am getting an error :- "missing equal sign".
Even i tried doing it in a different way (commented code in proc) but that is also giving an error.
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(update_values IN VCHAR2,myid IN INT)
sqlStmt VARCHAR2(1024);
BEGIN
UPDATE MY_TEST_TABLE SET update_values WHERE (TEST_Id = myid);
--sqlStmt := 'UPDATE MY_TEST_TABLE SET ' || update_values || ' WHERE TEST_Id = ' ||myid ;
-- EXECUTE sqlStmt;
END;
Try this (untested):
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(update_values IN VARCHAR2, myid IN NUMBER) AS
sqlStmt VARCHAR2(1024);
BEGIN
sqlStmt := 'UPDATE MY_TEST_TABLE SET ' || update_values || ' WHERE TEST_Id = ' || myid;
EXECUTE IMMEDIATE sqlStmt;
END;
/
The datatype of your first parameter should be VARCHAR2 (maybe just a typo in your post)
Syntax of simple update statement in Oracle is:
Update <table_name>
set <column_name> = some_value
where <conditions..>
You update statement is missing = some_value part that you need to provide.
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(P_update_values IN CHARVAR2, p_myid IN INT)
BEGIN
UPDATE MY_TEST_TABLE
SET col1 = p_update_values
WHERE TEST_Id = p_myid;
END;
/
Using Dynamic SQL, although not required in this case:
CREATE OR REPLACE PROCEDURE "MY_UPDATE_PROC"(p_update_values IN VARCHAR2, p_myid IN NUMBER) AS
sqlStmt VARCHAR2(1024);
BEGIN
sqlStmt := 'UPDATE MY_TEST_TABLE SET col1 = :a WHERE TEST_Id = :b';
EXECUTE IMMEDIATE sqlStmt USING p_update_values, p_myid;
END;
/
Things to be noted:
1) Always use meaningful and different names that are other than column names for the parameters.
2) Always use bind variables, :a and :b in above examples, to avoid SQL Injections and improve overall performance if you are going to call this procedure multiple times.