26 連続問合せ通知

この節では、以下のトピックについて説明します。

26.1 連続問合せ通知の概要

一般に、中間層データ・キャッシュは、バックエンド・データベース・サーバーの一部のデータを複製します。その目的は、データベースに対する冗長な問合せを回避することです。ただし、これが効率的なのは、データベース内でのデータの変更頻度が非常に低い場合に限られます。データベース内でデータが変更された際には、データ・キャッシュを更新するか、無効にする必要があります。11gリリース1以降、Oracle JDBCドライバは、Oracle Databaseの連続問合せ通知機能をサポートしています。この機能を使用すると、JDBCドライバから無効化イベントを受信することにより、多重化システムのデータ・キャッシュを可能なかぎり最新の状態に保つことができます。

JDBCドライバでは、SQL問合せをデータベースに登録して、次のイベントの発生時に通知を受け取ることができます。

  • 問合せに関連付けられたオブジェクトに対するDMLまたはDDL変更。

  • 結果セットに影響を与えるDMLまたはDDL変更。

通知は、DMLまたはDDLトランザクションのコミット時にパブリッシュされます(ローカル・トランザクションで行われた変更は、コミットされるまでイベントを生成しません)。

Oracle JDBCドライバの連続問合せ通知機能は、次のような流れで使用します。

  1. 登録: まず登録エントリを作成します。

  2. 問合せの関連付け: 登録エントリを作成したら、その登録エントリにSQL問合せを関連付けできます。これらの問合せは、登録エントリの一部となります。

  3. 通知: 表または結果セットに変更が生じると、通知が作成されます。Oracle Databaseは専用のネットワーク接続を使用してこれらの通知をJDBCドライバに伝え、JDBCドライバはそれらの通知をJavaイベントに変換します。

これらに加え、ユーザーにCHANGE NOTIFICATION権限を付与することも必要です。たとえば、HRというユーザー名を使用してデータベースに接続する場合は、データベース内で次のコマンドを実行する必要があります。

grant change notification to HR;

26.2 登録エントリの作成

登録エントリの作成は、1回かぎりのプロセスであり、現在使用中のトランザクションの外部で実行します。サーバーに登録エントリを作成するためのAPIは、専用のトランザクション内で実行され、即時にコミットされます。登録エントリの作成にはJDBC接続が必要ですが、その接続に登録エントリがアタッチされるわけではありません。登録エントリの作成後に接続をクローズしても、登録エントリ自体は有効な状態を維持します。Oracle RAC環境においては、登録エントリはすべてのノードに存在する永続的エンティティとなります。登録エントリはデータベース内に存在しています。そのため、ノードがダウンした場合でも、登録エントリは存在し続け、表の変更時には通知を受けます。

登録エントリの作成方法は、次の2種類に分けられます。

  • JDBCスタイルの登録: JDBCドライバを使用し、サーバー上に登録エントリを作成します。JDBCドライバは、サーバーからの通知を(専用チャネルを介して)リスニングする新しいスレッドを起動し、それらの通知メッセージをJavaイベントに変換します。その後、ドライバは作成したエントリに登録されているすべてのリスナーに通知を送ります。

  • PL/SQLスタイルの登録: 通知の処理にPL/SQLのストアド・プロシージャを使用する場合は、PL/SQLスタイルの登録エントリを作成します。JDBCスタイルの登録の場合と同様に、登録エントリに文(問合せ)をアタッチするにはJDBCドライバを使用します。ただし、通知はPL/SQLのストアド・プロシージャによって処理されるため、JDBCドライバはサーバーからの通知を受け取りません。

ノート:

この方法は、PHPなどマルチスレッド以外の言語の場合にのみ、便利です。

既存の登録エントリから特定の(1つの)オブジェクト(表)を削除する方法はありません。次善策としては、そのオブジェクトを含まない登録エントリを新規作成するか、そのオブジェクトに関連付けられているイベントを無視します。

JDBCスタイルの登録エントリの作成には、oracle.jdbc.OracleConnectionインタフェースのregisterDatabaseChangeNotificationメソッドを使用できます。このメソッドでは、optionsパラメータにより、特定の登録オプションを設定できます。次の項の「連続問合せ通知登録オプション」表には、設定できる登録オプションの一部を示します。これらのオプションは、java.util.Propertiesオブジェクトを使用して設定します。これらのオプションは、oracle.jdbc.OracleConnectionインタフェースで定義されています。これらの登録オプションは、JDBCドライバによって作成される通知イベントに直接影響を与えます。連続問合せ通知機能の使用方法は、(この章の終わりの)例に示します。

registerDatabaseChangeNotificationメソッドは、指定されたオプションを反映した、新しいデータベース変更登録エントリをデータベース・サーバー内に作成します。このメソッドはDatabaseChangeRegistrationオブジェクトを返し、そのオブジェクトを使用して登録エントリに文が関連付けられます。このメソッドはさらに、通知を送信するためにデータベースによって使用されるリスナー・ソケットをオープンします。

ノート:

別の登録エントリによって作成されたリスナー・ソケットがすでに存在している場合は、新しいデータベース変更登録エントリもそのソケットを使用します。

26.2.1 連続問合せ通知登録オプション

次の表では、連続問合せ通知登録オプションを示します。

表26-1 連続問合せ通知登録オプション

オプション 説明

DCN_IGNORE_DELETEOP

trueに設定された場合、DELETE操作が実行されてもデータベース変更イベントは生成されません。

DCN_IGNORE_INSERTOP

trueに設定された場合、INSERT操作が実行されてもデータベース変更イベントは生成されません。

DCN_IGNORE_UPDATEOP

trueに設定された場合、UPDATE操作が実行されてもデータベース変更イベントは生成されません。

DCN_NOTIFY_CHANGELAG

クライアントにトランザクションいくつ分までの遅延を許可するかを指定します。

ノート: このオプションを0以外の値に設定した場合、DCN_NOTIFY_ROWIDSオプションをtrueに設定しても、ROWIDレベルの粒度の情報は一連のイベントで利用できなくなります。

DCN_NOTIFY_ROWIDS

データベース変更イベントに、操作タイプやROWIDなど、行レベルの詳細情報を含めます。

DCN_QUERY_CHANGE_NOTIFICATION

オブジェクト変更通知のかわりに、問合せ変更通知をアクティブにします。

ノート: このオプションは、11.0データベースに対して実行する場合にのみ利用できます。

NTF_LOCAL_HOST

サーバーからの通知を受信するコンピュータのIPアドレスを指定します。

NTF_LOCAL_TCP_PORT

リスナー・ソケット用としてドライバに使用させるTCPポートを指定します。

NTF_QOS_PURGE_ON_NTFN

最初の通知イベント発生時に登録エントリを消去するかどうかを指定します。

NTF_QOS_RELIABLE

通知を永続的に維持するかどうかを指定します(永続的に維持する場合、パフォーマンス・コストが高くなります)。

NTF_TIMEOUT

このオプションで秒数を指定すると、登録エントリは指定した秒数の経過後にデータベースによって自動的に消去されます。

すでに登録エントリが存在している場合は、getDatabaseChangeRegistrationメソッドを使用して、既存の登録エントリを新しいDatabaseChangeRegistrationオブジェクトにマップすることもできます。このメソッドは、PL/SQLを使用した登録エントリをすでに作成してあり、それに文を関連付ける場合に特に便利です。

26.3 登録エントリへの問合せの関連付け

登録エントリの作成、または既存の登録エントリに対するマップが終わったら、登録エントリに問合せを関連付けることができます。登録エントリの作成と同様、登録エントリと問合せの関連付けは1回かぎりのプロセスであり、現在使用中の登録の外部で実行します。問合せの関連付けは、ローカル・トランザクションがロールバックされても実行されます。

登録エントリと問合せの関連付けは、OracleStatementクラスで定義されているsetDatabaseChangeRegistrationメソッドを使用して実行します。このメソッドは、DatabaseChangeRegistrationオブジェクトをパラメータとして取ります。次のコードは、登録エントリに問合せを関連付ける方法の例を示しています。

...
// conn is an OracleConnection object.
// prop is a Properties object containing the registration options.
DatabaseChangeRegistration dcr = conn.registerDatabaseChangeNotifictaion(prop);
...
Statement stmt = conn.createStatement();
// associating the query with the registration
((OracleStatement)stmt).setDatabaseChangeRegistration(dcr);
// any query that will be executed with the 'stmt' object will be associated with
// the registration 'dcr' until 'stmt' is closed or
// '((OracleStatement)stmt).setDatabaseChangeRegistration(null);' is executed.
...

26.4 データベース変更イベントの通知

連続問合せ通知を受け取るには、登録エントリにリスナーをアタッチします。データベース変更イベントが発生すると、データベース・サーバーはJDBCドライバに通知します。ドライバは、新しいJavaイベントを作成し、通知対象の登録エントリを特定した後、その登録エントリにアタッチされているリスナーに通知を送ります。イベントには、変更されたデータベース・オブジェクトのオブジェクトIDと、変更の原因となった操作のタイプが含まれます。登録オプションによっては、行レベルの詳細情報も含まれます。その後、そのイベントを使用して、データ・キャッシュに関する判断を行うリスナー・コードを実行することもできます。

ノート:

リスナー・コードは、JDBC通知メカニズムの処理速度を低下させないように作成する必要があります。データベースへの問合せによってデータ・キャッシュをリフレッシュするなど、リスナー・コードの処理時間が長い場合は、専用のスレッド内で実行する必要があります。

登録エントリにリスナーをアタッチするには、addListenerメソッドを使用します。次のコードは、登録エントリにリスナーをアタッチする方法の例を示しています。

...
// conn is an OracleConnection object.
// prop is a Properties object containing the registration options.
DatabaseChangeRegistration dcr = conn.registerDatabaseChangeNotifictaion(prop);
...
// Attach the listener to the registration.
// Note: DCNListener is a custom listener and not a predefined or standard 
// lsiener
DCNListener list = new DCNListener();
dcr.addListener(list);
...

26.5 登録エントリの削除

登録エントリをサーバーから削除し、ドライバ内のリソースをリリースするには、登録エントリを明示的に解除する必要があります。登録の解除には、作成時に使用したのとは別の接続を使用できます。登録の解除には、oracle.jdbc.OracleConnectionで定義されているunregisterDatabaseChangeNotificationメソッドを使用します。

このメソッドには、パラメータとしてDatabaseChangeRegistrationオブジェクトを渡す必要があります。このメソッドは、サーバーおよびドライバから登録を削除し、リスナー・ソケットをクローズします。

PL/SQLを使用するなどして、登録エントリをJDBCの外部で作成した場合は、DatabaseChangeRegistrationオブジェクトのかわりに登録IDを渡す必要があります。このメソッドは、サーバーから登録を削除しますが、ドライバ内のリソースは解放しません。

連続問合せ通知機能の使用方法は、例26-1に示しています。この例では、ユーザーHRがデータベースに接続しています。そのため、データベース内でこのユーザーに次の権限を付与する必要があります。

grant change notification to HR;

このコードは、Oracle Database 10gリリース2(10.2)でも動作します。このコードでは、表登録を使用しています。この場合、SELECT問合せを登録しても、登録されるのは問合せ自体ではなく、それに関与する表の名前です。つまり、表内の1行を選択した場合、それとは別の行が更新されていて、問合せの結果に影響がなくても、通知を受けることになります。

このコード例では、登録エントリをクローズせずにオープン状態のままにしておくと、連続問合せ通知スレッドは動作を継続するようになっています。したがって、HR.DEPARTMENTS表を変更するDML問合せを(SQL*Plusなどから)起動およびコミットすると、Javaプログラムによって通知が出力されます。

例26-1 連続問合せ通知

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import oracle.jdbc.OracleConnection;
import oracle.jdbc.OracleDriver;
import oracle.jdbc.OracleStatement;
import oracle.jdbc.dcn.DatabaseChangeEvent;
import oracle.jdbc.dcn.DatabaseChangeListener;
import oracle.jdbc.dcn.DatabaseChangeRegistration;
 
public class DBChangeNotification
{
  static final String USERNAME= "HR";
  static final String PASSWORD= "hr";
  static String URL;
  
  public static void main(String[] argv)
  {
    if(argv.length < 1)
    {
      System.out.println("Error: You need to provide the URL in the first argument.");
      System.out.println("  For example: > java -classpath .:ojdbc6.jar DBChangeNotification \"jdbc:oracle:thin:
@(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=yourhost.yourdomain.com)(PORT=5221))(CONNECT_DATA=
(SERVICE_NAME=orcl)))\"");
 
      System.exit(1);
    }
    URL = argv[0];
    DBChangeNotification demo = new DBChangeNotification();
    try
    {
      demo.run();
    }
    catch(SQLException mainSQLException )
    {
      mainSQLException.printStackTrace();
    }
  }
 
  void run() throws SQLException
  {
    OracleConnection conn = connect();
      
    // first step: create a registration on the server:
    Properties prop = new Properties();
    
    // if connected through the VPN, you need to provide the TCP address of the client.
    // For example:
    // prop.setProperty(OracleConnection.NTF_LOCAL_HOST,"14.14.13.12");
 
    // 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).
    DatabaseChangeRegistration dcr = conn.registerDatabaseChangeNotification(prop);
 
    try
    {
      // add the listenerr:
      DCNDemoListener list = new DCNDemoListener(this);
      dcr.addListener(list);
       
      // 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 * from dept where deptno='45'");
      while (rs.next())
      {}
      String[] tableNames = dcr.getTables();
      for(int i=0;i<tableNames.length;i++)
        System.out.println(tableNames[i]+" is part of the registration.");
      rs.close();
      stmt.close();
    }
    catch(SQLException ex)
    {
      // if an exception occurs, we need to close the registration in order
      // to interrupt the thread otherwise it will be hanging around.
      if(conn != null)
        conn.unregisterDatabaseChangeNotification(dcr);
      throw ex;
    }
    finally
    {
      try
      {
        // Note that we close the connection!
        conn.close();
      }
      catch(Exception innerex){ innerex.printStackTrace(); }
    }
    
    synchronized( this ) 
    {
      // The following code modifies the dept table and commits:
      try
      {
        OracleConnection conn2 = connect();
        conn2.setAutoCommit(false);
        Statement stmt2 = conn2.createStatement();
        stmt2.executeUpdate("insert into dept (deptno,dname) values ('45','cool dept')",
Statement.RETURN_GENERATED_KEYS);
        ResultSet autoGeneratedKey = stmt2.getGeneratedKeys();
        if(autoGeneratedKey.next())
          System.out.println("inserted one row with ROWID="+autoGeneratedKey.getString(1));      
        stmt2.executeUpdate("insert into dept (deptno,dname) values ('50','fun dept')",
Statement.RETURN_GENERATED_KEYS);
        autoGeneratedKey = stmt2.getGeneratedKeys();
        if(autoGeneratedKey.next())
          System.out.println("inserted one row with ROWID="+autoGeneratedKey.getString(1));
        stmt2.close();
        conn2.commit();
        conn2.close();
      }
      catch(SQLException ex) { ex.printStackTrace(); }
 
      // wait until we get the event
      try{ this.wait();} catch( InterruptedException ie ) {}
    }
    
    // At the end: close the registration (comment out these 3 lines in order
    // to leave the registration open).
    OracleConnection conn3 = connect();
    conn3.unregisterDatabaseChangeNotification(dcr);
    conn3.close();
  }
  
  /**
   * Creates a connection the database.
   */
  OracleConnection connect() throws SQLException
  {
    OracleDriver dr = new OracleDriver();
    Properties prop = new Properties();
    prop.setProperty("user",DBChangeNotification.USERNAME);
    prop.setProperty("password",DBChangeNotification.PASSWORD);
    return (OracleConnection)dr.connect(DBChangeNotification.URL,prop);
  }
}
/**
 * DCN listener: it prints out the event details in stdout.
 */
class DCNDemoListener implements DatabaseChangeListener
{
  DBChangeNotification demo;
  DCNDemoListener(DBChangeNotification dem)
  {
    demo = dem;
  }
  public void onDatabaseChangeNotification(DatabaseChangeEvent e)
  {
    Thread t = Thread.currentThread();
    System.out.println("DCNDemoListener: got an event ("+this+" running on thread "+t+")");
    System.out.println(e.toString());
    synchronized( demo ){ demo.notify();}
  }
}