Fellow Monks,

I have been using Oracle PL/SQL fairly extensively these days, and came up with a module that creates Perl wrappers for PL/SQL procedures, so that I can call them just like Perl functions (without the need to write SQL statements or to manually bind parameters). It seems to work fine, so I thought I can put in on CPAN, but I'd like to have some feedback from the Monastry first.

Update
Thanks for the feedback everyone. I have refactored, renamed, repackaged and taken the discussion over to the dbi-users mailing list.

Usage is like this

use Oracle::Procedures qw(sysdate); my $conn = DBI->connect(..); print sysdate($conn);

Like I said, it works. But what do you think about the interface? Would you use it? There is also DBIx::Procedures::Oracles, which does a similar task.

Also, when binding parameters, my module does not specify datatypes. I never do that. Seems to work just fine. Is there a problem with this approach?

Are there any obvious situations where it would not work? One thing that comes to mind are functions that return cursors.

And of course: Is the module name (Oracle::Procedures) any good?

Here is the module

package Oracle::Procedures; use strict; use warnings; use Carp qw(croak); our $VERSION = '0.01'; sub __run_procedure{ my $dbh = shift; croak "expected a database handle as first parameter, but got +nothing" unless $dbh; croak "expected an Oracle database handle as first parameter, +but got $dbh" unless ref $dbh; my $name = shift; croak "expected a procedure name to run against the Oracle dat +abase, but got nothing" unless $name; my $params; # if there is one more arg and it is a hashref, then we use na +med parameters if (@_ == 1 and ref $_[0] eq 'HASH') { return __run_procedure_named($dbh, $name, $_[0]); } # otherwise they are positional parameters my $sql = "begin $name"; if (@_){ $sql .= '(' . join (',' , map ({ '?'} @_ )) . ')'; } $sql .= '; end;'; # print $sql; # prepare $sql = $dbh->prepare($sql); # bind my $i = 1; foreach (@_){ $sql->bind_param($i++, $_); } # execute $sql->execute; } sub __run_procedure_named{ my ($dbh, $name, $params) = @_; my $sql = "begin $name"; my @p = sort keys %$params; if (@p){ @p = map { "$_ => :$_" } @p; $sql .= '(' . join (',', @p) . ')'; } $sql .= '; end;'; # print $sql; # prepare $sql = $dbh->prepare($sql); # bind foreach (keys %$params){ $sql->bind_param(":$_", $params->{$_}); } # execute $sql->execute; } sub __run_function{ my $dbh = shift; croak "expected a database handle as first parameter, but got +nothing" unless $dbh; croak "expected an Oracle database handle as first parameter, +but got $dbh" unless ref $dbh; my $name = shift; croak "expected a function name to run against the Oracle data +base, but got nothing" unless $name; my $params; # if there is one more arg and it is a hashref , then we use w +ith named parameters if (@_ == 1 and ref $_[0] eq 'HASH') { return __run_function_named($dbh, $name, $_[0]); } # otherwise they are positional parameters my $sql = "begin ? := $name"; if (@_){ $sql .= '(' . join (',' , map ({ '?'} @_ )) . ')'; } $sql .= '; end;'; # print $sql; # prepare $sql = $dbh->prepare($sql); # bind my $r; my $i = 1; $sql->bind_param_inout($i++, \$r, 100); foreach (@_){ $sql->bind_param($i++, $_); } #execute $sql->execute; return $r; } sub __run_function_named{ my ($dbh, $name, $params) = @_; my $sql = "begin :perl_oracle_procedures_ret := $name"; my @p = sort keys %$params; if (@p){ @p = map { "$_ => :$_" } @p; $sql .= '(' . join (',', @p) . ')'; } $sql .= '; end;'; # print $sql; # prepare $sql = $dbh->prepare($sql); # bind my $r; $sql->bind_param_inout(':perl_oracle_procedures_ret', \$r, 100 +); foreach (keys %$params){ $sql->bind_param(":$_", $params->{$_}); } # execute $sql->execute; return $r; } sub run{ my $w = wantarray; # in void context run a procedure return __run_procedure(@_) unless defined $w; # in non-void context run a function return __run_function( @_); } sub __run{ my $w = shift; my $name = shift; my $dbh = shift; # in void context run a procedure return __run_procedure($dbh, $name, @_) unless defined $w; # in non-void context run a function return __run_function($dbh, $name, @_); } sub import { my $class = shift; my $caller = (caller)[0]; no strict 'refs'; foreach (@_) { my $name = $_; my $subname = $name; $subname =~ s/\W/_/g; *{"$caller\::$subname"} = sub { Oracle::Procedures::__run(wantarray,$name,@_) }; } } 1; __END__ =head1 NAME Oracle::Procedures - Perl extension to make Oracle stored procedures l +ook like Perl subroutines =head1 SYNOPSIS use Oracle::Procedures qw(sysdate); my $conn = DBI->connect(.....); print sysdate($conn); =head1 DESCRIPTION When developing applications for an Oracle database, it is a good idea to put all your database access code into stored procedures. This module provides a convenient way to call these stored procedures from Perl by creating wrapper subroutines that produce the necessary SQL statements, bind parameters and run the query. =head2 EXPORT Oracle::Procedures exports subroutines for any stored procedures (and functions) that you ask it to. You specify the list of procedures that you want when using the module: use Oracle::Procedures qw[ sysdate ] # gives you print sysdate($conn); Calling such a subroutine will invoke the stored procedure. The subroutines expect a DBI database handle as their first parameter. =head3 Subroutine names The names of the subroutine is derived from the name of the stored procedure. Because the procedure name can contain characters that are not valid in a Perl procedure name, it will be sanitized a little: Everything that is not a letter or a number becomes underscores. This will happen for all procedures that are part of a PL/SQL package, where the package name and the procedure name are divided by a dot. sysdate => sysdate dbms_random.random => dbms_random_random hh$$uu => hh__uu You can request stored procedures that do not exist. This will not be detected by Oracle::Procedures, but results in a database error when you try to call them. =head3 Procedures and functions Oracle::Procedures needs to know if you are about to call a function or a procedure (because the SQL is different). You have to make sure you call the wrapper subroutines in the right context. You have to call procedures in void context. # works dbms_random_initialize($conn, 12345); # fails print dbms_random_initialize($conn, 12345); You have to call functions in non-void context. # works print sysdate($conn); # fails sysdate($conn); If you try to call a function as a procedure, you will get a database error. =head3 Parameters You can pass parameters to the subroutines (only IN parameters are supported at the moment) You can use both positional and named parameters, but cannot mix the two styles in the same call. Positional parameters are passed in after the database handle, which is always the first parameter: dbms_random_initialize($conn, 12345); Named parameters are passed as a hash reference: dbms_random_initialize($conn, { val => 12345678 } ); The parameters you use have to match the parameters defined (in the database) for the stored procedure. If they do not, you will get a database error at runtime. =head2 ALTERNATIVE INTERFACE If you do not want to import wrapper functions, you can still use the SQL generation and parameter binding mechanism of Oracle::Procedures: Oracle::Procedures::run($conn, 'dbms_random.initialize', 12345); print Oracle::Procedures::run($conn, 'sysdate'); This can be useful if you do not know the names of the stored procedures at compilation time. You still have to know if it is a function or a procedure, though, because you need to call Oracle::Procedures::run in the appropriate context. =head1 SEE ALSO This module is built on top of DBI and DBD::Oracle, and you need to use these modules to establish a database connection. DBIx::Procedures::Oracle offers similar functionality. Unlike Oracle::Procedures, it takes the additional step of checking in the data dictionary if the procedures you want exist, and what parameters they need. =head1 LIMITATIONS cannot mix named and positional parameters only IN parameters (this is expected to be fixed in a future release) =head1 AUTHOR Thilo Planz, E<lt>thilo@cpan.orgE<gt> =head1 COPYRIGHT AND LICENSE Copyright 2004 by Thilo Planz This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut

Replies are listed 'Best First'.
Re: calling Oracle stored procedures
by VSarkiss (Monsignor) on Nov 24, 2004 at 16:08 UTC

    I haven't read your code carefully, but I do have some general comments.

    If you're using a DBI handle, then I think your module definitely should have "DBI" in its name somewhere. I'd avoid Oracle:: entirely. You mention that your module is similar to an existing DBIx::Procedures::Oracle. What are the differences? If yours is a simplification, consider naming it DBIx::Procedures::Oracle::Simple or something similar.

    Second, I'm not a big fan of using the calling context to determine if a function or a procedure is being called. It'd be nice (although maybe too slow) to look at the schema to find what type of object is being called, then Doing The Right Thing.

    Just my 2 cents.

      You mention that your module is similar to an existing DBIx::Procedures::Oracle. What are the differences? If yours is a simplification, consider naming it DBIx::Procedures::Oracle::Simple or something similar.

      No, it is not a simplification. Just works differently. The main difference is that DBIx::Procedures::Oracle produces its wrapper code at runtime (after consulting the database if there is such a stored procedure), where as I want to do it at compile-time (without connecting to the DB before actually executing the code).

      Second, I'm not a big fan of using the calling context to determine if a function or a procedure is being called. It'd be nice (although maybe too slow) to look at the schema to find what type of object is being called, then Doing The Right Thing.

      That is how DBIX::Procedures::Oracle is working. I am aware that depending on the calling context is a bit too implicit, but I can think of no better way at the moment. Maybe I can enhance the code by trying to call it as a function/procedure depending on context first, and if that fails, try the other way.

      If you're using a DBI handle, then I think your module definitely should have "DBI" in its name somewhere. I'd avoid Oracle:: entirely.

      Hmm. Since the interface itself is not Oracle-specific at all, and the implementation could be extended to work with other databases, I kind of like DBIx::Procedures, but that seems taken. Maybe DBIx::Call. Who do I talk to about the DBIx namespace?

Re: calling Oracle stored procedures
by Arunbear (Prior) on Nov 24, 2004 at 10:14 UTC
    A small comment on the interface: why not store the database handle in a package global e.g.
    use Oracle::Procedures qw(sysdate); $Oracle::Procedures::DBH = DBI->connect(.....); print sysdate();
    so you don't have to pass the dbh to each of your functions.
      $Oracle::Procedures::DBH = DBI->connect(.....);

      If you could set the package global only once at program startup and then be done with it (which you cannot because there might be more than one connection, and connections sometimes have to be re-opened) that would be sweet.

      But as you cannot, you have to set it every time you call one of Oracle::Procedures' functions, and then there is no gain (in code prettiness) compared to just passing it in every time.

      What if you are connecting to two oracle databases at once then? Globals suck :) Or package wide, static like variables.

      ----
      Then B.I. said, "Hov' remind yourself nobody built like you, you designed yourself"

Re: calling Oracle stored procedures
by thor (Priest) on Nov 24, 2004 at 12:35 UTC
    Can a procedure in Oracle return a result set? If so, it would seem that you're forced to deal with those results being returned in an array. Seems a bit heavy handed given that the DBI provides many faster ways to return results. Moreover, all rows will be squashed together...how do you know where one row stops and the next starts? Also, the calling of a procedure vs the calling of a function seems rather arbitrary. Is there some way to perhaps distinguish the two at run time? Perhaps inside of an eval? (i.e. try the name of the function as a stored procedure first...if it fails because it doesn't exist, then try it as a function) Other than that...cool idea. :)

    thor

    Feel the white light, the light within
    Be your own disciple, fan the sparks of will
    For all of us waiting, your kingdom will come

    Update I forgot to mention...in your __run_procedure sub, you do croak "blah" unless ref $dbh. Your check will fail to detect an error if I call it like-a-so: sysdate(\1). It's not enough to check that $dbh is a reference...it has to be the right kind of reference.
      Can a procedure in Oracle return a result set?

      Yes, it can return a cursor. My module does not handle this case (yet...). To use a cursor, you have to explicitly declare this type to DBI, and then you can fetch from it.

      So for now: yes, Oracle can, but Oracle::Procedures cannot.

      Your check will fail to detect an error if I call it like-a-so: sysdate(\1).

      True.