Showing posts with label ORACLE. Show all posts
Showing posts with label ORACLE. Show all posts

Saturday, August 15, 2015

Oracle Developer Choice Awards - 1 Week Left to Nominate

Oracle has launched its inaugural Developer Choice Awards. The deadline to nominate someone has been extended to August 21st so take advantage of this extension and nominate someone you think deserves recognition for the work they do with Oracle!

A description of the competition is below and you can find out more by going to the main site.

The Oracle Database Developer Choice Awards celebrate and recognize technical expertise and contributions in the Oracle Database community. As longtime and new users of Oracle Database move to the Cloud and take advantage of this exciting new architecture, community experts will play a critical role in helping them succeed.

For 2015, awards will be given out in the following technology areas:

  • SQL
  • PL/SQL
  • Oracle REST Data Services
  • Oracle Application Express
  • Database Design

We encourage you to nominate a developer you believe is deserving of this award. We are accepting nominations through August 21. A panel of judges, composed of Oracle ACEs and Oracle employees, will then choose a set of finalists.

But who chooses the winners? You do! The worldwide Oracle technologist community votes on the finalists from September 15 through October 15.

The winners of the Oracle Database Developer Choice Awards will be announced at the YesSQL! Celebration during Oracle OpenWorld 2015.

This is your chance to shine and earn highly deserved recognition for yourself or your respected peers.

Wednesday, March 4, 2015

Logger is Moving

I'm pleased to announce that Logger has found a new home and will be maintained by OraOpenSource. A very big thank you goes out to Tyler Muth for starting this project many years ago and handing over the reins to OraOpenSource.

Here's a brief FAQ regarding the move:

What is Logger?

Logger is a (free) open source logging tool for Oracle PL/SQL. It is used in many instances ranging from small applications to very large applications in major corporations.

What will happen with links to the old project page?

They'll still work. The project was forked and a notice was put up on the old Github site letting people know its new home.

Will Logger still remain open source / free? 

Yes. We'll update the license slightly so that it will be easier to understand and use within your code. This will not affect any existing implementations.

When is the next release?

I did a lot of work last year, then was sidelined with my concussion. It turns out that picking up a project after not touching it for 8 months takes some time.

Since I'm working on this project as part of my rehab I can not give a definite time frame. As soon as the beta version is ready I'll make it available.

What is in the next release?

Lots of great stuff! Going forward you can monitor what will be in each subsequent release by tracking the upcoming milestones on the project's issue page.

Will you still post info on Logger on this site?

Over the years I have written a lot about Logger and posted it on this blog. To maintain a consistent branding and marketing effort all future posts regarding Logger will be on OraOpenSource's blog.

What can I do to help?

Right now the best thing you can do is sign up for the email list (link on oraopensource.com/logger). We'll be announcing the beta version and next major release when it's available. You can also follow OraOpenSource on twitter: @oraopensource.

Once the next release is available we'll be looking for help from the community to continue to develop and maintain Logger.

Saturday, April 12, 2014

NTILE vs WIDTH_BUCKET

Rick Cale recently asked a question on an article I wrote two years ago about Some Interesting Oracle Analytic Functions. His question (see comments on Apr 11, 2014) were about the results from the NTILE function and how the same value could be in two different buckets. It was an excellent question and one that got me digging a bit more into the functionality of NTILE.

They're two ways to handle "bucketing data" in Oracle. In the documentation Oracle describes the two types as either having equiwidth or or equiheight histograms.

Equiwidth (NTILE): Each bucket will have the same number of items in it with some buckets having at most 1 more item than other buckets. An easy way to think of this concept is to order all the items, then divide the data evenly into groups based on the number of buckets. For example, supposed you have 9 values (AAAABCDEF) and wanted to put them into 3 buckets. the buckets would be B1 = AAA, B2 = ABC, B3 = DEF. You'll notice that the value "A" is in both buckets B1 and B2.

When an uneven amount of objects need to go into the buckets, NTILE will fill the first bucket first, second bucket second, etc. For example, suppose you had 10 values (AAAABCDEFG) and wanted to fill them into 3 buckets. (Note this is similar to the previous example with an additional "G"). The buckets would be B1 = AAAA, B2 = BCD, B3 = EFG.

Equiheight (WIDTH_BUCKET): This will take the min and max range, divided by the number of buckets and place each value in it. For example, the salaries in the EMP table range from 801 ~ 5000. If you set the min/max range from 0~5000 3 buckets will be created. All values from 0~1,666 will go into B1, values from 1,6667~3333 into B2, and values 3334 to 5000 into B3. (Note: for simplicity I took out decimals in this split). 


An easy way to think of these two bucketing methods is that NTILE divides values based on the number of items. WIDTH_BUCKET divides values based on their values.

Here's an example that highlights the difference between the two functions. 
with data as (
  -- using this as data input
  select 3 as num_buckets
  from dual
)
select 
  d.dname, 
  e.ename, 
  e.sal,
  ntile (data.num_buckets) over (order by sal asc) ntile,
  width_bucket(sal, 0, max(sal+1) over (), data.num_buckets) width_bucket
from emp e, dept d, data
where e.deptno = d.deptno
order by sal;


DNAME          ENAME      SAL        NTILE      WIDTH_BUCKET
-------------- ---------- ---------- ---------- ------------
RESEARCH       SMITH         801          1        1
SALES          JAMES         950          1        1
RESEARCH       ADAMS        1100          1        1
SALES          WARD         1250          1        1
SALES          MARTIN       1250          1        1
ACCOUNTING     MILLER       1300          2        1
SALES          TURNER       1500          2        1
SALES          ALLEN        1600          2        1
ACCOUNTING     CLARK        2450          2        2
RESEARCH       JONES        2975          2        2
RESEARCH       SCOTT        3000          3        2
RESEARCH       FORD         3000          3        2
SALES          BLAKE        3850          3        3
ACCOUNTING     KING         5000          3        3
It's important to note that WIDTH_BUCKET is not an analytic function but NTILE is. For more information read the documentation for each function. For WIDTH_BUCKET, the documentation covers what happens with values outside the min/max range (they go into bucket 0 and num_buckets+1).

Thursday, August 23, 2012

SQL%ROWCOUNT and Logger

Logger (by Tyler Muth) is a free open source logging tool for PL/SQL. Tyler initially launched it in 2009 and now it has become a staple tool for many Oracle development teams. If you've never heard of it go check it out.

When using logger I like to log the number of rows an update statement made after the update was performed. In order to do that I use the SQL%ROWCOUNT variable. The thing to be aware of is if you log SQL%ROWCOUNT using logger it will be "reset" by the implicit insert that logger does.  In the example below you'll notice that after calling logger the SQL%ROWCOUNT now has a value of 0 since it does an insert:
SQL> BEGIN
  2    UPDATE emp
  3    set sal = sal;
  4
  5    dbms_output.put_line('Rows updated: ' || SQL%rowcount);
  6    dbms_output.put_line('Rows updated: ' || SQL%rowcount);
  7    logger.log('Rows updated: ' || SQL%rowcount);
  8    dbms_output.put_line('Rows updated: ' || SQL%rowcount);
  9  END;
 10  /
Rows updated: 14
Rows updated: 14
Rows updated: 0

PL/SQL procedure successfully completed.

SQL> SELECT text
  2  FROM logger_logs
  3  WHERE ROWNUM = 1
  4  ORDER BY ID DESC;

TEXT
------------------
Rows updated: 14
This is important to know because sometimes you may do some additional work after an update statement depending on how many records were updated in the previous statement. You should change your code from:
UPDATE emp
SET sal = sal;
logger.log('Rows updated: ' || SQL%rowcount);

IF SQL%rowcount > 0 THEN
  ...
END IF;
To:
UPDATE emp
SET sal = sal;
l_row_count := SQL%rowcount;
logger.LOG('Rows updated: ' || l_row_count);

IF l_row_count > 0 THEN
  ...
END IF;

Thursday, June 7, 2012

SELECT INTO Techinques

One thing that bothers a lot of PL/SQL developers is when they have to select some data from a table into local variables. The problem is that the data may or may not exist (i.e. return 0..1 rows). They're several ways to write this code, some taking longer than others which is the pain point for developers.

I've listed out various ways to do this below and hopefully it'll help you find some easier ways to solve this issue.

The first example is the classic case of handling the 0..1 rows issue in PL/SQL. They're a few issues (not problems) that I have with this. The first is that it adds some additional lines of code (highlighted) which can make the code harder to follow. The other issue is that instead of using "WHEN no_data_found" some developers tend to use "WHEN OTHERS" which is guaranteed to give you some false positives in the long run.
DECLARE
  l_sal emp.sal%TYPE;
  p_empno emp.empno%TYPE := 123; -- Invalid empno number
BEGIN

  BEGIN
    SELECT sal
    INTO l_sal
    FROM emp
    WHERE empno = p_empno;
  EXCEPTION
    WHEN no_data_found THEN
      l_sal := 0;
  END;
  
END;
/
Another way to get the value from the table is to use the aggregate function MAX. Since you're expecting 0..1 rows this will always return 1 row and eliminates the need to have the "BEGIN ..." block of code which saves 5 lines of code. You can handle what to do when no value is found by using the NVL function.
DECLARE
  l_sal emp.sal%TYPE;
  p_empno emp.empno%TYPE := 123; -- Invalid empno number
BEGIN

  SELECT NVL(MAX(sal), 0)
  INTO l_sal
  FROM emp
  WHERE empno = p_empno;
  
END;
/
The only issue with the above example is when you can have 0..n rows but should only get 0..1. Ideally you should have table constraints in order to guarantee 0..1 rows but it's not always the case in some system. The following example shows how you can use the same technique as above but it will trigger an exception if n rows are found (which is what you want to do):
DECLARE
  l_sal emp.sal%TYPE;
  p_empno emp.empno%TYPE := 123; -- Invalid empno number
BEGIN

  SELECT NVL(MAX(sal), 0)
  INTO l_sal
  FROM emp
  WHERE empno = p_empno
  HAVING count(1) <= 1;
  
END;
/

Tuesday, May 22, 2012

Some Interesting Oracle Analytic Functions

I had to write some reports for a personal application of mine last night and needed to expand beyond my usual set of analytic functions that I normally use. I thought it would be a good idea to do a quick blog post on these analytic functions.

If you've never heard of or used Oracle analytic functions please read this article first. It's by far the best article I've read at explaining what analytic functions are and how to use them. The Oracle analytic function documentation can be found here.

RATIO_TO_REPORT

RATIO_TO_REPORT compares the current value against the sum of the other set of values. The following example shows the percentage of each employees salary when compared to the sum of their department's salary. You'll notice that you can calculate the same value using a different method (sal_dept_ratio2) which may be why I haven't seen RATIO_TO_REPORT used that often.
SELECT d.dname, e.ename, e.sal, 
  round(ratio_to_report(sal) OVER (PARTITION BY e.deptno),2) sal_dept_ratio, 
  round(sal / sum(e.sal) OVER (PARTITION BY e.deptno),2) sal_dept_ratio2
FROM emp e, dept d
WHERE e.deptno = d.deptno;

-- Result

DNAME          ENAME             SAL SAL_DEPT_RATIO SAL_DEPT_RATIO2
-------------- ---------- ---------- -------------- ---------------
ACCOUNTING     CLARK            2450            .28             .28
ACCOUNTING     MILLER           1300            .15             .15
ACCOUNTING     KING             5000            .57             .57
RESEARCH       FORD             3000            .28             .28
RESEARCH       SCOTT            3000            .28             .28
RESEARCH       JONES            2975            .27             .27
RESEARCH       SMITH             800            .07             .07
RESEARCH       ADAMS            1100             .1              .1
SALES          WARD             1250            .13             .13
SALES          MARTIN           1250            .13             .13
SALES          TURNER           1500            .16             .16
SALES          JAMES             950             .1              .1
SALES          ALLEN            1600            .17             .17
SALES          BLAKE            2850             .3              .3

NTH_VALUE

NTH_VALUE returns the nth row in the window clause. It's extremely important that you explicitly define the window or your values may not make sense. I'll write another post explaining this later. Update: NTH_VALUE Windowing Clause

The following example shows the 2nd highest salary in each department.
SELECT d.dname, e.ename, e.sal, 
   nth_value(e.sal, 2) OVER (
    PARTITION BY e.deptno ORDER BY e.sal DESC 
    -- Don't forget to define the window
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) sec_high_sal_dept
FROM emp e, dept d
WHERE e.deptno = d.deptno;

-- Result

DNAME          ENAME             SAL SEC_HIGH_SAL_DEPT
-------------- ---------- ---------- -----------------
ACCOUNTING     KING             5000              2450
ACCOUNTING     CLARK            2450              2450
ACCOUNTING     MILLER           1300              2450
RESEARCH       FORD             3000              3000
RESEARCH       SCOTT            3000              3000
RESEARCH       JONES            2975              3000
RESEARCH       ADAMS            1100              3000
RESEARCH       SMITH             800              3000
SALES          BLAKE            2850              1600
SALES          ALLEN            1600              1600
SALES          TURNER           1500              1600
SALES          WARD             1250              1600
SALES          MARTIN           1250              1600
SALES          JAMES             950              1600

LISTAGG

LISTAGG was a highly requested feature that was implemented in 11gR2. Overall LISTAGG allows you to group values in multiple rows and put them in a comma delimited column on one row.

The following example (which is not an analytic function) shows all the employees for each department:
SELECT d.dname,
  listagg(e.ename, ',') WITHIN GROUP (ORDER BY e.ename) dept_emp_list
FROM emp e, dept d
WHERE e.deptno = d.deptno
GROUP BY d.dname;

-- Result

DNAME          DEPT_EMP_LIST
-------------- ----------------------------------------

ACCOUNTING     CLARK,KING,MILLER
RESEARCH       ADAMS,FORD,JONES,SCOTT,SMITH
SALES          ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
Using the LISTAGG as an analytic function you can see each employee's colleagues in their department, including themselves.
SELECT d.dname, e.ename,
  listagg(e.ename, ',') WITHIN GROUP (ORDER BY e.ename) 
    OVER (PARTITION BY e.deptno) dept_colleagues
FROM emp e, dept d
WHERE e.deptno = d.deptno;

-- Result

DNAME          ENAME      DEPT_COLLEAGUES
-------------- ---------- --------------------------------------

ACCOUNTING     CLARK      CLARK,KING,MILLER
ACCOUNTING     KING       CLARK,KING,MILLER
ACCOUNTING     MILLER     CLARK,KING,MILLER
RESEARCH       ADAMS      ADAMS,FORD,JONES,SCOTT,SMITH
RESEARCH       FORD       ADAMS,FORD,JONES,SCOTT,SMITH
RESEARCH       JONES      ADAMS,FORD,JONES,SCOTT,SMITH
RESEARCH       SCOTT      ADAMS,FORD,JONES,SCOTT,SMITH
RESEARCH       SMITH      ADAMS,FORD,JONES,SCOTT,SMITH
SALES          ALLEN      ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
SALES          BLAKE      ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
SALES          JAMES      ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
SALES          MARTIN     ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
SALES          TURNER     ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
SALES          WARD       ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD

NTILE

NTILE allows you to divide your values into buckets and see which bucket the current row belongs to. For educational institutions this is a very good function to let students know that they're in the top x% of the class.

Update (12-Apr-2014): NTILE vs WIDTH_BOX describes the different ways to bucket data in Oracle. It also explains why SAL 1250 exists in both buckets 2 and 3.

The following example shows the 3 tiers (or buckets) of salaries across the entire company. It allows you to easily see who's in the top 33% of salaries.
SELECT d.dname, e.ename, e.sal,
  ntile (3) over (order by sal desc) three_tier_sal
FROM emp e, dept d
WHERE e.deptno = d.deptno;

-- Result

DNAME          ENAME             SAL THREE_TIER_SAL
-------------- ---------- ---------- --------------
ACCOUNTING     KING             5000              1
RESEARCH       SCOTT            3000              1
RESEARCH       FORD             3000              1
RESEARCH       JONES            2975              1
SALES          BLAKE            2850              1
ACCOUNTING     CLARK            2450              2
SALES          ALLEN            1600              2
SALES          TURNER           1500              2
ACCOUNTING     MILLER           1300              2
SALES          WARD             1250              2
SALES          MARTIN           1250              3
RESEARCH       ADAMS            1100              3
SALES          JAMES             950              3
RESEARCH       SMITH             800              3

Monday, July 11, 2011

APEX Region Errors - Part 3

Disclaimer: This is an advanced post that discusses and modifies some of the inner workings of APEX.

In APEX Region Errors - Part 2 I discussed how to add triggers on the APEX Activity Log tables to store information in custom error tables when a user encounters an APEX region error.

Instead of storing the information in custom error tables you can leverage the APEX Feedback tool and trigger an automatic feedback entry. This may be a preferred option as you don't need to create custom tables and the feedback tool provides a lot of information.

Before continuing it's important that you know how to use the new APEX Feedback tool. If you don't know about the APEX Feedback tool I suggest that you read about how to create the feedback link (http://dgielis.blogspot.com/2010/03/apex-40-feedback-link.html) and how to access the feedback tool (http://dgielis.blogspot.com/2010/03/apex-40-looking-at-feedback-through.html). Try implementing it in a dummy application to see what you can do with it.

The following triggers will enter a feedback entry each time a region error occurs:

Disclaimer (again): Modifying anything in the APEX schema will put your APEX instance in an unsupported state. Please proceed with caution. I take no responsibility for any negative outcomes from this.

-- NOTE: Do this as the APEX_040000 user or a privlidged user such as SYSTEM
CREATE OR REPLACE TRIGGER apex_040000.trg_apex_activity_log1_air
AFTER INSERT
ON apex_040000.wwv_flow_activity_log1$
FOR EACH ROW
WHEN (new.SQLERRM IS NOT NULL)
DECLARE
BEGIN
-- Log as feedback
apex_util.submit_feedback (
p_comment => 'AUTO MSG: Region Error', -- Put a comment here that can be used to easily identify auto generated feedback messages
p_type => 3, -- Bug. See API documentation for different values
p_application_id => :new.flow_id,
p_page_id => :new.step_id,
p_email => null,
p_label_01 => 'Error Message', -- You can add up to 8 label/attributes. See API documentation for more information
p_attribute_01 => :new.sqlerrm,
p_label_02 => 'Component Type',
p_attribute_02 => :new.sqlerrm_component_type,
p_label_03 => 'Component Name',
p_attribute_03 => :new.sqlerrm_component_name
);

-- Could use APEX_MAIL to send a notification to a list of developers to take a look at problem
-- This is entirely optional. Modify as required
apex_mail.send(
p_to => 'someone@yourorg.com',
p_from => 'someone@yourorg.com',
p_body => 'Region Error Occured. Please Check Feedback',
p_body_html => '',
p_subj => 'APEX Region Error');

END;
/
And... (differences are highlighted)

-- NOTE: Do this as the APEX_040000 user or a privlidged user such as SYSTEM
CREATE OR REPLACE TRIGGER apex_040000.trg_apex_activity_log2_air
AFTER INSERT
ON apex_040000.wwv_flow_activity_log2$
FOR EACH ROW
WHEN (new.SQLERRM IS NOT NULL)
DECLARE
BEGIN
-- Log as feedback
apex_util.submit_feedback (
p_comment => 'AUTO MSG: Region Error', -- Put a comment here that can be used to easily identify auto generated feedback messages
p_type => 3, -- Bug. See API documentation for different values
p_application_id => :new.flow_id,
p_page_id => :new.step_id,
p_email => null,
p_label_01 => 'Error Message', -- You can add up to 8 label/attributes. See API documentation for more information
p_attribute_01 => :new.sqlerrm,
p_label_02 => 'Component Type',
p_attribute_02 => :new.sqlerrm_component_type,
p_label_03 => 'Component Name',
p_attribute_03 => :new.sqlerrm_component_name
);

-- Could use APEX_MAIL to send a notification to a list of developers to take a look at problem
-- This is entirely optional. Modify as required
apex_mail.send(
p_to => 'someone@yourorg.com',
p_from => 'someone@yourorg.com',
p_body => 'Region Error Occured. Please Check Feedback',
p_body_html => '',
p_subj => 'APEX Region Error');

END;
/
When a region error occurs you can now view the information in Team Development > Feedback.

If you click the Edit button you can get a lot of detailed information about was happening when the user encountered the error including all of the values in session state.

Monday, March 21, 2011

VirtualBox - High CPU Problem

I've been using Oracle's VirtualBox for several months now. I'm really impressed with this product and it has helped me a lot to rapidly prototype some APEX concepts.

One thing that bothered me when I started using it was that no matter what my virtual machine was doing, VirtualBox was using a lot of the CPU on my laptop. This didn't matter if the virtual machine was idle or doing any processing. After some digging around I found this very helpful post on the Oracle forums: http://forums.oracle.com/forums/thread.jspa?messageID=9305831. As weird as it may sound the suggestion is to create and start a second, dummy, virtual machine. I was pleasantly surprised to see that it worked. The configuration that I used was: DOS as an OS. 4 MB of memory and a 4 MB hard drive. Disable all the extra stuff (network, audio, usb, etc).

Here are the screen shots of my CPU before and after I started the second, dummy, virtual machine:

Before:

After:

After starting the second virtual machine the CPU spiked for a few seconds then dropped significantly. Hopefully this issue will be fixed in a future version of VirtualBox but until then this is a very simple work around.

Monday, October 25, 2010

APEX Hash Tag on Twitter: #orclapex

The old Oracle APEX hash tag on twitter was #apex. If you search for #apex on Twitter you get a lot of non Oracle APEX related topics as apex is a common name.

Today I proposed a new hash tag for Oracle APEX related content on Twitter. Thanks to Scott Spendolini for coming up with the final name and Dan McGhan for providing feedback.

When posting on Oracle APEX related content on Twitter please us #orclapex

Wednesday, October 20, 2010

Oracle XE and APEX: Where is my Trace File?

In APEX you trace your session by adding &p_trace=YES at the end of your URL. For example: http://fcapex40:8080/apex/f?p=101:1:2603384364125271::NO&p_trace=YES Please refer to the APEX documentation for more information regarding traces in APEX.

The documentation states that the trace file is located in the following directory:

SQL> -- Run as SYSTEM
SQL> SHOW PARAMETERS user_dump_dest

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
user_dump_dest string /usr/lib/oracle/xe/app/oracle/
admin/XE/udump

This isn't the case if you're using the Embedded PL/SQL Gateway, which is how APEX is run on Oracle XE. Thanks to Jari for answering my question on the APEX forum about this: http://forums.oracle.com/forums/thread.jspa?forumID=137&threadID=1771547

For APEX installations that use the PL/SQL Embedded Gateway (i.e. Oracle XE) the trace file is located in the following directory:

SQL> SHOW PARAMETER background_dump_dest

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
background_dump_dest string /usr/lib/oracle/xe/app/oracle/
admin/XE/bdump

To find the trace file that was created for your session:

$ cd /usr/lib/oracle/xe/app/oracle/admin/XE/bdump
$ grep -l "APP_SESSION" *.trc
#example
$ grep -l "2603384364125271" *.trc

Monday, July 26, 2010

Poor Man's VPD in APEX and Oracle XE

At this year's ODTUG Kaleidoscope I gave a presentation called "Enhancing APEX Security". A copy of the presentation can be downloaded here.

As part of the presentation I discussed how to create a "Poor Man's" VPD using Oracle XE. The main concept was to simulate basic VPD on a non Enterprise Edition (EE) (VPD is only available on EE). This post will cover how to do this. Please note, since this is for demonstration purposes I have kept things very simple and it is by no means a complete solution.

Before you can review the code, we need to discuss some of the basic architecture and technology that will be used. I strongly encourage you to do some additional research on these topics if you plan to use this method in production.

Schema Setup


Assuming you don't have Oracle EE, you'll need a way to secure your existing schema. Lets say you had a schema called "DEMO". You'll need to create a new schema called "DEMO_PUB". The DEMO_PUB schema will not contain any objects. Instead, it will have synonyms which point to views and packages in the DEMO schema. Note, the DEMO_PUB schema will not have any access to the DEMO tables. All DML statements will be made via packages and procedures. The views from the DEMO schema will be "secure views" which will restrict access to the data. On the flip-side the DEMO schema will only grant SELECT and EXECUTE to views and packages respectively, to the DEMO_PUB schema.

You APEX applications should use the DEMO_PUB so that security logic is stored in the database rather than the front-end. This should help prevent developers from displaying data that end users don't have access to.

The following diagram, taken from my presentation, highlights the overall schema structure.


Contexts

For people unfamiliar with Contexts, the easiest way to describe them is a globally accessible container of name/value pairs. The container is only accessible if you have the correct key. Oracle has some great reference material on this (search for VPD or FGAC) so I won't cover this any further Here's a diagram to illustrate Contexts.


The key to "Poor Man's" VPD is to leverage context values in your views to restrict the data. To demonstrate this in pseudo code, if you wanted to restrict access on the EMP table to only employees in your department you'd write a view like this:

CREATE OR REPLACE VIEW vemp AS
SELECT * FROM emp
WHERE deptno = some_value_from_a_context


The Code

Hopefully you understood the small bit of background information I wrote before. Here's how to implement a very simple "VPD" enabled application

-- Create Context
-- ctx_vpd is our context name
-- pkg_vpd is the only place where we can modify values in this context
-- This provides a lot of security since single access point
CREATE OR REPLACE CONTEXT ctx_vpd USING pkg_vpd ACCESSED GLOBALLY;

Create pkg_vpd spec

-- VPD Demo Package from www.talkapex.com
-- @author Martin Giffy D'Souza - martin@talkapex.com
CREATE OR REPLACE PACKAGE pkg_vpd
AS
--
-- Login function for APEX to login users
-- Spec must be as is, as required by APEX for custom authentication schemes
-- For demo purposes we'll use ENAME = ENAME from EMP
-- @param p_username username
-- @param p_password password
-- @return TRUE or FALSE
--
FUNCTION f_login (p_username IN VARCHAR2, p_password IN VARCHAR2)
RETURN BOOLEAN;

--
-- Set context identifier
-- This will register our "key" which will be required each time we want to access a Context (name/value) pair
-- @param p_session_key in APEX use :APP_USER || ':' || :APP_SESSION
--
PROCEDURE sp_set_context_identifier (p_session_key IN VARCHAR2);

--
-- Set context value
-- i.e. Sets a value for the name/value pair
--
PROCEDURE sp_set_context_value (p_name IN VARCHAR2, p_value IN VARCHAR2);

--
-- Get Context Value
-- i.e. Get value for name/value pair
--
FUNCTION f_get_context_value (p_name IN VARCHAR2)
RETURN VARCHAR2;

--
-- To be run in Post Authentication Process in APEX
-- Sets some of the name/value pairs required for VPD
--
PROCEDURE sp_apex_post_auth;

END pkg_vpd;
/

Create pkg_vpd body

-- VPD Demo Package from www.talkapex.com
-- @author Martin Giffy D'Souza - martin@talkapex.com

CREATE OR REPLACE PACKAGE BODY pkg_vpd
AS
--
-- Login function for APEX to login users
-- Spec must be as is, as required by APEX for custom authentication schemes
-- For demo purposes we'll use ENAME = ENAME from EMP
-- @param p_username username
-- @param p_password password
-- @return TRUE or FALSE
--
FUNCTION f_login (p_username IN VARCHAR2, p_password IN VARCHAR2)
RETURN BOOLEAN
AS
v_count PLS_INTEGER;
BEGIN
SELECT COUNT (e.ename)
INTO v_count
FROM emp e -- Notice how I'm not reference the view (vemp) it would return no rows at this point
WHERE LOWER (e.ename) = LOWER (p_username)
AND LOWER (p_username) = LOWER (p_password);

IF v_count = 1 THEN
RETURN TRUE;
END IF;

RETURN FALSE;
END f_login;

--
-- Set context identifier
-- This will register our "key" which will be required each time we want to access a Context (name/value) pair
-- @param p_session_key in APEX use :APP_USER || ':' || :APP_SESSION
--
PROCEDURE sp_set_context_identifier (p_session_key IN VARCHAR2)
AS
BEGIN
dbms_session.set_identifier (client_id => p_session_key);
END sp_set_context_identifier;

--
-- Set context value
-- i.e. Sets a value for the name/value pair
--
PROCEDURE sp_set_context_value (p_name IN VARCHAR2, p_value IN VARCHAR2)
AS
BEGIN
dbms_session.set_context ('CTX_VPD',
p_name,
p_value,
USER,
SYS_CONTEXT ('USERENV', 'CLIENT_IDENTIFIER'));
END;

--
-- Get Context Value
-- i.e. Get value for name/value pair
--
FUNCTION f_get_context_value (p_name IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
RETURN SYS_CONTEXT ('CTX_VPD', p_name);
END f_get_context_value;

--
-- To be run in Post Authentication Process in APEX
-- Sets some of the name/value pairs required for VPD
--
PROCEDURE sp_apex_post_auth
AS
v_empno emp.empno%TYPE;
v_deptno emp.deptno%TYPE;
BEGIN
-- Get User Information
SELECT empno, deptno
INTO v_empno, v_deptno
FROM emp
WHERE LOWER (ename) = LOWER (v ('app_user'));

-- Set Context Identifier
pkg_vpd.sp_set_context_identifier (p_session_key => v ('app_user') || ':' || v ('app_session'));
-- Set Name/Value pairs
pkg_vpd.sp_set_context_value (p_name => 'EMPNO', p_value => v_empno);
pkg_vpd.sp_set_context_value (p_name => 'DEPTNO', p_value => v_deptno);
-- This demo won't highlight the Last Access date, but can be very useful to kill sessions in the back end that have not been access for a given period of time.
pkg_vpd.sp_set_context_value (p_name => 'LAST_ACCESS', p_value => TO_CHAR (SYSDATE, 'DD-MON-YYYY HH24:MI:SS'));
END sp_apex_post_auth;
END pkg_vpd;
/

Create view which leverages the context

CREATE OR REPLACE FORCE VIEW vemp
AS
SELECT e.empno,
e.ename,
e.job,
e.mgr,
e.hiredate,
e.sal,
e.comm,
e.deptno
FROM emp e
WHERE e.deptno = pkg_vpd.f_get_context_value ('DEPTNO'); -- Can only view employees in same department
/
Create Custom Authentication Scheme

Shared Components / Authentication Schemes
Create
From Scratch
Name: VPD Demo
Page Sentry Function:
Session Verification Function:
Invalid Session Target: Page in This Application - 101 Login
Pre-Authentication Process:
Credentials Verification Method: Use my custom function to authenticate: return pkg_vpd.f_login
Post-Authentication Process: pkg_vpd.sp_apex_post_auth;
Cookies:
Logout URL: wwv_flow_custom_auth_std.logout?p_this_flow=&APP_ID.&p_next_flow_page_sess=&APP_ID.:1

Change current Authentication Scheme to "VPD Demo". This will vary between APEX 3.x and APEX 4.0

Create a Region Report with:

SELECT *
FROM vemp

Now login to your application with "KING/KING". Notice how you only see 3 rows in the report? Now logout and login with "MARTIN/MARTIN". You should now see 6 rows returned. As you can see, none of security was handled in the front end.

Tuesday, April 13, 2010

APEX Logs: Storing Mail Log Data

A while ago (almost a year ago) I wrote about how to permanently store APEX logs into your own tables (http://apex-smb.blogspot.com/2009/05/apex-logs-storing-log-data.html). I covered the APEX_WORKSPACE_ACCESS_LOG and APEX_WORKSPACE_ACTIVITY_LOG logs. I forgot to include how to permanently store the APEX_MAIL_LOG. You'll only need to do this if you use APEX_MAIL to send email.

1- Create the APEX Mail Log table

-- Mail Log
CREATE TABLE tapex_mail_log
AS SELECT * FROM apex_mail_log;
2- Update the APEX Mail Log table

Note: You may want to store this in a procedure and run as a nightly scheduled job so you don't forget to update the tables

BEGIN
-- apex_mail_log is linked to the workspace.
-- Need to access all the mail sent for each workspace that is linked to this schema

FOR x IN (SELECT workspace_id FROM apex_workspaces) LOOP
-- Set the workspace ID
wwv_flow_api.set_security_group_id (x.workspace_id);

INSERT INTO tapex_mail_log (mail_to,
mail_from,
mail_replyto,
mail_subj,
mail_cc,
mail_bcc,
mail_send_error,
last_updated_on)
SELECT aml.mail_to,
aml.mail_from,
aml.mail_replyto,
aml.mail_subj,
aml.mail_cc,
aml.mail_bcc,
aml.mail_send_error,
aml.last_updated_on
FROM tapex_mail_log x, apex_mail_log aml
WHERE NVL (aml.mail_to, -1) = NVL (x.mail_to(+), -1)
AND NVL (aml.mail_from, -1) = NVL (x.mail_from(+), -1)
AND NVL (aml.mail_replyto, -1) = NVL (x.mail_replyto(+), -1)
AND NVL (aml.mail_subj, -1) = NVL (x.mail_subj(+), -1)
AND NVL (aml.mail_cc, -1) = NVL (x.mail_cc(+), -1)
AND NVL (aml.mail_bcc, -1) = NVL (x.mail_bcc(+), -1)
AND NVL (aml.mail_send_error, -1) = NVL (x.mail_send_error(+), -1)
AND aml.last_updated_on = x.last_updated_on(+)
AND x.ROWID IS NULL;
END LOOP;
END;

Wednesday, March 24, 2010

How To Reference Package Variables Outside of PL/SQL

When developing with PL/SQL you may store public variables in the package specification. This has many uses, none of which I will get into for this post. The only catch in Oracle is that you can not easily reference these values in SQL statements outside of PL/SQL. The following example demonstrates this:

- Create Package Spec with Variable

CREATE OR REPLACE PACKAGE pkg_var
AS
c_my_var CONSTANT VARCHAR2 (5) := 'hello';
END pkg_var;


- Reference the variable in a SQL statement in SQL*Plus

SQL> SELECT pkg_var.c_my_var x
2 FROM DUAL;
SELECT pkg_var.c_my_var x
*
ERROR at line 1:
ORA-06553: PLS-221: 'C_MY_VAR' is not a procedure or is undefined

This results in an Oracle error.


- Try the same code, but in an block of PL/SQL

SQL> DECLARE
2 v_x VARCHAR2 (5);
3 BEGIN
4 SELECT pkg_var.c_my_var x
5 INTO v_x
6 FROM DUAL;
7
8 DBMS_OUTPUT.put_line (v_x);
9 END;
10 /
hello

PL/SQL procedure successfully completed.

As you can see this worked.


So how can we refernce package variables in a non-PL/SQL setting? I created the following function to do so. It will handle values that are of type VARCHAR2. I've also removed any spaces from the parameter (pkg_name.var_name) to ensure that no SQL injection will occur.

-- **
-- * Returns Package Variable value
-- * Note: for demo purposes I broke this function into various steps
-- *
-- * @param p_pkg_var_name fully qualified variable reference. Ex: pkg_x.var_y
-- * @return Varchar2 value
-- * @author Martin Giffy D'Souza: http://apex-smb.blogspot.com
-- **
CREATE OR REPLACE FUNCTION f_get_pkg_val_vc2 (p_pkg_var_name in varchar2)
RETURN VARCHAR2
AS
v_string VARCHAR2 (4000);
-- Full Variable Name (i.e. pkg.var)
v_var_full_name VARCHAR2 (61); -- Max of 61 chars since 30 + . + 30
BEGIN
v_var_full_name := p_pkg_var_name;
-- Remove any spaces to avoid SQL injections
v_var_full_name := REGEXP_REPLACE (v_var_full_name, '[[:space:]]', '');

EXECUTE IMMEDIATE 'begin :v_string := ' || v_var_full_name || '; end;'
USING OUT v_string;

RETURN v_string;
END f_get_pkg_val_vc2;


Now when you run in SQL*Plus you get the following:

SQL> SELECT f_get_pkg_val_vc2 ('pkg_var.c_my_var') x
2 FROM DUAL;

X
-----------------------------------------------------

hello

Thursday, October 15, 2009

Oracle and APEX Jobs

A lot of people were affected by the recession and may have lost their jobs. I know they're several companies looking for talented workers. To help out I've decided to start a small Jobs section on my blog (see right hand side). If you're interested in submitting a job, please leave a comment on this post with the following information:

- Job Title
- City the job is in
- Link to position

To keep with the theme of this blog please only submit Oracle (SQL,PL/SQL) and APEX related jobs.

On a side note I may create a small application where people can post jobs specific to APEX.

Monday, September 21, 2009

Enhanced APEX Session Timeouts

APEX has built in logic to set the lifetime of a session. To configure this option go to Shared Components / Edit Security Attributes / Maximum Session Idle Time in Seconds and set the time in seconds

This essentially terminates the user's session in the database and the next time they submit the page they'll be redirected to the login screen. The user will only know that they are logged out once they submit the page. If you have an Interactive Report (IR), or use Partial Page Refresh (PPR) the users won't know they're logged out. Instead it will look as though the report is still trying to load.


Another situation that may happen is that the user is filling out a long form on your page, their session timesout, then they click "submit". They'll be redirected to the login page and they'll lose all the information that they entered.

What if a user wants to extend their session? i.e. they haven't done anything to the page but would like a warning message before we automatically log them out? Or they are entering a log form and don't want to be logged out? I got this idea from the Air Canada web site when I was booking tickets. I really liked the fact that they let me know that they were to end my session, and gave me the option to extend my session.


The following solution will allow you to use APEX's session timeout and resolve the issues listed above. You can view the demo here: http://apex.oracle.com/pls/otn/f?p=20195:2600

Please note that since the demo page is set to public you can refresh after the session is supposed to have timedout and it will still work. If you set the Idle Session in APEX, this will work for pages that require authentication



Here's a high level overview of what this solution does:

  • Start a timer (pingApexSession) that will constantly "ping" (therefore refresh) your APEX session every X seconds.

  • Start a timer (idleTimer) to detect movement on the page.

  • If the idleTimer times out, give the user the option to extend session

  • If the user does not extend their session, terminate their session


I haven't put this code into a production application yet. As I mention below, I plan to make a jQuery plugin for this, so if you please send me any feedback that would be useful fur the plugin.

This solution uses jQuery and the following plugins:


- Create Application Process: AP_NULL
- Process Point: On Demand
- Name: AP_NULL
- Type: PL/SQL Anonymous Block
- Process Text: NULL;

- Create Application Process: AP_LOGOUT
- Process Point: On Demand
- Name: AP_LOGOUT
- Type: PL/SQL Anonymous Block
- Process Text:

BEGIN
apex_custom_auth.LOGOUT (p_this_app => :app_id,
p_next_app_page_sess => :app_id || ':1');
END;


- Create Region: "Extend Session" on Page 0
- Title: Extend Session
- Type: HTML Text
- Static ID: P0_REG_EXTEND_SESSION
- Region Attributes: style="display:none"
- Region Source: Your session will timeout in: <span id="timeoutCountdownDisplay" style="font-weight:bold"></span>
You can put whatever message you want. Just make sure the span tags exist for the countdown timer

- Create Button: "Extend Session" on Page 0
- Button Name: EXTEND_SESSION
- Text Label: Extend Session
- Display in Region: Extend Session
- Target is a: URL
- URL Target: javascript:gTimeout.timers.killSession.liveFn();

- Create Region: "Session Timedout" on Page 0
- Title: Session Ended
- Type: HTML Text
- Static ID: P0_REG_SESSION_ENDED
- Region Attributes: style="display:none"
- Region Source: Your session has ended. Please login.

- Create Button: "Login" on Page 0
- Button Name: LOGIN
- Text Label: Login
- Display in Region: Session Ended
- Target is a: Page in this Application
- Page: 1

- Create Region: "JavaScript - Session Timeout" on Page 0
- Title: JavaScript - Session Timeout
- Type: HTML Text
- Template: No Template
- Region Source:










You'll need to upload the JS files beforehand. Please see the list above to obtain the files

Here's the script to put into the region above. I separated them for display purposes.

I probably should have created this as a jQuery plugin. I may convert it later on

var gTimeout = {
//debug
debug: false, //Set to True to turn on debugging
debugFn: function(pMsg) {
if (gTimeout.debug){
console.log(pMsg);
}
}, //debug
modalRegions: {
//Region that contains the "Extend Session" information
extendSession: {
id: 'P0_REG_EXTEND_SESSION',
backgroundColor: '#CCC',
opacity: 70,
openFn: function() {
gTimeout.debugFn('gTimeout.modalRegions.extendSession.openFn');
// Start display timeout counter
$('#timeoutCountdownDisplay').countdown('destroy');
$('#timeoutCountdownDisplay').countdown({
until: '+' + (gTimeout.timers.killSession.time / 1000),
compact: true,
format: 'M:S'
});
// Load modal box to give user option to extend session
$('#' + gTimeout.modalRegions.extendSession.id).modal({
overlayCss: {backgroundColor: this.backgroundColor},
opacity: this.opacity
});
return;
}, //openFn
closeFn: function(){
gTimeout.debugFn('gTimeout.modalRegions.extendSession.closeFn');
$.modal.close();
return;
}//closeFn
},
//Region that will be displayed if the user does not extend thier session
sessionEnded: {
id: 'P0_REG_SESSION_ENDED',
backgroundColor: 'black',
opacity: 70,
openFn: function() {
gTimeout.debugFn('gTimeout.modalRegions.sessionEnded.openFn');
// Close Extend Sessios modal window
gTimeout.modalRegions.extendSession.closeFn();
// Open Logout modal window
$('#' + gTimeout.modalRegions.sessionEnded.id).modal({
overlayCss: {backgroundColor: this.backgroundColor},
opacity: this.opacity
});
return;
}// openFn
}//sessionEnded
},//modalRegions
timers: {
//Ping APEX Session timer will update the database session timer
pingApexSession: {
id: -1,
time: 5000, //Time to keep database session alive. This should be really close to the APEX idle time
loadFn: function(){
gTimeout.debugFn('gTimeout.timers.pingApexSession.loadFn:');
this.id = setTimeout('gTimeout.timers.pingApexSession.fn();', this.time);
return;
},
unloadFn: function(){
gTimeout.debugFn('gTimeout.timers.pingApexSession.unloadFn:');
clearTimeout(this.id);
this.id = -1;
return;
},//unloadFn
fn: function(){
gTimeout.debugFn('gTimeout.timers.pingApexSession.fn: Extending APEX Session');
jQuery.jApex.ajax({
appProcess: 'AP_NULL',
success: function(){},
async: true
});
gTimeout.timers.pingApexSession.loadFn();
return;
}//fn
},//pingApexSessions
//Kill current session. This is called when the user gets the option to extend their session
killSession: {
id: -1,
time: 5000, // Time to kill the APEX session once launched. Should only be run when extend session popup box is loaded
loadFn: function(){
gTimeout.debugFn('gTimeout.timers.killSession.loadFn:');
this.id = setTimeout('gTimeout.timers.killSession.killFn();', this.time);
return;
},
unloadFn: function(){
gTimeout.debugFn('gTimeout.timers.killSession.unloadFn: ');
clearTimeout(this.id);
this.id = -1;
gTimeout.modalRegions.extendSession.closeFn(); // Close extendSession Modal
return;
},
killFn: function(){
gTimeout.debugFn('gTimeout.timers.killSession.killFn: Killing APEX Session');
// Open Logout modal window
gTimeout.modalRegions.sessionEnded.openFn();
// Stop ping Apex session
gTimeout.timers.pingApexSession.unloadFn();
// Logout APEX session
jQuery.jApex.ajax({
appProcess: 'AP_LOGOUT',
success: function(){},
async: true
});
return;
},
// Prevents the session from being killed. We should be in a about to kill state now
liveFn: function(){
gTimeout.debugFn('gTimeout.timers.killSession.liveFn: ');
//Check that we're about to be killed
if (this.id == -1){
alert('Session is not marked to be killed');
return;
}

// Stop the kill timer
this.unloadFn();
return;
}//liveFn
},//killSession
// Timer for user movement time
idle:{
time: 5000, // Time to load the "Extend Session" popup box
loadFn: function(){
gTimeout.debugFn('gTimeout.timers.idle.loadFn:');
$.idleTimer(this.time);
$(document).bind("idle.idleTimer", function(){gTimeout.timers.idle.idleFn();});
// Trigger countdown timer
return;
},
idleFn: function(){
gTimeout.debugFn('gTimeout.timers.idle.idleFn:');
// Load modal box to give user option to extend session
gTimeout.modalRegions.extendSession.openFn();
// Only load if we're not in a kill state
if (gTimeout.timers.killSession.id == -1){
gTimeout.timers.killSession.loadFn();
}
return;
} //idle Fn
} // idle
},//timers
loadFn: function() {
gTimeout.timers.pingApexSession.loadFn(); // Keep database sessions alive
gTimeout.timers.idle.loadFn(); // Turn on user idle timer
return;
}//loadFn

};//gTimeout

$(document).ready(function(){
// Set Parameters
gTimeout.timers.pingApexSession.time = 5 * 1000; // Refresh APEX session every 5 seconds. This should be really close to your apex session timeout values
gTimeout.timers.idle.time = 10 * 1000; // 10 seconds of inactivity will trigger this window
gTimeout.timers.killSession.time = 10 * 1000; // Once the warning message pops up, user has 10 seconds to extend their session
// Configure Modal windows (not required)
gTimeout.modalRegions.extendSession.backgroundColor = '#CCC';

gTimeout.loadFn();
});

Thursday, September 17, 2009

Redirects for APEX Tabs instead of page submits

With APEX Tabs you may not want the page to be submitted each time the user clicks on a tab. If you look at the link for the tabs they look like: javascript:doSubmit('xxxx'); Where "xxx" is the name of the tab. "doSubmit" will submit the page and could trigger page computations, validations, and processes. If all you want to do is use the tabs as a form of navigation (i.e. when you click on a tab it redirects to another page) then this could cause some problems.

To avoid triggering Page Processing on tabs you can modify all the conditions on your Page Process (this could be very long) or change the link. They're multiple ways to change the links, here's a simple one using jQuery.

Here's a link to the demo: http://apex.oracle.com/pls/otn/f?p=20195:2500. Note, in the demo I don't trigger the javascript to change the link automatically so you can see what they look like before and after changing the links.

- Create an Application Process
Note: You may want to only apply this to certain pages depending on the use case

Name: AP_UNSUBMIT_TABS
Process Point: On Load: After Header

BEGIN
FOR x IN (SELECT tab_name,
tab_page,
'' tabinfo
FROM apex_application_tabs t
WHERE application_id = :app_id)
LOOP
HTP.p (x.tabinfo);
END LOOP;
END;


- Create a HTML region
Note: You'll need to install the jQuery JS file in Shared Components / Static Files




Tuesday, September 15, 2009

Tooltip Help in APEX: An alternative to popup help

The default help functionality for APEX is ok but can cause some problems with users browsers since it uses popup windows to display the help. If you do some digging around regarding web development best practices you'll find a lot of articles discussing why you should avoid popup windows. Instead of using the default help popup windows I prefer to use tooltips to display the help. Besides avoiding the popup window, tooltips generate a good user experience by displaying item help very quickly.

This demo uses a jQuery tooltip plugin: http://bassistance.de/jquery-plugins/jquery-plugin-tooltip/. Please visit the Bassistance website to find out how to configure the look and feel of the tool tips.

Here's a link to the demo: http://apex.oracle.com/pls/otn/f?p=20195:2400



- Create or Update Label Template
Note: You don't need to create a new template. If you want to, just update the existing templates
- Copy the "Optional Label with Help" and rename to "Optional Label with ToolTip".
Note: You can do this for required labels as well

- Change the "Before Label" From:
<label for="#CURRENT_ITEM_NAME#" tabindex="999"><a class="t20OptionalLabelwithHelp" href="javascript:popupFieldHelp('#CURRENT_ITEM_ID#','&SESSION.')" tabindex="999">

To:
I removed the href reference and replaced with #
<label for="#CURRENT_ITEM_NAME#" tabindex="999"><a class="t20OptionalLabelwithHelp" href="#" tabindex="999">

- Create a HTML region
This can be done on P0 to load for each page
Don't forget to upload the jQuery and tooltip JS files into Shared Components / Static Files









- Create an Application Process
Name: AP_GET_HELP_TEXT
Process Point: On Load: After Header


BEGIN
FOR x IN (SELECT ''
|| item_help_text
|| '
' help_html
FROM apex_application_page_items
WHERE application_id = :app_id
AND page_id = :app_page_id
AND item_help_text IS NOT NULL)
LOOP
HTP.p (x.help_html);
END LOOP;
END;


- Change Item Labels
Change Item labels to "Optional Label with ToolTip". Only do this if you created a new template

Monday, September 14, 2009

Updateable Interactive Report for APEX

A colleague had a requirement where he had over 10,000 rows of data which had to be updateable. Using Interactive Reports (IR) was the preferred approach as it would allow users to filter the data to modify the rows they wanted to before submitting the page. Tabular forms wouldn't work since the page would be to large. This is the solution that I proposed to make "Updateable IRs".

The following solution will work with IRs and standard reports with pagination. If the users applies filters or paginates to another set of data, the changes they make will remain in the collection. This is using a similar technique that I wrote about for APEX Report with checkboxes (advanced). To summarize this process:
  • Store the current query data into a collection as well as the md5 checksum.
  • Build an IR against the collection and use APEX_ITEMs to display input fields.
  • When a user changes a field, we submit that change to the collection.
  • Once the user is done with their changes you'll need to process the collection as required. In the last step in this example I have a query that will help identify changed rows.

You can do a lot with this approach but if you don't have an urgent need I'd suggest holding off until APEX 4.0. They're some security issues that would need to be addressed before launching this code in a public application. I didn't include the security updates in this example since I did not want to lose scope of the base functionality. Updating the code to make it secure shouldn't be too difficult.

Here's the link to the demo: http://apex.oracle.com/pls/otn/f?p=20195:2300



- Create IR Report Region
Note: You can use this for regular reports with pagination as well

SELECT e.empno,
apex_item.text (2,
ac.c002,
NULL,
NULL,
'class="updateableIR" seqid="'
|| ac.seq_id
|| '" attrNum="2"'
) ename,
apex_item.text (3,
ac.c003,
NULL,
NULL,
'class="updateableIR" seqid="'
|| ac.seq_id
|| '" attrNum="3"'
) job,
apex_item.select_list_from_query
(4,
ac.c004,
'select E.ENAME d, E.EMPNO r from emp e where E.EMPNO != '
|| e.empno,
'class="updateableIR" seqid="' || ac.seq_id || '" attrNum="4"',
'YES',
NULL,
'- Manager -'
) mgr,
apex_item.text (5,
ac.c005,
NULL,
NULL,
'class="updateableIR" seqid="'
|| ac.seq_id
|| '" attrNum="5"'
) hiredate,
apex_item.text (6,
ac.c006,
NULL,
NULL,
'class="updateableIR" seqid="'
|| ac.seq_id
|| '" attrNum="6"'
) comm,
apex_item.select_list_from_query
(7,
ac.c007,
'SELECT d.dname d, d.deptno r FROM dept d',
'class="updateableIR" seqid="'
|| ac.seq_id
|| '" attrNum="7"',
'YES',
NULL,
'- Department -'
) deptno
FROM apex_collections ac, emp e
WHERE ac.collection_name = :p2300_collection_name AND ac.c001 = e.empno


- Create Region Items
- Hidden & Protected: P2300_COLLECTION_NAME

- Create Page Computation
Note: This is just to set the collection name. You can call it whatever you want

Item: P2300_COLLECTION_NAME
Computation Point: Before Header
Computation Type: Static Assignment
Computation: P2300_IR_COLLECTION

- Create a Page Process (PL/SQL)

Name: Load Collection
Process Point: On Lead - Before Header

-- This creates the collection if it isn't created yet.
DECLARE
v_collection_name apex_collections.collection_name%TYPE
:= :p2300_collection_name;
v_reset_flag VARCHAR2 (1) := 'N';
BEGIN
-- Create collection if it does not exist or reset collection required
IF apex_collection.collection_exists
(p_collection_name => v_collection_name) =
FALSE
OR v_reset_flag = 'Y'
THEN
apex_collection.create_collection_from_query
(p_collection_name => :p2300_collection_name,
p_query => q'! SELECT empno,
ename,
job,
mgr,
TO_DATE (hiredate, 'DD-MON-YYYY'),
comm,
deptno
FROM emp !',
p_generate_md5 => 'YES'
); -- Generated md5 is important to help identify changed columns
END IF;
END;


- Create HTML Region to store JS code:
Note: This uses jQuery and Tyler Muth's jApex plugin. You'll have to upload the .js files as static files in Shared Components.








- Create Application Process:
Name: AP_UPDATE_COLLECTION_CELL
Process Point: On Demand


-- AP_UPDATE_COLLECTION_CELL
BEGIN
apex_collection.update_member_attribute
(p_collection_name => apex_application.g_x01,
p_seq => apex_application.g_x02,
p_attr_number => apex_application.g_x03,
p_attr_value => apex_application.g_x04
);
END;


- Query to see which rows were changed

SELECT ac.collection_name, ac.seq_id, ac.c001, ac.c002, ac.c003, ac.c004,
ac.c005, ac.c006, ac.c007, ac.md5_original,
CASE
WHEN apex_collection.get_member_md5
(:p2300_collection_name,
ac.seq_id) =
ac.md5_original
THEN 'NO'
ELSE 'YES'
END row_change
FROM apex_collections ac
WHERE collection_name = :p2300_collection_name

Wednesday, September 2, 2009

APEX Rules & Guidelines

After several late nights of nerding I've finally submitted/completed my APEX application for the APEX Developer Competition 2009.

You can demo and download the application here: http://apex.oracle.com/pls/otn/f?p=46865
Note: Demo is no longer up and will not be supported.

This purpose of this application is to allow development teams to create an online Rules & Guidelines development document. The template for this application is based on Patrick Cimolini's (Cayman Islands Government) presentation at ODTUG.

The concept of the R&G template is:

Rule: Follow 100% of the time
Guideline: Follow 90% of the time

Mandatory Headings:

Rule/Guideline: Active voice sentence.
Why:Important for Team Buy-In

Optional Headings:

Result: Illustration of desired outcome
How: How the result is achieved.
Notes: State the non-obvious
See Also: Point to related Rules and Guidelines

You can use this template for both APEX development and your PL/SQL standards. I've included the installation files in the demo application so feel free to install at your organization. If you are using it please post any comments/suggestions on this post and I'll try to write some updates.



Friday, July 24, 2009

Oracle: How to update all sequences

If you ever do data refreshes from production to development or test environments you may run into an issue where your sequences are not up to date. It seems that Oracle exports the sequences first, then the data. If your sequence numbers change during the entire export process you may get errors when using them in your refreshed schema.

To fix this problem you can try to find where your sequences are used and get the MAX(value) to find the next value. Alternatively you can just add a large random number, say 1,000,000, to all your sequences. For most users this will fix the problem and is very easy to do. Here's how:


-- Update all sequences
DECLARE
v_increase_by NUMBER;
v_bkp_increment_by NUMBER;
v_str VARCHAR2 (1000);
v_count NUMBER;
BEGIN
v_increase_by := 1000000;

FOR rec IN (SELECT *
FROM user_sequences) LOOP
-- Backup current incrementation number
v_bkp_increment_by := rec.increment_by;
-- Alter the sequence to increase by a defined amount
v_str := 'alter sequence ' || rec.sequence_name || ' increment by ' || v_increase_by;

EXECUTE IMMEDIATE v_str;

-- Increase by that amount
v_str := 'select ' || rec.sequence_name || '.nextval from dual';

EXECUTE IMMEDIATE v_str
INTO v_count;

-- Reset the increment factor
v_str := 'alter sequence ' || rec.sequence_name || ' increment by ' || v_bkp_increment_by;

EXECUTE IMMEDIATE v_str;
END LOOP;
END;
/