Jul 02

Following on from the last post, given that we were automatically creating hidden items to support the grid filters we still needed to add the conditions to the SQL query. I toyed with the idea of putting the required additional query structure in a table, out to a file, or to the screen, but I thought: Why don’t I just update the query source myself dynamically. So next comes the problem, is there an API call to do this or will I have to update an APEX table…..

Initially I though I found my answer, wwv_flow_api.set_plug_source, but after implementing the code to do the update I was getting ORA errors relating to LOB(s) given that the API call requires a varchar2 for plug_source but the underlying table column used a CLOB I came to the conclusion that either I had the wrong API call or it was simply deprecated/not in use.

So the solution I adopted was to update the APEX table directly (yeah I know bit of hack and not supported but come on it’s not like I’m going to break anything, well unless my code is wrong which happened during development on multiple occasions. Remember always ensure you have a good backup policy in place! Oh and don’t forget to ammend this when you upgrade APEX in the future!): “apex_030200.WWV_FLOW_PAGE_PLUGS”

Here’s the dependency SQL…

grant select,update on apex_030200.WWV_FLOW_PAGE_PLUGS to USERNAME
/
create synonym USERNAME.WWV_FLOW_PAGE_PLUGS for apex_030200.WWV_FLOW_PAGE_PLUGS
/

Before I get to the underlying PLSQL code, as this is a bit of a hack and only required in development I decided that I wanted to code it in such a way that the grant will only be provided in the development database however the PLSQL package it resides in, will be migrated to production, therefore I’d use “EXECUTE IMMEDIATE” to perform the update so that the package would still be valid even if he grant wasn’t applied.

Here’s the PLSQL source extract…

.............
          --
          -- For dates we will apply the defined format otherwise we will rely on
          -- the application format default
          --
          IF c1.data_type = 'DATE' AND c.format_mask IS NOT NULL THEN
            v_qry_filter := v_qry_filter ||' AND (('||c.column_alias||' <= '
                                         ||'to_date(:'||v_item_name||','''||c.format_mask||''')) OR (:'|| v_item_name
                                         ||' IS NULL))'||CHR(10)||CHR(13);
          ELSE
            v_qry_filter := v_qry_filter ||' AND (('||c.column_alias||' <= :'
                                         ||v_item_name||') OR (:'|| v_item_name
                                         ||' IS NULL))'||CHR(10)||CHR(13);
          END IF;
..........
    IF v_items_added THEN

      --
      -- Allocate memory (temp table space) for the clob.
      --
      dbms_lob.createtemporary
      ( lob_loc  => v_qry_source
      , cache    => TRUE
      ) ;

      --
      -- Lets make sure we have the grant to update the table
      --
      FOR c_has_tab_priv IN (
        SELECT table_name
        FROM   user_tab_privs
        WHERE  table_name = 'WWV_FLOW_PAGE_PLUGS'
        AND    owner      = wwv_flow.g_flow_schema_owner
        AND    privilege  = 'UPDATE'
      ) LOOP

        --
        -- We need to update the query definition with our new filters
        --
        FOR c IN (
          SELECT region_id
          ,      region_source
          FROM   apex_application_page_regions v
          WHERE  application_id  = v('APP_ID')
          AND    page_id         = v('APP_PAGE_ID')
          AND    template IN ('Ext.grid','Ext.grid.edit')
        ) LOOP
          --
          -- To simplify adding our filters to an exisiting query we will simply
          -- wrap the query in an inlinve view and perform the filters on the inline view
          --
          dbms_lob.write
          ( lob_loc     => v_qry_source
          , amount      => length('SELECT * FROM (')
          , offset      => 1
          , buffer      => 'SELECT * FROM ('
          );

          dbms_lob.append
          ( dest_lob     => v_qry_source
          , src_lob      => c.region_source
          );

          dbms_lob.write
          ( lob_loc     => v_qry_source
          , amount      => length(') WHERE 1=1 '||v_qry_filter)
          , offset      => DBMS_LOB.getlength(v_qry_source)+1
          , buffer      => ') WHERE 1=1 '||v_qry_filter
          );

          --
          -- Lets parse the SQL query before saving it, and we won't save it if it's not valid
          --
          c_id := dbms_sql.open_cursor;
          dbms_sql.parse(c_id, v_qry_source, dbms_sql.native);
          dbms_sql.close_cursor(c_id);

          --
          -- Lets using EXECUTE IMMEDIATE rather than a explicit update statement
          -- in case people may not want to apply the grant on  WWV_FLOW_PAGE_PLUGS
          --
          EXECUTE IMMEDIATE 'UPDATE WWV_FLOW_PAGE_PLUGS '||
                            'SET    plug_source = :b1 '||
                            'WHERE  id          = :b2 '||
                            'AND    flow_id     = :b3 '||
                            'AND    page_id     = :b4'
          USING IN v_qry_source, c.region_id, v('APP_ID'), v('APP_PAGE_ID');

          --
          -- We need to erase the lob in case there is more than 1 grid on the page
          --
          v_source_length := DBMS_LOB.getlength(v_qry_source);

          dbms_lob.erase
          ( lob_loc     => v_qry_source
          , amount      => v_source_length
          , offset      => 1
          );
        END LOOP;
      END LOOP;

      --
      -- Free the temporary LOB
      --
      dbms_lob.freetemporary(v_qry_source);

    END IF;

The end result is that we can create a normal APEX report via the wizard, set the template to our Ext.grid template, update the column attributes to enable/disable filtering, and we’re done. It takes like 2 minutes to complete the report in full, so we get a big productivity boost by dynamically generating the items and appending the query source with our additional filters.

Jun 30

A while back we posted on adding grid filters to our APEX Ext Grid template by simply setting attributes under the “Column Definition” setting in the report definition.

In order to support grid filters on our APEX grid/report we need to create a number of hidden items on the page to filter the query by. e.g. here’s a query example

SELECT geoname_id
,      name
,      modification_date
,      latitude
,      longitude
,      population
FROM geoname
WHERE  (modification_date = to_date(:P9_EXT_MODIFICATION_DATE_EQ, 'DD-MON-YYYY') OR :P 9_EXT_MODIFICATION_DATE_EQ IS NULL)
AND instr(upper("NAME"),upper(nvl(:P9_EXT_NAME,"NAME"))) > 0
AND (geoname_id = :P 9_EXT_GEONAME_ID_EQ OR :P 9_EXT_GEONAME_ID_EQ IS NULL)
AND (geoname_id >= :P 9_EXT_GEONAME_ID_GT OR :P 9_EXT_GEONAME_ID_GT IS NULL)
AND (geoname_id <= :P 9_EXT_GEONAME_ID_LT OR :P 9_EXT_GEONAME_ID_LT IS NULL)
AND modification_date BETWEEN to_date(nvl(:P9_EXT_MODIFICATION_DATE_GT,'01-JAN-0001'), 'DD-MON-YYYY') AND to_date(nvl(:P9_EXT_MODIFICATION_DATE_LT,'01-JAN-2999'), 'DD-MON-YYYY')
AND (population > :P 9_EXT_POPULATION_GT OR :P 9_EXT_POPULATION_GT IS NULL)

For string filters we simply do a “like” comparison or “soundex” so we only require 1 hidden item. However for date and numeric filters we have a number of options, “Less Than / Before”, “Equal To / On”, and “Greater Than / After” so we need three hidden items for the one column filter.

We are faced with two options, we can either create these hidden items manually…. boring! or we can use the APEX data dictionary and API to dynamically create them using our own custom PLSQL. This is where APEX really gets really interesting, we can use wwv_flow_api to generate the items as well as a region to hold them. To make it that little bit sweeter, we’ll also add the PLSQL call to an application process that runs before header (this gives us the benefit of being within the APEX runtime engine), which means that for every new grid page we create, when we view the page in runtime mode (first time only) it will auto create the items for us! When it comes time for our production release we will simply set the condition of the application level process to “Never” to avoid any unnecessary processing.

When we create the hidden items, we need work out the data type for the column filter and create the necessary supporting hidden items which will appear in our query filter. We simply use the APEX data dictionary to get the report items with filtering enabled and then query the databae data dictionary to get the data type in order to work out what hidden items we need to generate.

Here’s an extract of our PLSQL…

    IF NOT v_region_exists THEN
      vId := wwv_flow_id.next_val;
      vPlugId := vId;
      wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
      wwv_flow_api.create_page_plug
      ( p_id                        => vId
      , p_flow_id                   => v('APP_ID')
      , p_page_id                   => v('APP_PAGE_ID')
      , p_plug_name                 => 'APExtJS Hidden Items'
      , p_region_name               => 'APExtJS Hidden Items'
      , p_plug_display_sequence     => '10000'
      , p_plug_template             => 0
      );
    END IF;
    --
    -- Lets have a look at the current page and see if there is a apex report
    -- defined and filtering enabled by setting the reference table fields
    -- if so we will create a hidden item if one does not exist
    --
    FOR c IN (
      SELECT v.column_alias
      ,      v.reference_schema
      ,      v.reference_table_name
      ,      v.reference_column_name
      FROM   apex_application_page_rpt_cols v
      WHERE  v.application_id        = v('APP_ID')
      AND    v.page_id               = v('APP_PAGE_ID')
      AND    upper(v.column_alias)   <> 'TOTALROWS'
      AND    v.reference_schema      IS NOT NULL
      AND    v.reference_table_name  IS NOT NULL
      AND    v.reference_column_name IS NOT NULL
      AND NOT EXISTS (SELECT item_name
                      FROM   apex_application_page_items
                      WHERE  application_id = v.application_id
                      AND    page_id = v.page_id
                      AND    (item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_LT'
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_EQ'
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_GT'
                              )
                    )
      ORDER BY v.region_id, v.display_sequence
    ) LOOP
      FOR c1  IN (SELECT data_type
                  FROM   all_tab_columns
                  WHERE  owner       = c.reference_schema
                  AND    table_name  = c.reference_table_name
                  AND    column_name = c.reference_column_name)
      LOOP
        IF c1.data_type IN ('VARCHAR2','CLOB','NVARCHAR2','NCLOB','CHAR') THEN
          --
          -- The following was taken from John Scotts book Pro Application Express
          -- page 675
          --
          vId := wwv_flow_id.next_val;
          wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
          wwv_flow_api.create_page_item
          ( p_id                        => vId
          , p_flow_id                   => v('APP_ID')
          , p_flow_step_id              => v('APP_PAGE_ID')
          , p_display_as                => 'HIDDEN'
          , p_item_plug_id              => vPlugId
          , p_name                      => 'P' || v('APP_PAGE_ID') || '_EXT_' || c.column_alias
          );
        ELSIF c1.data_type IN ('DATE','NUMBER','FLOAT') THEN
          --
          -- The following was taken from John Scotts book Pro Application Express
          -- page 675
          --
          vId := wwv_flow_id.next_val;
          wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
          wwv_flow_api.create_page_item
          ( p_id                        => vId
          , p_flow_id                   => v('APP_ID')
          , p_flow_step_id              => v('APP_PAGE_ID')
          , p_display_as                => 'HIDDEN'
          , p_item_plug_id              => vPlugId
          , p_name                      => 'P' || v('APP_PAGE_ID') || '_EXT_' || c.column_alias ||'_LT'
          );

If you interested in the dynamic generation of APEX components then you should have a look at the APEXGEN package, though it is a little disappointing it hasn’t been updated since it’s initial release, but the concept is fantastic. It won’t be long before we will be creating APEX applications with an Ext interface just using an ER model generated by SQL Developer’s new modeling tool. Of course there are a lot of code generators out there but none the likes of APEX with it’s excellent customization framework, so watch this space!

preload preload preload