Skip to main content

The magic of dynamic views in APEX

E

Everyone should know the power of WITH clause these days. For me this is a great way how to have more readable views and with some magic it also allows me to have dynamic views (well, the views returning different rows based on page items).


You are asked to show a monthly based calendar on a page (and let's skip the Calendar region for demo purposes) based on a page item.

Here is how you can utilize WITH clause on a regular query (returning a calendar data for a whole year):

WITH days AS (
    SELECT
        TRUNC(SYSDATE, 'Y') + LEVEL - 1                         AS day,
        TO_CHAR(TRUNC(SYSDATE, 'Y') + LEVEL - 1, 'IYIW')        AS week,
        TO_CHAR(TRUNC(SYSDATE, 'Y') + LEVEL - 1, 'MM/YYYY')     AS month
    FROM DUAL
    CONNECT BY LEVEL <= ADD_MONTHS(TRUNC(SYSDATE, 'Y'), 12) - TRUNC(SYSDATE, 'Y')
)
SELECT
    d.month,
    d.week,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'MON', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS mon,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'TUE', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS tue,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'WED', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS wed,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'THU', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS thu,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'FRI', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS fri,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'SAT', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS sat,
    MAX(DECODE(TO_CHAR(d.day, 'DY'), 'SUN', TO_NUMBER(TO_CHAR(d.day, 'FMDD')))) AS sun
FROM days d
GROUP BY d.month, d.week
ORDER BY 1, 2;

Almost always I see this done as a region with whole query as a region source, with page items binded here and there:

SELECT
    ...
FROM days d
WHERE d.month = :P100_MONTH
ORDER BY 1, 2;


I really don't like queries on regions. You can read about this on my Object Referencing article. So I would create a view and use that as a region source.

CREATE OR REPLACE VIEW p100_calendar_v AS
SELECT
    ...
FROM days d
ORDER BY 1, 2;

Since you can't use the binded variables in a view, you would have to specify the WHERE condition under the region source. Which sucks, because when you change the region source, you will loose this filter and also you now have half of the logic in database view and other on region WHERE filter.

month = :P100_MONTH  -- :P100_MONTH := TO_CHAR(TRUNC(SYSDATE), 'MM/YYYY');


How can you use page items in a view?

Most people don't realize you can actually access the page items (and other APEX things) from PL/SQL through the APEX_UTIL.GET_SESSION_STATE function.

CREATE OR REPLACE VIEW p100_calendar_v AS
WITH x AS (
    SELECT
        APEX_UTIL.GET_SESSION_STATE('P100_MONTH') AS month
    FROM DUAL
)
SELECT
    ...
FROM days d
JOIN x
	ON x.month = d.month
ORDER BY 1, 2;

This is great, but it is slow. I didn't realized this at first, but calling APEX_UTIL.GET_SESSION_STATE will fire for each row in a query. Hence you will face performance issues and you might abandon this solution. Don't.

All you need is a hint (and I recommend to read article by Tomasz Lesinski and article by Tim Hall):

CREATE OR REPLACE VIEW p100_calendar_v AS
WITH x AS (
    SELECT /*+ MATERIALIZE */
        APEX_UTIL.GET_SESSION_STATE('P100_MONTH') AS month
    FROM DUAL
)
SELECT
    ...
FROM days d
JOIN x
	ON x.month = d.month
ORDER BY 1, 2;

With this solution you can have all logic stored in a view. You are not limited just to views or just to page items. Or just getting the values. You can get/set page/application item values from a procedure too. Or get values passed from IG.


Base views

I was asked by a client to create 3 similar reports on 3 different pages. I did used these WITH clause and page items there and I have realized these filters are basically the same on all reports. So I created a REPORT_BASE_V view to filter data on one place and in the reports I have selected data from this view. Later when I was asked to add a new filter, I have added it just to the base view. Simpler, faster, less bugs, less clutter, less copy pasting... I love this.

The trick is that I have checked the page number from which the view is called and adjusted the page item name based on that. So the same report_base_v view would use P100_MONTH item on page 100, but P200_MONTH item when called from page 200. Neat.

CREATE OR REPLACE VIEW report_base_v AS
WITH x AS (
    SELECT /*+ MATERIALIZE */
        core.app.get_item('$MONTH') AS month  -- this return different item on different pages
    FROM DUAL
)
SELECT
    ...
FROM days d
JOIN x
	ON x.month = d.month
ORDER BY 1, 2;

You can find this in my APP package:

    FUNCTION get_item (
        in_name                 VARCHAR2
    )
    RETURN VARCHAR2
    AS
        v_item_name             apex_application_page_items.item_name%TYPE;
    BEGIN
        v_item_name := app.get_item_name(in_name);
        --
        IF v_item_name IS NOT NULL THEN
            RETURN APEX_UTIL.GET_SESSION_STATE(v_item_name);
        END IF;
        --
        RETURN NULL;
    END;



    FUNCTION get_item_name (
        in_name                 apex_application_page_items.item_name%TYPE,
        in_page_id              apex_application_page_items.page_id%TYPE            := NULL,
        in_app_id               apex_application_page_items.application_id%TYPE     := NULL
    )
    RETURN VARCHAR2
    AS
        v_item_name             apex_application_page_items.item_name%TYPE;
        v_page_id               apex_application_page_items.page_id%TYPE;
        v_app_id                apex_application_page_items.application_id%TYPE;
        is_valid                CHAR;
    BEGIN
        v_app_id        := NVL(in_app_id, app.get_app_id());	-- APEX_APPLICATION.G_FLOW_ID
        v_page_id       := NVL(in_page_id, app.get_page_id());  -- APEX_APPLICATION.G_FLOW_STEP_ID
        v_item_name     := REPLACE(in_name, '$', 'P' || v_page_id || '_');

        -- check if item exists
        BEGIN
            SELECT 'Y' INTO is_valid
            FROM apex_application_page_items p
            WHERE p.application_id      = v_app_id
                AND p.page_id           IN (0, v_page_id)
                AND p.item_name         = v_item_name;
        EXCEPTION
        WHEN NO_DATA_FOUND THEN
            BEGIN
                SELECT 'Y' INTO is_valid
                FROM apex_application_items g
                WHERE g.application_id      = v_app_id
                    AND g.item_name         = in_name;
            EXCEPTION
            WHEN NO_DATA_FOUND THEN
                RETURN NULL;
            END;
        END;
        --
        RETURN v_item_name;
    END;


MVC design pattern

Remember design patterns from "real" programming languages? Think about MVC and APEX...

  • Model = your tables - basically your stored data
  • Controller = your packages, views, sequences - basically ALL your business logic
  • View = your APEX app (pages, regions, components) to present data to user and gather inputs

Why would you store some business logic on a View layer? It doesn't belong there.


Comments

Post a Comment