21.01.2016

ADF: Уход со страницы с незафиксированными данными

У концепции ADF есть интересное свойство: обязанность по отслеживанию незакоммиченных данных полностью ложится на программиста. Сложно сказать, как с этим планировалось справляться силами недоучившихся студентов, на которых рассчитано декларативное программирование в ADF. Да и не об этом речь. Речь пойдет о том, как уйти со страницы, которая создала новую запись в VO, связанном c EO, не зафиксировать эту запись, и потом благополучно вернуться обратно.

Что сразу приходит в голову:
    @PostConstruct
    public void initBean() {
        AppModuleImpl mod = (AppModuleImpl)ADFUtils.getBindingApplicationModule();
        DBTransaction conn = mod.getDBTransaction();
        if (conn.isDirty()) conn.rollback();
        ...
Так нехорошо, потому что initBean() будет вызываться при каждой активности на странице и транзакция будет теряться тогда, когда это не нужно. К тому же  транзакция будет откатываться на фазе prepareRender, что тоже идеологически неверно. То есть контекст уже проинициализирован, загружена модель данных, проверены валидации, всё готово к прорисовке страницы - и тут мы сообщаем системе, что данные нужно изменить. Не знаю, что именно будет происходить глубоко внутри приложения, но думаю, что ничего хорошего.

Рабочее решение заключается в создании бина уровня приложения, унаследованного от PagePhaseListener, где мы будем анализировать страницу на ранней фазе жизненного цикла и отслеживать грязные данные. Создаем бин и прописываем его в adf-settings.xml:


Сам класс:
    import java.util.ArrayList;
    import java.util.Map;
    import javax.faces.context.FacesContext;

    import javax.servlet.http.HttpServletRequest;
   
    import oracle.adf.controller.v2.lifecycle.PagePhaseEvent;
    import oracle.adf.controller.v2.lifecycle.PagePhaseListener;

    import oracle.adf.share.ADFContext;
   
    import oracle.jbo.server.DBTransaction;
   
    import *.ADFUtils;
    import *.AppModuleImpl;

public class PageListenerBean implements PagePhaseListener{
   
    // если conn.isDirty() и переходим на из url из списка, то rollback
    private static String[] deniedUrls = {"/myapp/faces/task-flow-definition/tree"};
   
    @Override
    public void beforePhase(PagePhaseEvent pagePhaseEvent) {
            try{
                    if (pagePhaseEvent.getPhaseId() == 9){
                        AppModuleImpl mod = (AppModuleImpl)ADFUtils.getBindingApplicationModule();
                        DBTransaction conn = mod.getDBTransaction();
                        if (isPageChanged()){
                            if (changedToDisallowed()){
                                if (conn.isDirty()){
                                    conn.rollback();
                                    System.out.println("= rolled back!");
                                }
                            }
                        }
                     }
            }catch(Exception e){
                ;          
            }
    }
   
    @Override
    public void afterPhase(PagePhaseEvent pagePhaseEvent) {
        ;       
    }
   
    private void setApplicationScopeParameter(String val){
        ADFContext adfCtx =  ADFContext.getCurrent();
        Map applicationScope = adfCtx.getApplicationScope();
        applicationScope.put("currentURL", val);
    }
   
    private String getApplicationScopeParameter(){
        ADFContext adfCtx =  ADFContext.getCurrent();
        Map applicationScope   = adfCtx.getApplicationScope();
        return (String)applicationScope.get("currentURL");
    }
   
    private boolean isPageChanged(){
        try{
            if (!getCurrentUrl().equals(getApplicationScopeParameter())){
                setApplicationScopeParameter(getCurrentUrl());
                return true;
            }
        }catch(NullPointerException e){
            System.out.println("--! PageListenerBean: can't get current url");
        }
        return false;
    }
   
    private boolean changedToDisallowed(){
        for(String url : this.deniedUrls)
            if (url.equals(getCurrentUrl()))
                return true;
        return false;     
    }
   
    private String getCurrentUrl(){
        FacesContext ctx = FacesContext.getCurrentInstance();
        HttpServletRequest servletRequest = (HttpServletRequest) ctx.getExternalContext().getRequest();
        return servletRequest.getRequestURI();
    }
}
При каждом срабатывании проверяется соответствие текущего URL переменной #{applicationScope.currentURL}. Если страница отличается, находится в "стоп-листе" и данные грязные, тогда откатываем транзакцию. Стоп-лист введен во избежание непредвиденных потерь транзакции при единой фиксации нескольких страниц (такое может быть в приложении).

Код 9 - id фазы инициализаци Restore View. Интересно проследить жизненный цикл страницы-источника (откуда переходим) после изменения данных:

В целевой странице (куда переходим)  грязные данные присутствуют во всем цикле:


14.01.2016

Переменные PL/SQL пакета: неочевидность

2 часа просидел над отладкой простейшего кода, не замечая подводного камня.
Допустим:
create or replace package VARTEST is
  BANNER varchar2(255);
  procedure test (p_cur out sys_refcursor);
end VARTEST;

create or replace package body VARTEST is
  procedure test (p_cur out sys_refcursor)
  is
  begin
    open p_cur for
       select count(*) from v$version v where v.BANNER = BANNER;
  end test;
end VARTEST;
Вызовем-ка функцию
begin
  VARTEST.BANNER := 'some awsome text';
  vartest.test(p_cur => :p_cur);
end;

--------------
count(*) 
5
Результат казался неожиданным, пока (случайно!) не выполнил запрос
select * from v$version v where v.BANNER = BANNER;
Запрос, на удивление, выполнился и честно вернул всё содержимое вьюхи. Т.е. BANNER - это в данном случае имя поля, а не переменная пакета.
Итого: имена переменных пакета лучше начинать с префикса, либо использовать явное указание принадлежности переменной пакету.