Tuesday, January 24, 2012

Executing iterator binding in deferred Mode when there are required bind variable parameters

An iterator binding can be set to execute in "deferred" mode.  This is the default behavior if you don't change it.   What this means is that the query will not execute unless the page needs to display it's data.   This is great for use on a page that doesn't initially display the data to the user, like in a popup or tab that is not initially disclosed.  If the popup is displayed or the tab is disclosed, then and only then will the query be executed.  Also for tables that are set to lazy load (retrieve it's data after the page initially loads) this prevents the query from executing until the page is first displayed and the "Fetching data..."  message is displayed over the table.  A problem arises when the view object requires named bind variables.  The bind variables do not get set unless you add an ExecuteWithParams action binding to the task flow.   Calling an ExecuteWithParams will execute the query but that defeats the value of setting the iterator binding to "deferred"   So how do I set the bind variables without executing the query?

Here is one technique:
  • Edit the View object, select the Java panel and check the "Generate View Object Class" and "Include bind_variable accessors.
  • Edit the Client Interface and add the setMyValueBind()  method to the interface.
  • Select the Data Control panel and click the refresh icon to refresh the data controls.
  • Find your view object in the data controls panel and expand it.  You should see the setMyValueBind method under the list of attributes.
  • Drag the method call over onto your task flow and add an EL expression to set the bind variables value.
  • insert the method call into the chain of operations that execute prior to displaying the page.
That is all that is needed.  The first time the data is displayed, the iterator binding will execute the query using the set bind parameters and if the user never displays the popup or tab that displays the data, the query will not need to be executed.

Friday, January 13, 2012

How to cancel a long running query from the UI

I've long wondered if there was any way to cancel a query that is taking too long to complete from the page where the query is being run, similar to hitting the cancel button in SQL Developer.   I noticed that the view object class has a cancelQuery() method on it but how do you call that when the thread is busy running the query.   I did some experimenting and found that if I overrode the executeQuery method in the view and saved the view object on the session I could then use that to cancel the query from another task flow running in another tab or browser window.
   Here is the code
    @Override
    public void executeQuery() {

        Map sessionScope = 
            ADFContext.getCurrent().getSessionScope();
        sessionScope.put("MyQuery",this);
        try{
        super.executeQuery();
        } finally{
          // only keep on the Session 
          //for the duration of the query  
          sessionScope.remove("MyQuery");
       }

On a different page put a button for canceling the query that calls this action method
  
public void cancelQueries(ActionEvent actionEvent) {
    Map sessionScope = ADFContext.getCurrent().getSessionScope();
    ViewObjectImpl vo = (ViewObjectImpl)sessionScope.get("MyQuery");
    if (vo!=null){
        if (vo.isDead()==false){
          vo.cancelQuery();        

        }
      sessionScope.remove("MyQuery");
    }

}
          
This will cause the query to throw a "JBO-27126 Long running query has been canceled" Exception.
In order to prevent the page from becoming broken you must catch this exception in the executeQueryForCollection method and re-throw a new JboException which will be displayed to the user.   See this blog for an example of that.


This seems to work fine but how would I put a button on the same browser tab as the one running the query.   I haven't implemented that but I can think of a couple of ways that might be done.  One would be to have the button call a javascript method that makes a custom AJAX call back to a plain old servlet that retrieves the users session and calls the cancelQuery method.  Another might be to add an inline frame that has a URL to a different task flow and page containing only the single button.  ADF doesn't let you submit a button on the same page until the page is no longer busy but using an inline frame is the same as running the task flow on another tab so concurrent requests are allowed.


I make no guarantees that this isn't violating some threading rules for view objects by calling the cancelQuery method on a different thread but for my experiments the query was canceled fine and the other page woke up and displayed the timeout message.  Results may vary.



Thursday, September 22, 2011

How to Ignore Time Component of Date column when filtering on Date.

When you create a table that supports column filtering,  the default behavior for filtering on dates is to compare the date directly.  This means that if the date in the database has a time component the row will not match and no results will be found.    If you don't need to display the time along with the date in the table then you can just put a trunc(...) around the date in the original query.   If you do need to display the time then you can add another view attribute to your query that outputs the trunc'd version of the date.   The new column will not be displayed but will be used for sorting and filtering.   The af:column component has a sortProperty attribute that usually contains the column attribute name for the column displayed below it.   This does not have to be the case however.  The sortProperty can be set to any column in the view object so that when you put a value in the filter field it filters and sorts on a different column then the one displayed below it.  Set the sortProperty for the date/time column to the date only column and you will get the desired behavior.  When you filter on a date it will return all rows with that date regardless of the time portion of the date.

There is another way to accomplish this that will make every Date filter within your application time agnostic so that developers do not need to add this extra column every time they want to filter on the date portion of a date/time column.To do this you need to create a custom SQL builder class that extends OracleSQLBuilderImpl and override the getFormattedLHSCompareFragment method (Left Hand Side Compare Fragment)  This is called whenever filtering is performed.   In this method add the trunc(...) around the value.  Sample code is shown below.


This class is activated by adding a JVM parameter on startup


  -Djbo.SQLBuilder=com.yourCompany.model.custom.MySQLBuilderImpl

/**
 * Override method in OracleSQLBuilderImpl in order to trunc (remove time component)
 * when filtering on Dates in tables (Query By Example) QBE
 * This class is activated by adding a JVM parameter
 *  -Djbo.SQLBuilder=com.yourCompany.model.custom.MySQLBuilderImpl
 */
public class MySQLBuilderImpl extends OracleSQLBuilderImpl {
    
    private static final Log log = LogFactory.getLog(MySQLBuilderImpl.class);
    private static SQLBuilder mSQLBuilderInterface=null;  
    
    public MySQLBuilderImpl() {
    }


  /**
   * Gets the singleton instance of this class.
   * @return a SQLBuilder object.
   */
  synchronized public static SQLBuilder getInterface()
  {
     if (mSQLBuilderInterface == null)
     {
        if (Diagnostic.isOn())
        {
           Diagnostic.println("OracleSQLBuilder reached getInterface");
        }

        mSQLBuilderInterface = (SQLBuilder)(new MySQLBuilderImpl());

        if (Diagnostic.isOn())
        {
           Diagnostic.println(mSQLBuilderInterface.getVersion());
        }
     }
     return mSQLBuilderInterface;
  }
 

  @Override
  public String getFormattedLHSCompareFragment(ViewObject vo, ViewCriteria vc,
                                               AttributeDef attrDef,
                                               int sqltype, String lhs,
                                               Object rhs) {
   String value =   super.getFormattedLHSCompareFragment(vo, vc, attrDef, sqltype, lhs, rhs);
   
    if (sqltype==Types.DATE || sqltype==Types.TIMESTAMP) {
      
      // only apply trunc to database columns, not view alias names used for in-memory filtering
      // ADF does not handle a trunc for a in-memory filter and will get an error
      Integer attr = vo.getAttributeIndexOf(value); // 
      if (attr == -1){  // not found, meaning it's a database column name, not an attribute name.
      
       value = "TRUNC("+value+")";
        if (log.isDebugEnabled()) {
            log.debug("MySQLBuilderImpl truncating date value:"+value);
        }
      }
    }
    return value;
  }
}

Tuesday, January 18, 2011

Commiting an ADF view that contains an outer join

Here is my problem

  • I have a ADF writable View of table1 that that contains a left outer join to table2.  
  • Table2 has a foreign key to table1. 
  • Both Table1 and Table2 contain editable fields displayed in the UI.
  • Table1 contains an existing row to be edited and will always be commited but I don't want to create a row in table2 unless the user enters data in one of table2's fields
My problem was that if I tried to commit, I would get an "attribute x is required" message because the foreign key was missing.  To solve this I opened the view in jdeveloper, clicked on the Java tab and selected the "Generate View Row Class" so that a ViewRowImpl.java class is created.

 I then opened this generated class and updated the setter for each attribute that can be edited in the UI. Below is an example. The red text was what I added.   KeyAttribute is the primary key of table1 KeyAttribute2 is the foreign key in table2.  They need to be set to the same value before table2 can be commited. If there is currently no matching row in table2, KeyAttribute2 will be null.  KeyAttribute1 is a DBSequence and KeyAttribute2 is a Number, hence the call to getSequenceNumber to convert it to a Number.  With this code, now I don't get the error any more and also it won't commit a row in table2 if no data for it has been entered.

    public void setMyDate(Date value) {
        if (value!=null && getKeyAttribute2() == null){
          setKeyAttribute2(getKeyAttribute().getSequenceNumber());
        }
        setAttributeInternal(MYDATE, value);
    }

This works great when the row you are editing in table1 already exists.  If you want to create a new row in table1 and commit it at the same time as a new row in table2 it is a bit more challenging as you will need to generate a DBSequence to be added to both the Primary and Foreign key fields but only to the foreign key field if other data exists for table2, otherwise you will create a empty row in table2 with only the foreign key.

Monday, December 13, 2010

Accessing ADF components using client side script

Occasionally there is a need to access a client side component using javascript.   One technique is to send script  down to the page from the backing bean that looks up the component using the ADF javascript method
AdfPage.PAGE.findComponent('clientIdOfComponent');
That technique works well but requires that you bind the component to the backing bean or look up the component using the full naming container path.  The technique I am going to describe allows you to declaratively access a UI component in script without having to add java code in the backing bean.  Because the getClientId(FacesContext)   method of the UIComponent requires a parameter it cannot be called directly within a EL expression on the page. The only parameter that can be passed in a EL Expression is the key to a Map object.  Because of this I created a Application scope helper bean  (ClientIdMap) that implements the Map interface.   It's not a real map and it's sole purpose is to evaluate the Key as a EL expression and return the Client ID of the component it finds. Just add this as an Application scope bean to the adfc-config.xml file.  (unused methods are not shown but must be implemented). 

public class ClientIdMap implements Map {
    /**
     * @param key el expression where UIComponent is stored
     * @return client side component ID
     */
    public Object get(Object key) {
       
        String returnValue = "";
        Object val = JSFUtils.resolveExpression("#{"+key+"}");
        if(val instanceof UIComponent){
            returnValue = ((UIComponent)val).getClientId(FacesContext.getCurrentInstance());
        }
           
        return returnValue;
    }

}

Then add this line to the top of your page which sets the client ID into a javascript variable that can then be used by your script.  Don't ask how the double nested single quotes work but they do.

<trh:script text="var myPopup = '#{ClientIdMap['requestScope.myComponent']}';"/>

Notice that for this to work, an EL expression (without the surrounding #{}) must be passed that is the same as the binding for the component.  Note that you can bind a component to a backing bean or directly to requestScope as shown. 

<af:popup binding="#{requestScope.myComponent}" id="myPopup">


Binding to requestScope is easiest because it can be added at runtime without restarting the server.

Then to retrieve the UIComponent do something like this (shows the popup without making an ajax call to the server)


var popup = AdfPage.PAGE.findComponent(myPopup);
var hints = {autodismissNever:true};
popup.show(hints);

The advantage of retrieving the ClientId from the binding instead of adding the fully qualified path in your javascript is that ADF may add indexes to your client ID that may change.  (see this link) Also, if someone adds a naming container around your page, region or component the Client ID will change so it's never a good idea to hard code the client ID.

Removing rows from a Query Collection

Here's a link to a blog that describes how to remove Rows from a query Collection without deleting the row from the database.  This works for entity based views but I had a need to remove rows from a Query Collection of a non-entity based read only view.     The way I did this was to add a transient attribute "Displayed" to the view.  I then create a view criteria (filterCriteria) that filters out any row where Displayed='False'.  In the "Edit View Criteria" dialog I set the Query Execution Mode to "In Memory"   After executing the query in my backing bean I loop through the rows and set the Displayed attribute to 'False" for any row I don't want to display.   Then I apply an in-memory filter by executing the following

        setApplyViewCriteriaName(filterCriteria);
        setQueryMode(QUERY_MODE_SCAN_VIEW_ROWS);                                                           
        super.executeQuery();




Normally you would just filter the original results using SQL but in my case the rules for determining which rows to remove did not translated easily to SQL so this technique was employed.  This might be useful if you were filtering out rows based on the users permissions and those permissions were stored in LDAP and couldn't be made part of the query.

Wednesday, June 30, 2010

Case Insensitive Search

We had an interesting problem when migrating to the PS2 version of jdeveloper. All of our  QBE table filters that previously were case insensitive became case sensitive.


ADF supports case-insensitive queries but it relies on use of the UPPER() function to wrap the criteria and column selection.  We made design decisions a long time ago, that (A) the business data would not be converted to all upper case, and (B) the application search screens would not convert user criteria to upper case. To make all searches case-insensitive, the application use Oracle’s case-insensitive string comparison feature NLS_COMP=LINGUISTIC and NLS_SORT=BINARY_CI (where CI means case-insensitive). Further, to support indexed case-insensitive searches, the data modeler added linguist-sort indexes on all indexed character strings. That works fine for the “=” operator but we found out it does not work for the LIKE operator. I understand that this has been fixed in the 11g database. Unfortunately our 11G front-end is connected to a 10g database so filtering using the like operator will invalidate the indexes and produce a full table scan.  Furthermore, the use of the UPPER() function on the table column will invalidate the linguistic sort index so we can't use the ADF way of making filters be case insensitive.

The reason filters on tables became case sensitive in PS2 was that even though the database was case insensitive by default, ADF would perform an in-memory filter immediately after executing the query and the in-memory filter, being case sensitive would remove all of the non-matching rows from the result set.  The fix for this was a little tricky.   All of our view objects inherit from a custom base class we created.  In this class we have overridden the getViewCriteraClause(boolean forQuery) method.   This method is called whenever ADF is about to apply view criteria (i.e. filter)  for either a database or a in-memory query.   It conveniently lets you know which type (forQuery) it is requesting.  So now all we need to do is return null when forQuery==false and no criteria will be applied to in-memory filters.   But what if we actually want to perform in-memory filtering?   Well, we can check the query mode to determine if the query is a "database only" query  and no in-memory filtering should be performed.   The problem caused in PS2 was because ADF was performing this in-memory filter even though the query mode had not been set to do so. Therefore we can detect this condition and only allow in-memory filtering when the view's query mode is set to do so.

In the example code below, I'm allowing the in-memory filter to occur but simply calling vc.setUpperColumns(true); to put the UPPER() clause around the in-memory filter making it case insensitive. I want to do just the opposite when the query is bound for the database. There is one section of code added there to support date filters where we skip the in-memory filter all together by returning null. I'll talk about that in a later blog.



   public String getViewCriteriaClause(boolean forQuery) { 

    if (forQuery == false){
         // clause is for cache (in-memory)
    
    
  // WARNING - It is possible to set the criteria mode to cache and the query mode
  // to database.  That will query for a larger result set, then immediately filter it down in-memory
  // queryModeDoesNotUseCache will be true in that case even though it performs a
  // in-memory filter after the fact.  Doing so is not recommended as it wastes memory unless you
  

            boolean queryModeDoesNotUseCache =
                ((getQueryMode() & ~QUERY_MODE_SCAN_DATABASE_TABLES) == 0);


            ViewCriteria[] vcs =
                getApplyViewCriterias(ViewCriteria.CRITERIA_MODE_CACHE);
            if (vcs != null && vcs.length > 0) {

                boolean criteriaForCacheModeOnlyFound = false; //
                for (ViewCriteria vc : vcs) {

                    criteriaForCacheModeOnlyFound |=
                            (vc.getCriteriaMode() == ViewCriteria.CRITERIA_MODE_CACHE);

                    if (vc.isUpperColumns() == false) {
                        vc.setUpperColumns(true); // puts UPPER(..) around the values for filtering in-memory
                    }
                }
                if (queryModeDoesNotUseCache &&
                    !criteriaForCacheModeOnlyFound) {
                    // if query mode does not apply to cache and criteria mode
                    // is not "cache only" mode (i.e not for both cache & database)
                    // don't apply any in-memory filter.
                    // This is needed in PS2 to prevent in-memory filtering of dates which causes
                    // no results because the time component is compared as well.
                    // see CustomSQLBuilderImpl which has a PS2 workaround to trunc the date
                    // so the date filters work (without comparing time component)
                    // trunc will fail if applied to a in-memory filter on PS2
                    return null;
                }


            }
    } else{
      
      ViewCriteria[] vcs =
          getApplyViewCriterias(ViewCriteria.CRITERIA_MODE_QUERY);
      if (vcs != null && vcs.length > 0) {
          for (ViewCriteria vc : vcs) {

              if (vc.isUpperColumns()) {
                  vc.setUpperColumns(false); // remove UPPER(..) around the values for database queryies
              }
          }
      
      
      }
    }
        String clause = super.getViewCriteriaClause(forQuery);

        return clause;

    }