Database Change Notification – odbieranie zdarzeń z bazie danych Oracle

Dzisiaj będzie krótko, bo w związku ze zmianą mojego Centrum Dowodzenia (czyt. mieszkania) mam ostatnio trochę mało czasu i dużo gratów na głowie :) Będzie o tym, w jaki sposób w aplikacji napisanej w Javie odbierać zdarzenia z bazy danych Oracle. Po co to robić? Ano czasami trzeba, np. gdy duża część logiki biznesowej siedzi już w procedurach na bazie i nie ma szybkiego sposobu, żeby się jej pozbyć, albo mamy kilka aplikacji korzystających z tej samej bazy i nie mamy innego sposobu niż baza na zapewnienie komunikacji między nimi. W każdym bądź razie kiedyś musiałem takiego potworka zrobić i pomyślałem, że się tym podzielę.

Muszę też zaznaczyć, że jeśli tworzysz aplikację od nowa, bądź masz nieco możliwość wyboru rozwiązania, nie czytaj dalej. Użyj JMS, wywołuj zdarzenia po RMI, zrób cokolwiek byle nie to. Potraktuj bazę danych jako składnicę danych i nic więcej – ułatwisz życie sobie i innym, którzy mają utrzymywać później Twój kod. Zapytasz dlaczego? Po pierwsze nie ma jednego uniwersalnego sposobu na otrzymywanie zdarzeń z bazy (no chyba że w grę wchodzi cykliczne odpytywanie) i jest on zależny od danego dostawcy bazy danych. Po drugie, nie wszystkie bazy danych taki mechanizm mają. Po trzecie, może okazać się, że implementacja tego mechanizmu nie jest doskonała i napotkamy szereg problemów przy jej wdrożeniu. Jeśli już naprawdę potrzebujesz takiego rozwiązania, chcę Ci zaprezentować mechanizm Database Change Notification w bazie danych Oracle.

Wymogiem do działania DCN jest minimalna wersja sterownika 11.x.x.x, baza danych Oracle 10g i Java 5. DCN może działać na dwa sposoby:

  • za pomocą procedury składowanej PL/SQL
  • przy pomocy wywołania zwrotnego OCI

Pierwszy sposób został dość dobrze opisany na stronie Oracle’a. Na temat drugiego informacji jest już sporo mniej, choć dla mnie to on jest o wiele ciekawszy. Pierwszy wymaga pisania jakiś procedur w PL/SQL’u, drugi można w całości zrealizować w Javie. Wiesz więc już, którym się zajmiemy.

Pomimo tego, że mowa jest o wywołaniu zwrotnym OCI, mechanizm działa poprzez sterownik THIN – taki też zalecam użyć. Pierwsze co należy zrobić, aby zarejestrować obsługę zdarzeń, to dobrać się do obiektu klasy oracle.jdbc.driver.OracleConnection. Zakładam, że masz jakieś pojęcie o JDBC i umiesz ustanowić połączenie z bazą, skutkiem czego będziesz posiadał obiekt klasy java.sql.Connection. Jak ten ostatni zamienić na OracleConnection? To zależy, w jakim środowisku uruchamiasz aplikację, czy połączenie odbywa się poprzez źródła danych, czy masz skonfigurowaną pulę połączeń, itp. W każdym bądź razie zawsze jakoś się da. Możesz na początku spróbować zrzutować obiekt Connection na OracleConnection. Jeśli nie pójdzie, spróbuj tego:

public OracleConnection unwrapOracleConnection(java.sql.Connection conn) {
  return conn.getMetaData().getConnection();
}

Jeśli to też nie pójdzie, to masz problem :) W zależności od serwera aplikacji, którego używasz, prawdopodobnie będziesz dostawał inną implementację interfejsu java.sql.Connection, a co najlepsze, zapewne nie będziesz posiadał kodu źródłowego ani nawet javadoc’a tej implementacji. Poniżej przedstawię Ci sposób, jak wybrać OracleConnection w aplikacji osadzonej na serwerze Websphere Application Server 6.1 i GlassFish V3, bo na nich miałem przyjemność to robić.

public OracleConnection unwrapWebSphereOracleConnection(java.sql.Connection connection) {
  OracleConnection unwrappedConnection = null;

  try {
    Class<?> connectionWrapperClass = connection.getClass().getClassLoader().loadClass("com.ibm.ws.rsadapter.jdbc.WSJdbcConnection");
    if (connectionWrapperClass.isInstance(connection)) {
      Method method = Class.forName("com.ibm.ws.rsadapter.jdbc.WSJdbcUtil").getDeclaredMethod("getNativeConnection", connectionWrapperClass);
      unwrappedConnection = (OracleConnection) method.invoke(null, connection);
    }
  } catch (Exception e) {
    LOG.error("cannot unwrap connection for websphere", e);
  }

  return unwrappedConnection;
}

public OracleConnection unwrapGlassFishOracleConnection(java.sql.Connection connection) {
  OracleConnection unwrappedConnection = null;

  try {
    Class<?> connectionWrapperClass = connection.getClass().getClassLoader().loadClass("com.sun.gjc.spi.base.ConnectionHolder");
    if (connectionWrapperClass.isInstance(connection) ) {
      Method unwrapMethod = connectionWrapperClass.getDeclaredMethod("getConnection");
      unwrappedConnection = (OracleConnection) unwrapMethod.invoke(connection);
    }
  } catch (Exception e) {
    LOG.error("cannot unwrap connection for glassfish", e);
  }
	 	
  return unwrappedConnection;
}

Uciesze się, jeśli pomogłem. Jeśli nie, a znajdziesz lepszy sposób, proszę zamieść go w komentarzu.
Załóżmy, że udało Ci się już wybrać obiekt klasy oracle.jdbc.driver.OracleConnection. Przejdźmy więc do sedna sprawy.

public void registerNotification(OracleConnection connection, String serverIp) throws SQLException {
  Properties prop = new Properties();
  prop.setProperty(OracleConnection.DCN_NOTIFY_ROWIDS, "true");
  prop.setProperty(OracleConnection.DCN_IGNORE_DELETEOP, "true");
  prop.setProperty(OracleConnection.DCN_IGNORE_UPDATEOP, "true");
  prop.setProperty(OracleConnection.NTF_LOCAL_HOST, serverIp);

  DatabaseChangeRegistration dcr = connection.registerDatabaseChangeNotification(prop);
  
  LOG.info("registration id: " + dcr.getRegId());
  LOG.info("database name: " + dcr.getDatabaseName());
	
  dcr.addListener(new DatabaseChangeListener() {
    public void onDatabaseChangeNotification(DatabaseChangeEvent dce) {
	  LOG.info("database change notification event");

      if (dce.getTableChangeDescription() != null) {
        for (TableChangeDescription tcd : dce.getTableChangeDescription()) {
          for (RowChangeDescription rcd : tcd.getRowChangeDescription()) {
            LOG.info("row change description: " + rcd);

            (...)
          }
        }
      }
    }
  });
	
  OracleStatement stmt = (OracleStatement) con.createStatement();
  stmt.setDatabaseChangeRegistration(dcr);

  stmt.executeQuery("select * from t_events");
}

Aby zarejestrować obsługę zdarzeń, należy sprecyzować najpierw kilka właściwości. W powyższym przykładzie DCN_NOTIFY_ROWIDS sprawia, że w zdarzeniu będziemy otrzymywali informacje o identyfikatorze wiersza tabeli, który będzie nam potrzebny do pobrania danych w tym wierszu przechowywanych. Właściwości DCN_IGNORE_DELETEOP, DCN_IGNORE_UPDATEOP oraz DCN_IGNORE_INSERTOP pozwalają na sprecyzowanie, na jakie zdarzenia będziemy konkretnie nasłuchiwali. Natomiast właściwość NTF_LOCAL_HOST określa adres serwera aplikacji, pod który będą przychodziły komunikaty zwrotne. Opcjonalnie można również określić właściwość NTF_LOCAL_TCP_PORT precyzującą port, pod który komunikaty mają przychodzić.
W linii 32 mamy jeszcze zdefiniowane zapytanie SQL. Mechanizm DCN działa w taki sposób, że sprawdza, czy zgodnie z nowym stanem bazy dla określonego zapytania zwracany jest inny wynik – jeśli tak, wysyłane jest zdarzenie. Wystarczy więc, że do tabeli „t_events” wstawimy nowy rekord i dostaniemy nowy komunikat zwrotny.

Co zrobić, jeśli w nowo wstawionym wierszu przechowujemy ważne z punktu widzenia aplikacji informacje? Możemy dostać się do nich za pomocą wspomnianego wcześniej ROWID:

Statement st = con.createStatement();
String sql = "select * from t_events where rowid = '" + rcd.getRowid().stringValue() + "'";
ResultSet rs = st.executeQuery(sql);

Aby całość nam zadziałała, użytkownik bazy danych powinien dostać stosowne uprawnienia:

grant change notification to <user name>

W celu sprawdzenia, czy rejestracje zdarzeń odbywają się poprawnie, możesz uruchomić na bazie takiego SQL’a:

select regid, table_name from user_change_notification_regs;

Na dzisiaj to tyle. Jeśli faktycznie mechanizm DCN przydał Ci się i udało Ci się go poprawnie uruchomić, koniecznie pochwal się tym w komentarzach. Sam jestem ciekaw, ze względu na jakie potrzeby można go zastosować :)

Skomentuj

5 Komentarze.

  1. Baza wcale nie sprawdza czy to główne zapytanie zwraca inny wynik(select * from t_events). To tak naprawdę tylko wskazuję bazie tabelę i tylko tyle. Równie dobrze do zapytanie może mieć taką formę:
    select * from t_events where 1=2 co jak widać nigdy nie zwróci wyniku a cały mechanizm i tak działa.
    To tylko taka mała uwaga po moich ostatnich bojach :)

    • Dzięki za uwagę, możliwe że jest tak jak mówisz :) Ogólnie odnoszę wrażenie, że cały ten mechanizm działa nieco tajemniczo, np. w jakiś automagiczny sposób po odpaleniu metody „execute” tworzy osobny wątek nasłuchujący na zdarzenia.

  2. To wszystko działa na zasadzie osobnego wątku i socketa, który nasłuchuje na info od bazy. Zauważ, że podajesz swoje ip i ewentualnie port. Te informacje lecą do bazy i już wiadomo komu ma wysyłać info o zmianach.

  3. no chopy – dobrze wam idzie – mam nadzieję, że w robocie też razem rozwiązujecie problemy :twisted:

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge