Specified JDBC Driver com.mysql.jdbc.Driver class not found

PCの引越しをしていますが、Eclipse上で以前作成したWebApplicationがローカルでDBにアクセスする際にエラーになりました。

一応、Reference Library には
mysql-connector-java-5.1.20-bin.jar
へのパスが張ってあるのですが、Driverのクラスが見つからないようでした。
最終的にはWeb Applibrary(WEB-INF/libへのファイルコピー)にファイルを追加することによって、動作しました。
Hibernate実行時に使用されるライブラリはWeb-INFに存在しないといけないのかも知れません。

 

Jboss Hibernate上でのKeep Alive

先日の、Jboss上のHibernateがMySQLにアクセスできなくなるという話。
Jboss上のDBにアクセスできない
一つの解決方法として、Keep Aliveを組み込んでみました。
Jboss上にServletを作成し、起動時での自動起動及び、Threadの無限ループで30分ごとにサーチを掛けるだけの物です。このKeep Aliveを導入するまでは毎日朝、サーバにアクセスしようとするとHibernateがMySQLにアクセスできない現象が発生していましたが、このKeep Aliveを組み込んでからはその現象は解消しました。サーブレット内のコードはこんな感じです。

public class KeepAlive extends HttpServlet {
 private static final long serialVersionUID = 1L;
 static KeepAliveMain t = null;

    public KeepAlive() {
        super();
    }

  public void init(ServletConfig config) throws ServletException {
  t = new KeepAliveMain();
  t.start();
 }

 }
class KeepAliveMain extends Thread{

 public KeepAliveMain() {
  // TODO Auto-generated constructor stub
  super();
 }
 public void run(){
  Logger logger = Logger.getLogger(“mylog”);
  Date d = new Date();
  logger.warn(“”+d+”: “+this.getClass().getName()+” KeepAliveMain(service) started”);
  for (int i=0;;i++){
   d = new Date();
   logger.warn(“”+d+”: KeepAliveMain(service):”+i);
         YearDao yearDao = new YearDao();;
      List<Year> yearList = null;
   yearList = yearDao.searchAll();
   yearList.isEmpty();
   try {
    Thread.sleep(30*60*1000);
   } catch (Exception e){
    e.printStackTrace();
   }
  }
 }
}

GetやPostは実装しないで、init()だけで、localのThreadクラスに起動をかけているだけです。

自分のHackとしてはこれでもいいのですが、平行してネットで現象を調べていくと要は、Hibernateにデフォルトで付属しているコネクションプールは試験的なのもで長時間未使用状態が続くとMySQL(DB)側でコネクションを切断してしまうのにたいして、Hibernate側ではまだコネクションが残っていると考えて問題が発生するようです。
また、Jbossのログにはこんなメッセージが出ています。

09:26:48,260 INFO  [DriverManagerConnectionProvider] Using Hibernate built-in co
nnection pool (not for production use!)

Hibernateを使うのであればコネクションプールはThirdPartyのものを使えということのようです。Hibernateのマニュアルにもそのあたりの記述がありました。
次はこのコネクションプールの設定方法が調査項目になります。

Jboss上のDBにアクセスできない

最近、Jboss上のDBにWebService経由でアクセスしようとするとExceptionが発生してデータベースにアクセスできません。Jbossを再起動すれば解消するのですが、今日は少し調査してみました。
Jbossのコンソールログに現れるのは以下のメッセージです。

09:24:14,157 INFO  [STDOUT] Hibernate: select temperatur0_.date as date30_, temp
eratur0_.average as average30_, temperatur0_.high as high30_, temperatur0_.low a
s low30_ from temperature temperatur0_ where temperatur0_.date=?
09:24:14,371 WARN  [JDBCExceptionReporter] SQL Error: 0, SQLState: 08S01
09:24:14,371 ERROR [JDBCExceptionReporter] Communications link failure

The last packet successfully received from the server was 83,742,985 millisecond
s ago.  The last packet sent successfully to the server was 69 milliseconds ago.
09:24:14,466 ERROR [STDERR] AxisFault
 faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.userException
 faultSubcode:
 faultString: org.hibernate.exception.JDBCConnectionException: could not execute
 query

おそらく、HibernateからMySQLへのコネクションがMySQL側では切断されているのにHibernate側ではまだそれが使用可能と思っているような気はします。Tomcatではこの現象は観察していません。もし、そうであれば、Hibernateが使えなくなったコネクションを切断して、最終的には新たなコネクションを再作成して動きそうな気はするのですが、一旦この状態になると自律的には解消しないようです。
Hibernateのマニュアルを少しみると、Connection Poolに関して、実装は試験用なのでThirdpartyのコードを使うことを推奨しているようにも見えました。そのようなライブラリもパッケージには入っているようです。
そのあたりも学習するとして、本当にIdle時間が長くてTimeoutしているのかどうか、ダミーのThreadから定期的にDBへのアクセスをさせてみようかなと思います。所謂KeepAliveですね。KeepAliveで状況が変わるかを確認してさらにTuningしてみようと思います。

Web Serviceによるリモートサーバへのデータセーブ

既に、リモートサーバからデータを取得してローカルのDBに格納するWeb Service及びサーブレットの構築は終了したので今回はローカルのデータをリモート側のサーバにWeb Service経由で格納する仕組みを作りました。とりあえず、ローカルサーバ上のサーブレットによるGUIからのマニュアル作業になりますが、将来的には自動化の予定。
リモート側でのsaveやupdateになりますので、その作業でのExceptionの扱いが今回の勉強ポイントかなと思います。
LocalのDB作業に関してはHibernateのExceptionを問題なくつかめたコードがWeb Service内部ではなぜか捕まえることができませんでした。今回少しExceptionの処理に関して修正が必要でした。また、リモート側にExceptionの成功や失敗を伝えるためにリターンコードはStringにしていました。
まず、Hibernateのsave, updateはこんな感じでラップしました。
 public void save(Year year) throws Exception {
  Session session = getSession();
        Transaction transaction = session.beginTransaction();
        try {
            session.save(year);
            transaction.commit();
        }catch (Exception e){
         transaction.rollback();
         throw e;         
        }finally{
         session.close();
        }
 }
 public void saveOrUpdate(Year year)throws Exception {
  Session session = getSession();
        Transaction transaction = session.beginTransaction();
        try {
            session.saveOrUpdate(year);
            transaction.commit();
        }catch (Exception e){
         transaction.rollback();
         throw e;         
        }finally{
         session.close();
        }
 } 
    public void update(Year year) throws Exception {
  Session session = getSession();
        Transaction transaction = session.beginTransaction();
        try {
            session.update(year);
            transaction.commit();
        }catch (Exception e){
         transaction.rollback();
         throw e;         
        }finally{
         session.close();
        }
    }
このレベルでは全てExceptionを上に投げてしまいます。
Web Service上では受け取ったExceptionによってClient側に返すStringを決定します。
 public String saveYear(Year year){
  initYearDao();
  try {
   yearDao.save(year);   
  } catch (ConstraintViolationException e){
   return “NG: ConstraintViolationException”;
  } catch (Exception e){
   e.printStackTrace();
   return “NG: Exception”;
  }
  return “OK”;
 }
 public String saveOrUpdateYear(Year year){
  initYearDao();
  try {
   yearDao.saveOrUpdate(year);
  } catch (ConstraintViolationException e){
   return “NG: ConstraintViolationException”;
  } catch (StaleStateException e){
   return “NG: StaleStateException”;
  } catch (Exception e){
   e.printStackTrace();
   return “NG: Exception”;
  }
  return “OK”;
 }
 public String updateYear(Year year){
  initYearDao();
  try {
   yearDao.update(year);
  } catch (StaleStateException e){
   return “NG: StaleStateException”;
  } catch (Exception e){
   e.printStackTrace();
   return “NG: Exception”;
  }
  return “OK”;
 }
とりあえず、以下の二つのExceptionに関しては個別に
ConstraintViolationException: save時に既存のデータがある場合
StaleStateException:      update時に既存のデータがない場合
とりあえず、学習のために、最終的なアプリではsaveを試して上のエラーがでればupdateを実行するようにしてみました。saveOrUpdateメソッドもあるみたいですが…
Client側のコードはこんな感じです。
        URL url = new URL(“http://”+targetServer+targetService);
        stub = new SocialWebServiceSoapBindingStub(url,null);
        stub.setUsername(“username”);
        stub.setPassword(“password”);
        String ret = stub.saveYear(year);
        out.println(ret+”<br>”);
        if(ret.contains(“ConstraintViolationException”)){
         ret = stub.updateYear(year);
         if (ret.equals(“OK”)){
          out.println(“Update successful<br>”);
         }else{
          out.println(“Update fails<br>”);          
         }
        }else if(ret.contains(“Exception<br>”)){
       out.println(“Save fails<br>”);                   
        }

Table Changes とMySQL triggerの設定

Hibernateのキャッシュ関連でテーブル変更の記録をとるTriggerとその記録Tableのサンプルを作成してみました。
作業中にHibernateがつかんでいるテーブルに関してはTriggerを作成することができませんでした、…実際にはテーブルに関して変更がロックされている感じでした。Tomcatを停止して作業すると瞬時に完了しました。

ログテーブルのSample:
CREATE TABLE table_changes (
  SERIAL_NO  MEDIUMINT NOT NULL AUTO_INCREMENT,
  DATETIME  DATETIME,
  TABLENAME VARCHAR(30),
  METHOD    VARCHAR(10),
  PRIMARY KEY (SERIAL_NO)
);

Triggerのサンプル:
CREATE TRIGGER triggerHourlyInsert AFTER insert ON hourly FOR EACH ROW
  INSERT INTO table_changes  (DATETIME,TABLENAME,METHOD)
         VALUES (now(),’hourly’,'insert’);
CREATE TRIGGER triggerHourlyUpdate AFTER update ON hourly FOR EACH ROW
  INSERT INTO table_changes  (DATETIME,TABLENAME,METHOD)
         VALUES (now(),’hourly’,'update’);
CREATE TRIGGER triggerHourlyDelete AFTER delete ON hourly FOR EACH ROW
  INSERT INTO table_changes  (DATETIME,TABLENAME,METHOD)
         VALUES (now(),’hourly’,'delete’);

こんな感じで管理が必要なテーブルに対してTriggerを設置しておけばそのテーブルに対する変更の履歴をいつでも確認することが可能となります。

 

Hibernateのキャッシュ操作関連

HibernateとHiberneteの外側でのデータベース作業の同期について、Hibernateの本家のマニュアルを読んでも、確かに、Hibernateの外側でデータ変更があった場合、Hibernateでは関知できないとあります。するとやはり、Hibernate側でキャッシュをクリアする仕組みを入れないといけなさそうです。少し調査してみました。私の調査結果なので現時点の私の理解のメモとなります。
Hibernateに関してのキャッシュは以下の二つがあるようです。
①Session Levelでのキャッシュ。一次キャッシュとも呼ばれます。
②Session Factory レベルでのキャッシュ。二次キャッシュとも呼ばれます。
Session Levelに関してはflush()コマンドにてDBとHibernate側との同期も行えるようですが、一つの作業ごとに新たにSessionを取得して終了後にclose()する限りはこのレベルのキャッシュを意識的に操作する必要は無さそうです。
一方で重要なのはSession Factoryレベルでのキャッシュ。sessionは常に作成、破棄が行われますが、一般的にサービスが始まれば同一のSession Factoryを継続的に使用することになります。DBに対して同一のSession Factoryの外側でDBに対する変更がありうる場合には、意図的にSession Factory のキャッシュのクリア、DBとの再同期が必要になります。また、これはシステム的にはメモリーの有効利用にも繋がります。頻繁に使用されないサービスのためにメモリーが専有されるのは好ましくありません。
まだ実際の試験はしていませんが、Session Factoryのキャッシュをクリアするにはevict()メソッドを使うようです。例としてはこんな感じ…

sessionFactory.evict(Test.class, testId); 
sessionFactory.evict(Test.class); 
sessionFactory.evictCollection(“Test.collection”, testId);    
sessionFactory.evictCollection(“Test.collection”); 

ある、特定のクラスやそのコレクション全体やあるいはその要素に関してキャッシュがクリアできるようです。
さらにシステム全体への組み込みを考えてみますと例えば、
①DB上の特定のテーブルに、変更があった場合にTableAにエントリを追加するトリガーを設定します。
②また、サービス側ではHibernateによるDBへの変更があった場合に同じくTableAにエントリを作成すると共に、エントリにはサービス名を書き込みます。
③サービス側のアプリはDB作業を行う場合にはまず、TableAの最新エントリを検索します。そのエントリに自分の名前があればそのまま作業を実行します。もし、そのエントリの名前が自分と異なればDBへの変更があったことになりますのでキャッシュのクリアを実行します。
この仕組みを使えば、自身が関知できないエンティティからの変更があった場合だけキャッシュのクリアを行い、DBとの再同期が図られます。
ちなみにTableAへの検索はキャッシュを経由する訳には行きませんので常にキャッシュクリア、あるいはjdbcで直接DBにアクセスするような仕組みが必要と思います。