Showing posts with label PL/SQL. Show all posts
Showing posts with label PL/SQL. Show all posts

Wednesday, 20 January 2010

CakePHP: Calling Oracle stored procedures and functions

Our testing with Cake is almost complete. We have been able to read and write data correctly in our non UTF-8 Oracle database, managed to communicate with SAP, played enough with AJAX, so our users will not complain when dealing with tree like data and even managed to get authenticated by our MS-Windows 2003 active directory servers. So the last question left was how does one call an Oracle PL/SQL stored procedure or function from CakePHP ?

A little bit of googling and a little bit of digging into the CakePHP code revealed the following: The most common approach to calling stored procedures and functions is to create a member function in your model class and set up the call from there. In the simple case of calling a stored procedure with IN parameters only, the Model's query() method can be used to perform the actual call via the CALL PROC_NAME( ... ) SQL statement. The usual approach is to create a model method like this :

class MyModel extends AppModel {
    var $name = "MyModel";

    ...
    function callStoredProc( $param1, $param2)
    {
        $this->query("CALL my_stored_proc( $param1, $param2");
    }
    ...
}

If however you need to get data in and out of Oracle then you have to get your hands dirty and set up the call using low level oci_* functions. A simple example will clarify everything.

Let us suppose that you library users require that you display the average number of pages of the books stored in your library. A simple PL/SQL function to return this would probably be something like this: /p>

CREATE OR REPLACE FUNCTION average_book_pages RETURN NUMBER
IS
   page_avg NUMBER;   
BEGIN
   SELECT avg( num_pages) 
      INTO page_avg
      FROM books;
   RETURN page_avg;   
END AVERAGE_BOOK_PAGES;

The next thing to do would be to create a getAvergaeBookPages() function in our Book model class :

<?php
class Book extends AppModel {
    var $name = 'Book';
    var $belongsTo = ...
    var $validate = array( ...

    function getAverageBookPages()
    {
        // every model has a datasource
        $dataSource = $this->getDataSource();
        // and every datasource has a  database connection handle
        $connection = $dataSource->connection;
        if (!$connection)
            return -1;

        // from now you need this Oracle specific approach 
        $statement = oci_parse( $connection,
                "begin :ret := AVERAGE_BOOK_PAGES(); end;");
        oci_bind_by_name($statement, ':ret', $r, 100);
        oci_execute($statement);
        return $r;
    }
}
?>

The last parameter of oci_bind_by_name is the number of bytes to allocate for returning the string representation of the bind variable. Just make sure that you allocate enough memory for that. My test data yield an average of 762,666666666666666666666666666666666667 pages per book (Thank you Mr. Minasi) and so oci_execute kept giving me ORA-06502: PL/SQL: numeric or value error: character string buffer too small until I raised the value to 100.

So that does it. Now calling this from you controller code is as easy as :

<?php
class BooksController extends AppController {
    var $name = 'Books';

    ...

    function index()
    {
        $this->Book->recursive = 0;
        $this->set('books', $this->paginate());
        $this->set('averagePages', $this->Book->getAverageBookPages());
    }

    ...
}

Monday, 11 February 2008

Oracle 10g Killing open sesions

We have a client program that leaves lots of open sessions. These sessions take up server resources and we wanted to find a way to locate and kill them.

Thanks to Natasa Oikonomidou from Softworks who provided the code, I ended up with the following solution:

Create a the following procedure on the sys schema.

create or replace
PROCEDURE inactive_session_killer
IS
  now DATE;
BEGIN
  now := sysdate;
  FOR rec IN
    (SELECT sid,
       serial#,
       username,
       schemaname,
       osuser,
       terminal,
       machine,
       PROGRAM,
       status,
       type,
       saddr,
       logon_time,
       now - last_call_et / 86400 last_activity
     FROM v$session
     WHERE username = 'XXXXX'   -- user name of open session "leaver"
           AND type = 'USER')
  LOOP

    IF rec.last_activity <( now  - 8 / 24) THEN
      -- inactive for more than 8 hours
      -- you can keep a log table here: inserrt into ...
      EXECUTE IMMEDIATE 'alter system kill session ''' ||  
                                        rec.sid || ',' || 
                                        rec.serial#    || 
                                        ''' immediate';

    END IF;

  END LOOP;
END;

Using SQLPlus or SQL Developer execute the following

 
VARIABLE jobno number;
BEGIN
   DBMS_JOB.SUBMIT(:jobno, 'inactive_session_killer;', SYSDATE, 'SYSDATE + 1/24');    -- runs every hour
   commit;
END;

... and that does it.

Wednesday, 12 December 2007

Oracle 10g SQL Mass rebuild and move the indexes of a specified user

Here is a function that rebuilds or coalesces the indexes of a specified user.If asked to rebuild the indexes then you can supply a table space to move the rebuild indexes into.

The function takes three parameters :

  • user_name : The name of the user whose indexes are to be updated
  • rebuild_type : Ether C or R to indicate Coalesce or Rebuild operation
  • tablespace_name : Optional parameter to indicate where to move the indexes in case of rebuild

The function should be executed by a user with DBA privileges and this code is as follows

create or replace PROCEDURE rebuild_user_indexes( user_name IN VARCHAR2, 
                                rebuild_type IN VARCHAR2, 
                                tablespace_name IN VARCHAR2 DEFAULT NULL) 
AS
  full_index_name VARCHAR2(100);
  index_command VARCHAR2(10);
  sql_str VARCHAR2(100);
  
  total_indexes Integer;
  updated_indexes Integer;
  
  CURSOR get_index_cursor IS
    SELECT index_name, index_type 
    FROM dba_indexes
    WHERE owner = user_name AND
          index_type in ( 
              'BITMAP', 
              'NORMAL', 
              'FUNCTION-BASED NORMAL', 
              'FUNCTION-BASED DOMAIN');
      
BEGIN
  /* check initial parameters */
  IF rebuild_type <> 'R' AND rebuild_type <> 'C' THEN
    raise_application_error( -20999, 'rebuild type should be "C" or "R"');
  ELSIF rebuild_type = 'R' THEN
    index_command := ' Rebuild';
    /* if a rebuild tablespace is also defined */
    IF tablespace_name <> '' THEN
      index_command := index_command || ' tablespace ' || tablespace_name;
    END IF;
  ELSE
    index_command := ' Coalesce';
  END IF;
  
  total_indexes := 0;
  updated_indexes := 0;
  /* get all indexes that belong to the specified user */
  FOR r IN get_index_cursor LOOP
    total_indexes := total_indexes + 1; 
    dbms_output.put_line( total_indexes || ' ' || 
                          r.index_name || ' ' || 
                          r.index_type);

    /* Coalescing works only on normal B-Tree indexes, 
     * while rebuilding works with everything.
     */    
    IF (rebuild_type = 'R') OR 
       (rebuild_type = 'C' AND instr( r.index_type, 'NORMAL') > 0) THEN
      
      full_index_name := user_name || '.' || r.index_name;
      sql_str := 'alter index ' || full_index_name || index_command;
      
      BEGIN
        /* attempt to modify the index */
        EXECUTE IMMEDIATE sql_str;
        updated_indexes := updated_indexes + 1;
      EXCEPTION
        WHEN OTHERS THEN
          /* display the command and the error that occured during index 
           * update 
           */
          dbms_output.put_line( sql_str);
          dbms_output.put_line( sqlcode || ' ' || sqlerrm);
          /* continue working */
          NULL;
      END;
    END IF;
  END LOOP;
    
  /* report results */
  dbms_output.put_line( 'Indexes examined :' || total_indexes);
  dbms_output.put_line( 'Indexes Updated  :' || updated_indexes);
END rebuild_user_indexes; 

Thanks to SQLDeveloper running the function can is as easy as copying and pasting the following in SQLPlus.

DECLARE
  USER_NAME VARCHAR2(200);
  REBUILD_TYPE VARCHAR2(200);
  TABLESPACE_NAME VARCHAR2(200);
BEGIN
  USER_NAME := NULL;       -- add your user here, for example 'HR'
  REBUILD_TYPE := NULL;    -- either 'C' or 'R'
  TABLESPACE_NAME := NULL; -- optional new tablespace name if desired

  REBUILD_USER_INDEXES(
    USER_NAME => USER_NAME,
    REBUILD_TYPE => REBUILD_TYPE,
    TABLESPACE_NAME => TABLESPACE_NAME
  );
END;

Note

The above code provides a very simplistic approach to index rebuilding as it rebuilds all indexes of a user. In order to determine the indexes in your database that would really benefit from rebuilding you need to run the script available from Oracle Metalink Note:122008.1.

Tuesday, 24 July 2007

PL/SQL: User Privileges are not inherited from roles

FACT: When executing DDL from withing PL/SQL using NDS, then the user running the procedure must have the appropriate privilege assigned directly to create a whatever object the DDL is trying to create.

Privileges are not inherited from a role if a procedure is used.

Tuesday, 17 July 2007

PL/SQL: Collection Types

All programming languages need arrays. Oracle PL/SQL provides three types. The following post aims to serve as a quick reference and provide some minor examples for each type.

VARRAY's
VARRAY's are the closest to the Pascal or C motion of an array. They are always defined to be of fixed length and start at index 1. VARRAY's can be used on both SQL or PL/SQL and whenever they are stored into the databse, they preserve the order of their elements.

Declaring a VARRAY can be performed as follows

    declare
      TYPE week_type IS VARRAY(7) of VARCHAR2(30);
      week_day week_type := week_type();
  
Nested Tables

Nested tables are much like VARRAY's in the sense that they are single dimensional arrays that can be used in both SQL and PL/SQL, but they do not posses the maximum length limitation. Unlike VARRAY's, nested tables are multisets which means that there is no inherent order to their elements

Both VARRAY's and nested tables must be initialized with the default collection-type constructor, meaning that all declarations of the form my_collection CollectionType must end like := collectionType().

Both VARRAY's and nested tables must be extended to accumulate space for their data. No matter wheither you define the maximum size of an array or not, array slots start with a null value. So you cannot just start assigning values to slots unless you create the appropriate space first. See the examples later on.

Nested tables like their "Associative Array" cousins that we shall discuss shortly are what Steven Feuersteinin refers to as sparse data structures. This means that elements can be removed from nested tables leaving an empty slot in the middle of the array. For a demonstration of the concept, Sse the example on nested tables later on.

Declaring a nested table can be performed as follows

    DECLARE
      TYPE string30_table IS TABLE OF Varchar2(30);
      week_day week_type := tring30_table();
  
Associative Arrays
Associative Arrays are single dimensional arrays with a unique index only available in PL/SQL code. Their index can be of any type may that be Integer or Varchar2. Associative Arrays do not need to be initialized using any default constructor. You just declare them and user them.

Declaring an associative array can be performed as follows

    DECLARE
      TYPE string30_array IS TABLE OF VARCHAR2(30)
            INDEX BY BINARY_INTEGER;
      week_day string30_array;
  

Steven Feuersteinin his excelant Oracle PL/SQL Programming 4th Edition book presents the following example regarding usage of associative arrays.

DECLARE
  TYPE list_of_names_t IS TABLE OF VARCHAR2(50)
           INDEX BY PLS_INTEGER;
  happy_family list_of_names_t;
  l_row PLS_INTEGER;
BEGIN
  happy_family(202020202) := 'Eli';
  happy_family(-15070) := 'Steven';
  happy_family(-90900) := 'Chris';
  happy_family(88) := 'Neva';

  l_row := happy_family.FIRST;
  WHILE l_row IS NOT NULL
  LOOP
    DBMS_OUTPUT.put_line( happy_family(l_row));
    l_row := happy_family.NEXT(l_row);  
  END LOOP;
END;

When dealing with VARRAY's and nested tables then the type definition may be stored at schema level. Again a small example this time from Oracle PL/SQL for Dummies . Notice the different naming styles between the two.

DECLARE 
  TYPE month_nt IS TABLE OF VARCHAR2(50);
  v_month_nt month_nt := month_nt();
  i NUMBER;
BEGIN
  v_month_nt.extend(3);
  v_month_nt(1) := 'January';
  v_month_nt(2) := 'February';
  v_month_nt(3) := 'March';

  v_month_nt.DELETE(2);
  DBMS_OUTPUT.PUT_LINE('Count ' || v_month_nt.COUNT);
  DBMS_OUTPUT.PUT_LINE('Last ' || v_month_nt.LAST);

  i := v_month_nt.FIRST;
  LOOP
    DBMS_OUTPUT.PUT_LINE(v_month_nt(i));
    i := v_month_nt.NEXT(i);

    IF i IS NULL THEN
      EXIT;
    END IF;

  END LOOP;
END;

The output of the previous script is ...

Count 2
Last 3
January
March

... meaning that we have an array with three slots and a gap between them. I wanted to take this example a little bit further so I changed the definition of the array type to VARRAY., making the program look like this. (Notice the standard FOR loop used for traversing a VARRAY.

DECLARE 
  TYPE month_va IS VARRAY(20) OF VARCHAR2(50);
  v_month_va month_va := month_va();
  i PLS_INTEGER;
BEGIN
  v_month_va.extend(3);
  v_month_va(1) := 'January';
  v_month_va(2) := 'February';
  v_month_va(3) := 'March';

  DBMS_OUTPUT.PUT_LINE('Count ' || v_month_va.COUNT);
  DBMS_OUTPUT.PUT_LINE('Last ' || v_month_va.LAST);

  FOR i IN v_month_va.FIRST .. v_month_va.LAST
  LOOP
    DBMS_OUTPUT.PUT_LINE(v_month_va(i));
  END LOOP;
END;