четверг, 10 октября 2013 г.

19й - практикум. Oracle ADF. Database Change Notification + Active Data Service

В этом практикуме соединим две технологии: Database Change Notification и Active Data Service (ADS). Т.е. будем следить за изменениями данных в таблице БД  на странице приложения в браузере.

Схема HR на Orcale XE.
На странице будет отображаться сумма зарплат по отделам. При изменении у служащего з.п., сразу увидим это изменение в сводном списке. 

От БД до сервера приложений эти уведомления доставим с помощью Database Change Notification, а от сервера приложений до клиента (браузера) доставим изменения с попощью ADS. При этом все будет происходить автоматически без участия пользователя.

Часть I - Database Change Notification 

public class DBChangeNotification

  void run()
    throws SQLException
  {

    // first step: create a registration on the server:
    Properties prop = new Properties();

    // Ask the server to send the ROWIDs as part of the DCN events (small performance
    // cost):
    prop.setProperty(OracleConnection.DCN_NOTIFY_ROWIDS, "true");
    //Set the DCN_QUERY_CHANGE_NOTIFICATION option for query registration with finer granularity.
    prop.setProperty(OracleConnection.DCN_QUERY_CHANGE_NOTIFICATION, "true");

    // The following operation does a roundtrip to the database to create a new
    // registration for DCN. It sends the client address (ip address and port) that
    // the server will use to connect to the client and send the notification
    // when necessary. Note that for now the registration is empty (we haven't registered
    // any table). This also opens a new thread in the drivers. This thread will be
    // dedicated to DCN (accept connection to the server and dispatch the events to
    // the listeners).
    dcr = conn.registerDatabaseChangeNotification(prop);

    try
    {
      // add the listenerr:
      DCNDemoListener list = new DCNDemoListener(this);
      dcr.addListener(list);
      System.out.println("registration.");
      // second step: add objects in the registration:
      Statement stmt = conn.createStatement();
      // associate the statement with the registration:
      ((OracleStatement) stmt).setDatabaseChangeRegistration(dcr);      
      ResultSet rs = stmt.executeQuery("select employee_id, salary from EMPLOYEES where department_id > 10");
      while (rs.next())
      {
      }
Здесь  происходит настройка свойств уведомления и их регистрация на сервере, создание Listener, а также регистрируется запрос результат выборки которого будем мониторить. Т.е. если в таблице Eployees изменится salary, то мы получим уведомление в Listener. Одно из свойств на которое подписались - это ROWID измененной записи.

public class DCNDemoListener

public class DCNDemoListener
  implements DatabaseChangeListener
{

  @Override
  public void onDatabaseChangeNotification(DatabaseChangeEvent databaseChangeEvent)
  {
    System.out.println("onDatabaseChangeNotification");
    QueryChangeDescription[] changeDescription = databaseChangeEvent.getQueryChangeDescription();

    //System.out.println("changeDescription:"+changeDescription);
    for (QueryChangeDescription change: changeDescription)
    {
      TableChangeDescription[] tdescription = change.getTableChangeDescription();
      for (TableChangeDescription tchange: tdescription)
      {
        RowChangeDescription[] rchange = tchange.getRowChangeDescription();
        for (RowChangeDescription rcd: rchange)
        {
          System.out.println("Affected ROWID -> " + rcd.getRowid().stringValue());
          dbChangeNotification.onDatabaseChange(rcd.getRowid().stringValue());
        }
      }

    }
  }

В событии  onDatabaseChangeNotification получим информацию об изменениях и ROWID измененной записи. Далее управление можно передать в ADS.

Часть II - ADS

Механизм доставки уведомления ADS реализован по модели - "Active Data Service with an Asynchronous Backend".

public class DepartmentManager

public class DepartmentManager extends ActiveCollectionModelDecorator implements IBackendListener
{
    private MyActiveDataModel _activeDataModel = new MyActiveDataModel();
    private CollectionModel _model = null;

    
    public CollectionModel getCollectionModel() {
       if (_model == null) {
         FacesContext ctx = FacesContext.getCurrentInstance();  
         ExpressionFactory ef = ctx.getApplication().getExpressionFactory();  
         ValueExpression ve = ef.createValueExpression(ctx.getELContext(), "#{DepartmentBackend}", DepartmentBackend.class);  
         DepartmentBackend context = (DepartmentBackend)ve.getValue(ctx.getELContext());  
          _model = new SortableModel(context.getDepartments());        
       }
       return _model;
     }

    public void onDeptartmentUpdate(Integer rowKey, String attribue, BigDecimal value)
    {
      
      if (rowKey != null) {
          System.out.println("changeEvent "+rowKey);  
          
      MyActiveDataModel asm = getMyActiveDataModel();
      
      // start the preparation for the ADS update
      asm.prepareDataChange();

      // create an ADS event, using an _internal_ util.....
      // this class is not part of the API
      ActiveDataUpdateEvent event = 
        ActiveDataEventUtil.buildActiveDataUpdateEvent(
        ActiveDataEntry.ChangeType.UPDATE, // type
        asm.getCurrentChangeCount(), // changeCount
        new Object[] {rowKey}, // rowKey
        null, //insertKey (null as we don't insert stuff;
        new String[] {attribue}, // attribue/property name that changes
        new Object[] {value}   // the payload for the above attribute
        );
      // deliver the new Event object to the ADS framework
      asm.notifyDataChange(event);
      }
    }
  
Все начинается с класса DepartmentManager, который расширяет - ActiveCollectionModelDecorator. В котором, основное что он делает - это вычисляет новую модель данных основанную на бине - DepartmentBackend. Сам тоже в свою очередь является зарегистрированным бином. Так же в нем создается событие ADS - ActiveDataUpdateEvent для отправки в framework.

public class DepartmentBackend

public class DepartmentBackend
{
  public void setListener(IBackendListener listener)
  {
    this.listener = listener;
  }

  public IBackendListener getListener()
  {
    return listener;
  }

  private IBackendListener listener;

  private final List<Department> departments = new ArrayList<Department>();

  public List<Department> getDepartments()
  {
    return departments;
  }

/**
 * Инициализировать список начальными данными
 * А также создать ждущий поток по отправки уведомления об изменении данных
 */
  @PostConstruct
  public void initData()
  {
    System.out.println("initData begin");
    am = (AppModuleImpl) ADFUtils.getApplicationModuleForDataControl("AppModuleDataControl");
    ViewObject vo = am.findViewObject("DepartSalaryViewObj1");
    vo.executeQuery();
    Row row = vo.first();
    Long i = 0L;
    while (row != null)
    {
      departments.add(new Department((String) row.getAttribute("DepartmentName"), i,
                                     (Integer) row.getAttribute("DepartmentId"),
                                     (BigDecimal) row.getAttribute("Salary")));
      i++;
      row = vo.next();
    }
    System.out.println("initData end");

    dataChanger = new Runnable()
      {
        @Override
        public void run()
        {
          synchronized (DepartmentBackend.this)
          {
            try
            {
              System.out.println("wait");
              DepartmentBackend.this.wait();
              System.out.println("end wait");              
       // Вычислить Salary и ID в списке для измененной записи по ROWID
              BigDecimal salary = DepartmentBackend.this.calcSalary();
              DepartmentBackend.this.changeData(id.intValue(), "salary", salary);
            }
            catch (InterruptedException e)
            {
              System.out.println(e);
            }
          }
          DepartmentBackend.this.isBusy = false;
        }
      };

    System.out.println("start");
    // start the process
    Thread newThread = new Thread(dataChanger);
    newThread.start();
  }

  /**
   * Начать обновление данных в списке.
   * Сюда приходит вызов из DCNDemoListener (onDatabaseChangeNotification)
   * @param rowid
   */
  public void startUpdateProcess(String rowid)
  {
    System.out.println("startUpdateProcess:" + rowid);
    this.rowid = rowid;
    if (!isBusy)
    {
      synchronized (this)
      {
        isBusy = true;
        System.out.println("notify");
        this.notify();
        // start the process
        Thread newThread = new Thread(dataChanger);
        newThread.start();
      }
    }
  }

DepartmentBackend - в нем формируется начальный список отделов (Departments) в initData, который вызывается после создания бина (@PostConstruct).И запускается ждущий поток, в ожидании события об измененной записи. После получения ROWID измененной записи из startUpdateProcess, запускаем ожидающий поток, узнаем ИД отдела и далее вычисляем для него новую сумму - calcSalary(), ее и отображаем в списке на странице для указанного отдела.

public class MyActiveDataModel

public class MyActiveDataModel extends BaseActiveDataModel {

    @Override
    public int getCurrentChangeCount()
    {
      return changeCounter.get();
    }

    /**
     * Does not much; simple increments the change counter...
     */
    public void prepareDataChange()
    {
      changeCounter.incrementAndGet();
    }

    /**
     * Delivers an <code>ActiveDataUpdateEvent</code> object to the ADS system.
     * 
     * @param event the ActiveDataUpdateEvent object
     */
    public void notifyDataChange(ActiveDataUpdateEvent event)
    {
      // delegate even to internal fireActiveDataUpdate() method
      fireActiveDataUpdate(event);
    }

Есть еще один класс - MyActiveDataModel, он используется в основном для хранения счетчиков изменений и вызова события уведомления - notifyDataChange.  

Конфигурация ADS - \.adf\META-INF\adf-config.xml

<ads:adf-activedata-config xmlns="http://xmlns.oracle.com/adf/activedata/config">
    <transport>long-polling</transport>
    <latency-threshold>10000</latency-threshold>
    <keep-alive-interval>10000</keep-alive-interval>
    <polling-interval>3000</polling-interval>
    <max-reconnect-attempt-time>1800000</max-reconnect-attempt-time>
    <reconnect-wait-time>10000</reconnect-wait-time>
  </ads:adf-activedata-config>

файл - \.adf\META-INF\services\adf-config.properties

http\://xmlns.oracle.com/adf/activedata/config=oracle.adfinternal.view.faces.activedata.ActiveDataConfiguration$ActiveDataConfigCallback

Для работы приложения используется прямой доступ к DataSource через JNDI, поэтому на WLS надо создать  DataSource с JNDI именем - jdbc/HRDS

Вид страницы со списком отделов и их суммарной  з.п.




После нажатия кнопки "Start watching" начнется наблюдение за изменениями в БД.
Для имитации изменений, можно запустить PL-SQL Developer  и нем внести зменения в таблицу - EMPLOYEES.


Изменив значение в поле Salary и выполнив commit, сразу увидим изменения в списке на странице в браузере, поле на мгновение будет подсвечено другим цветом, примерно так
    
(картинка из другого приложения).

Источники:

Database Change Notification
Using the Active Data Service with an Asynchronous Backend

Исходник



Комментариев нет:

Отправить комментарий