Триггеры в TimesTen (XLA application)

Иногда, заказчики задают вопрос относительно поддержки триггеров в TimesTen.
TimesTen поддерживает PL/SQL (процедуры, функции, пакеты и др.), но поддержка триггеров отсутствует, т.к. триггеры пагубно влияют на производительность. Но что делать, если необходимо реализовать триггерную логику?

Ответ - написать XLA приложение. Написать можно на С или Java, кому что ближе.
Ниже я буду описывать пример с использованием java :).

В документации (Java Developer's Guide) сказано:

"You can use the TimesTen JMS/XLA API (JMS/XLA) to monitor TimesTen for
changes to specified tables in a local data store and receive real-time notification of these changes. One of the purposes of JMS/XLA is to provide a high-performance, asynchronous alternative to triggers."

Т.е. вы можете написать java приложение, которое может использовать JMS/XLA API для получения сообщений (в асинхронном режиме) об изменениях в TimesTen. JMS/XLA использует JMS publish-subscribe interface для доступа к XLA изменениям. Подробнее про JMS можно почитать здесь (http://download.oracle.com/javaee/1.3/jms/tutorial).

Далее попробуем создать такое приложение.

Первоначально, создадим объекты в TimesTen.
[oracle@tt1 xla]$ ttisql dbxla

Copyright (c) 1996-2010, Oracle.  All rights reserved.
Type ? or "help" for help, type "exit" to quit ttIsql.

connect "DSN=dbxla";
Connection successful: DSN=dbxla;UID=oracle;DataStore=/u01/app/oracle/datastore/dbxla;DatabaseCharacterSet=WE8MSWIN1252;ConnectionCharacterSet=US7ASCII;DRIVER=/u01/app/oracle/product/11.2.1/TimesTen/tt1/lib/libtten.so;PermSize=32;TempSize=50;TypeMode=0;PLSQL=0;CacheGridEnable=0;
(Default setting AutoCommit=1)
Command> CREATE USER oratt IDENTIFIED BY oracle;

User created.

Command> grant create session, create table, XLA to oratt;
Command> connect "DSN=dbxla;UID=oratt;PWD=oracle;";
Connection successful: DSN=dbxla;UID=oratt;DataStore=/u01/app/oracle/datastore/dbxla;DatabaseCharacterSet=WE8MSWIN1252;ConnectionCharacterSet=US7ASCII;DRIVER=/u01/app/oracle/product/11.2.1/TimesTen/tt1/lib/libtten.so;PermSize=32;TempSize=50;TypeMode=0;PLSQL=0;CacheGridEnable=0;
(Default setting AutoCommit=1)
con1: Command> create table xlatest ( id   NUMBER NOT NULL PRIMARY KEY,
             >                        name VARCHAR2(100) );
con1: Command>

Теперь, создадим закладку (bookmark). XLA закладки используются для отметки позиции чтения в журналах транзакций. Данную закладку булем использовать для отслеживания изменений в таблице xlatest.
con1: Command> call ttXlaBookmarkCreate('bookmark');
con1: Command>

Далее, определим, изменения какой таблицы будем наблюдать. Для этого вызовем процедуру ttXlaSubscribe. В данном случае будем наблюдать за изменениями с таблицей xlatest с использованием закладки bookmark.
con1: Command> call ttXlaSubscribe('xlatest','bookmark');
con1: Command>

Далее, перейдем к настройке приложения.
Для соединения с XLA необходимо установить соединение с JMS Topic, который связан с опреденной базой данных TimesTen. Кофигурационный файл JMS/XLA обеспечивает привязку между именем топика и базой данных. По умолчанию, приложение ищет данный файл, названный jmsxla.xml, в текущей директории, но при желании можно определить другое имя и местоположение данного файла (см. документацию).

В данном случае я использую следующий файл jmsxla.xml:
<xlaconfig>
  <topics>

    <!-- topic for Xla demo -->
    <topic name="xlademo"
           connectionString="DSN=dbxla"
           xlaPrefetch="100"
    />

  </topics>
</xlaconfig>

Как видно, я связал имя топика xlademo с базой данных dbxla.

Теперь приступим, непосредственно, к написанию java Приложения.
Первоначально, инициализируем контекст.
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.timesten.dataserver.jmsxla.SimpleInitialContextFactory");
InitialContext ic = new InitialContext(env);

Далее, используем JMS connection factory для соединения с XLA. После чего, вызываем метод start() у соединения для активации отправки сообщений. После этого, используя данное соединение создаем сессию.
private javax.jms.TopicConnection connection; /** JMS connection */
private TopicSession session;  /** JMS session */
...
TopicConnectionFactory connectionFactory = (TopicConnectionFactory)ic.lookup("TopicConnectionFactory");
connection = connectionFactory.createTopicConnection();
...
// get Session
session = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
...
Также при создании сессии необходимо указать транзакционность сессии и тип модели подтверждения (acknowledgment modes).
JMS/XLA поддерживается три модели (AUTO_ACKNOWLEDGE,DUPS_OK_ACKNOWLEDGE, CLIENT_ACKNOWLEDGE), подробнее про модели можно почитать в документации. В примере я использую первую модель и транзакционность сессии устанавливаю в значение false.

Далее, необходимо определиться с режимом получения сообщений. Возможны два варианта: синхронный и асинхронный.

В синхронном варианте, сообщения обрабатываются последовательно (одно за другим). Это означает, что пока сообщение не обработано, другое ожидает.
Для синхронного варинта - вызываем метод start() у соединения, для активации отправки сообщений, создаем Topic, затем создаем подписчика и получаем сообщения с помощью методов receive() и receiveNoWait().

connection.start();
Topic topic = session.createTopic(topicName);     
TopicSubscriber subscriber = session.createDurableSubscriber(topic, bookmark);                 
..
MapMessage message = (MapMessage)subscriber.receive(); 
...

В асинхронном режиме необходимо создать листенер и в нем обрабатывать сообщения.

MyListener myListener = new MyListener(outStream);

Topic xlaTopic = session.createTopic(topic);
TopicSubscriber subscriber = session.createDurableSubscriber(xlaTopic, bookmark);
..
subscriber.setMessageListener(myListener);
connection.start();
..

Ниже представлен пример класса, реализующего синхронный режим (файл DemoXLA.java).
import java.util.Enumeration;
import java.util.Hashtable;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicConnection;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class DemoXLA {

    private TopicConnectionFactory connectionFactory;
    private TopicConnection connection;
    private TopicSession session;
    private Topic topic;
    private TopicSubscriber subscriber;

    public DemoXLA( String cf, 
                    String topicName, 
                    String selector) throws JMSException, NamingException {
        String key;
        Context messaging = getInitialContext(); // getting the context

 connectionFactory = (TopicConnectionFactory)messaging.lookup(cf);        
 connection = connectionFactory.createTopicConnection();
 connection.start();                        
 session = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);                
        
 topic = session.createTopic(topicName);     
        subscriber = session.createDurableSubscriber(topic, selector);                 
        int i=0;
        while (i<10) {
            MapMessage message = (MapMessage)subscriber.receive();                        
            Enumeration e = message.getMapNames();

            while (e.hasMoreElements()) {
                key = (String)e.nextElement();
                System.out.println("[ " + key + " = " + message.getObject(key) + " ]");
            }
            System.out.println("----------------------------------------");                        
        }
        
        session.unsubscribe(selector);
        subscriber.close();
        session.close();
        connection.stop();
    }

    private Context getInitialContext() throws NamingException {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.timesten.dataserver.jmsxla.SimpleInitialContextFactory");        
        InitialContext initialContext = new InitialContext(env);
        return initialContext;
    }

    public static void main(String[] args) throws JMSException, NamingException {
        DemoXLA demo = new DemoXLA("TopicConnectionFactory", "Level2Demo", "bookmark");
    }
}
Ниже представлен пример классов, реализующих асинхронный режим (MyListener.java, DemoXLA2.java). MyListener.java
import java.util.Enumeration;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageListener;


public class MyListener implements MessageListener   {
    public MyListener() {}

    public void onMessage(Message message) {
        MapMessage mp = (MapMessage)message;
        Enumeration e;
        try {
            e = mp.getMapNames();
        } catch (JMSException s) {
            e = null;
            System.out.println("error 1");
        }
        while (e.hasMoreElements()) {
            String key = (String)e.nextElement();
            try {
                System.out.println("[ " + key + " = " + mp.getObject(key) + " ]");
            } catch (JMSException f) {
                System.out.println("error 2");
            }
        }
        System.out.println("----------------------------------------");
    }
}
DemoXLA2.java
import java.util.Hashtable;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class DemoXLA2 {

    private javax.jms.TopicConnectionFactory connectionFactory;
    private javax.jms.TopicConnection connection;
    private TopicSession session;
    private Topic topic;
    private TopicSubscriber subscriber;

    public DemoXLA2( String cf,
                    String topicName,
                    String selector) throws JMSException, NamingException, InterruptedException {

        Context messaging = getInitialContext();
        Object connectionFactoryObject = messaging.lookup(cf);
        connectionFactory = (TopicConnectionFactory)connectionFactoryObject;
        connection = connectionFactory.createTopicConnection();

        MyListener myListener = new MyListener();
        session = connection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
        topic = session.createTopic(topicName);
        subscriber = session.createDurableSubscriber(topic, selector); 
 
subscriber.setMessageListener(myListener);
        connection.start();
        Thread.sleep(60000);

        session.unsubscribe(selector);
        subscriber.close();
        session.close();
        connection.stop();
    }

    private Context getInitialContext() throws NamingException {
        Hashtable env = new Hashtable();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.timesten.dataserver.jmsxla.SimpleInitialContextFactory");
        InitialContext initialContext = new InitialContext(env);
        return initialContext;
    }


    public static void main(String[] args) throws JMSException,
                                                  NamingException,
                                                  InterruptedException {
        DemoXLA2 demo = new DemoXLA2("TopicConnectionFactory", "xlademo", "bookmark");

    }

}
Далее, запускаем любой из примеров и попытаемся внести, изменить и удалить данные из таблицы xlatest.
Command> insert into xlatest values (2, 'w');
1 row inserted.
Command> update xlatest set name = 'test' where id=2;
1 row updated.
Command> delete from xlatest;
1 row deleted.
Command>
Соответственно, в приложении получаем:
[ __TYPE = 10 ]
[ __COMMIT = true ]
[ __FIRST = true ]
[ __NULLS =  ]
[ __TBLNAME = XLATEST ]
[ __TBLOWNER = ORATT ]
[ __mver = 5621355056449191939 ]
[ __mtyp = null ]
[ ID = 2 ]
[ NAME = w ]
----------------------------------------
[ __TYPE = 11 ]
[ __COMMIT = true ]
[ __FIRST = true ]
[ __UPDCOLS = NAME ]
[ __NULLS =  ]
[ __TBLNAME = XLATEST ]
[ __TBLOWNER = ORATT ]
[ __mver = 5621355056449191942 ]
[ __mtyp = null ]
[ _ID = 2 ]
[ ID = 2 ]
[ _NAME = w ]
[ NAME = test ]
----------------------------------------
[ __TYPE = 12 ]
[ __COMMIT = true ]
[ __FIRST = true ]
[ __NULLS =  ]
[ __TBLNAME = XLATEST ]
[ __TBLOWNER = ORATT ]
[ __mver = 5621355056449191945 ]
[ __mtyp = D ]
[ ID = 2 ]
[ NAME = test ]
----------------------------------------

Как видно, мы получили сообщения о прошедших с таблицей xlatest операциях.
Сообщения имеют следующий формат:
Системные атрибуты начинаются с двойного подчеркивания, например:
__TYPE - тип операции (Insert (10), Update(11), Delete(12)) также возможны другие типы (см. документацию). Для определения типа операции присутствуют константы.
__COMMIT - сигнализирует об завершении транзакции (если true).
__FIRST - сигнализирует о первой операции в транзакции (если true).
__TBLNAME - имя таблицы
__TBLOWNER - владелец таблицы
__NULLS - сигнализирует об атрибутах, в которых содержется значение null
__mver и __mtyp - системные атрибуты.
и т.д. (см. документацию)
Атрибуты без подчеркивания - колонки таблиц, имеющие определенные значения, например:
[ ID = 2 ]
[ NAME = test ]
Атрибуты начинающиеся на одно подчеркивание - старые значения полей (появляются при операции Update), например:
[ _ID = 2 ]
[ ID = 2 ]
[ _NAME = w ]
[ NAME = test ]

Итог

Следовательно, имея достаточно поверхностные знания по java, можно написать XLA приложение, которое может обрабатывать различные сообщения, полученнные из TimesTen. Кроме того, XLA приложение работает в асинхронном режиме, что практически не влияет на производительность Oracle TimesTen.

Доступ ко всем данным из TimesTen (Passthrough=1)

Недавно ко мне обратился заказчик с задачей:
существуют "огромные партиционированные таблицы (терабайты данных), к части из которых идет частое обращение. Возможно ли чтобы из ТТ были доступны определенные партиции, а остальные брались из оракла по тому же подключению ТТ".

Попробуем решить данную задачу.

Предположим у насть есть партицированная таблица в Oracle Database.

[oracle@tt1 cache_article]$ sqlplus oratt/oracle@orcl

SQL*Plus: Release 11.1.0.7.0 - Production on Tue May 24 09:22:15 2011

Copyright (c) 1982, 2008, Oracle.  All rights reserved.

Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> create table accounts (
  2  Id number,
  3  Name varchar2 (50),
  4  Surname varchar2(50),
  5  constraint accounts_pk primary key (id)
  6      using index (create unique index account_pk_idx on accounts(id))
  7  )
  8  partition by range (id) (
  9  partition part1 values less than (1000),
 10  partition part2 values less than (2000),
 11  partition part3 values less than (3000)
 12  );

Table created.

SQL> begin
2  for i in 1 .. 100 loop
3    insert into accounts values (i, 'Pit'||i, 'Jounes'||i);
4  end loop;
5 end;
6/
  
PL/SQL procedure successfully completed.

SQL> begin
2  for i in 1001 .. 1100 loop
3    insert into accounts values (i, 'Pit'||i, 'Jounes'||i);
4  end loop;
5 end;
6/
  
PL/SQL procedure successfully completed.

SQL> begin
2  for i in 2001 .. 2100 loop
3    insert into accounts values (i, 'Pit'||i, 'Jounes'||i);
4  end loop;
5 end;
6/
  
PL/SQL procedure successfully completed.

SQL> select count(*) from accounts;

  COUNT(*)
----------
       300

SQL> grant select on accounts to cacheadmin;

Grant succeeded.


Предположим, что частое обращение происходит к первой партиции, т.е. будем кэшировать первую партицию (ограничим ее условием where в определении кэш группы).

Command> CREATE READONLY CACHE GROUP read_cg
       >   AUTOREFRESH INTERVAL 5 SECONDS
       > FROM oratt.accounts (   Id number not null primary key,
       >                       Name varchar2 (50),
       >                    Surname varchar2(50)
       > ) where id <= 999;
Command> load cache group read_cg commit every 265 rows;
100 cache instances affected.


Но как получить данные из других партиций?
Для этого будем использовать параметр Passthrough.
Если данный параметр имеет значение 1, то каждый запрос, выполненный к несуществующим обектам в TimesTen будет перенаправлен в Oracle Database для исполнения.

Следовательно, создаем представление в Oracle Database, которое будет содержать данные из остальных партиций.

SQL> create view accounts_all as select * from accounts where id > 999;

View created.

SQL> grant select on accounts_all to cacheadmin;

Grant succeeded.


После чего можем иметь доступ ко всей информации через одно подключение (но, к сожалению, через разные таблицы).

Command> connect "DSN=db_cache1;UID=oratt;PWD=oracle;";
Connection successful: DSN=db_cache1;UID=oratt;DataStore=/u01/app/oracle/datastore/db_cache1;DatabaseCharacterSet=WE8MSWIN1252;ConnectionCharacterSet=US7ASCII;DRIVER=/u01/app/oracle/product/11.2.1/TimesTen/tt1/lib/libtten.so;PermSize=32;TempSize=50;TypeMode=0;PLSQL_TIMEOUT=1000;CacheGridEnable=0;OracleNetServiceName=ORCL;
(Default setting AutoCommit=1)
Command> select count(*) from accounts;
< 100 >
1 row found.
Command> select count(*) from accounts_all;
 2206: Table ORATT.ACCOUNTS_ALL not found
The command failed.
Command> set autocommit 0;
Command> call ttOptSetFlag('PassThrough', 1);
Command> select count(*) from accounts_all;
< 200 >
1 row found.
Command>
Command> set showplan 1;
Command> select count(*) from accounts;

Query Optimizer Plan:

  STEP:                1
  LEVEL:               1
  OPERATION:           TblLkSerialScan
  TBLNAME:             ACCOUNTS
  IXNAME:              
  INDEXED CONDITION:   
  NOT INDEXED:         

< 100 >
1 row found.
Command>  select count(*) from accounts_all;

Query Optimizer Plan:

  STEP:                1
  LEVEL:               1
  OPERATION:           Oracle PassThrough
  TBLNAME:             
  IXNAME:              
  INDEXED CONDITION:   
  NOT INDEXED:         

< 200 >
1 row found.
Command>


Следовательно, получили доступ ко всей таблице Accounts через одно подключение в Oracle TimesTen.