竹形誠司 ブログ
Java+MySQL+Tomcat    »トピック一覧
掲示板へのスパムが多いため、「ご質問」のコーナーはユーザー登録制とさせていただきました。お手数ですが、上の「新規ユーザーの登録」メニューより登録をお願いします。
帳票Web
アプリケーション

受注開発始めました
詳しくは こちら
竹形 誠司 著/ラトルズ刊
JSP帳票アプリケーション実践開発入門
JSP帳票アプリケーション
実践開発入門

JSP業務アプリケーション短期開発入門
JSP業務アプリケーション
短期開発入門

Java+MySQL+Tomcatで始めるWebアプリケーション構築入門
Java+MySQL+Tomcatで始めるWebアプリケーション構築入門

Java+MySQL+Tomcatで作る掲示板とブログ
Java+MySQL+Tomcatで作る
掲示板とブログ
LinkedHashMapを使ったデータベースのキャッシュ
by 竹形 誠司[takegata]
SQL文のJOINはデバッグが面倒・・・
データベースから100行のリストを取り出す場合に、100回のSQL文を実行していたのではとても効率が悪くなってしまいます。普通は1回の実行で必要な行数のデータを
取得できるようにSQL文を工夫して書きますが、たとえば請求書のレコードには作成者のユーザIDだけが登録されていて、そのユーザの名前はユーザレコードに書かれているような場合はJOINの構文を使って請求書テーブルとユーザテーブルを結合する必要があります。
2つのテーブルを単純に結合するだけであれば、さほど面倒ということもありませんが、請求書テーブルに作成者だけでなく、最終更新者、決済者など複数のユーザIDが記録されているような場合は(更に他のテーブルとの結合もあったりして)、SQL文も複雑になって、だんだんデバッグが面倒になってきます。

Mapを使った対応表
このような場合、ユーザIDを指定するとユーザ名を返すMapオブジェクトを作り、staticな構成でメモリ上に常駐するようにしておくと、JOINを使ってユーザテーブルを結合する必要がなくなります。ただし、この方法ではアプリケーション起動時にMapオブジェクトに全ユーザのIDと名前をロードする必要があったり、ユーザ情報に変更があった場合にデータベースだけでなくMapオブジェクトも更新する必要があるなど、いろいろ気を使わなければならないこともあります。めったにシステムを利用しないユーザが大量にいる場合などは、Mapオブジェクトが占めるメモリも無駄になります。

LinkedHashMapを使ったキャッシュ
そこで、Mapオブジェクトにユーザ名を問い合わせた際に、見つからなかった場合にのみデータベースから読み込んでMapオブジェクトに登録する方法を考えます。こうすれば2度目以降の問い合わせに対してはSQL文の実行が不要になります。更に、あまり参照されないユーザを自動的に削除すればメモリも節約できます。Java標準のAPIであるLinkedHashMapを使えばこのようなMapオブジェクトが簡単に作れます。


Cacheクラス
次にLinkedHashMapを使ったCacheクラスの例を示します。

import java.util.Map;
import java.util.LinkedHashMap;

public class Cache<K,V> extends LinkedHashMap<K,V>{
    private int fSize;//キャッシュサイズ
    public Cache(final int aSize) { //コンストラクタ
        fSize = aSize;
        super(16, 0.75f, true);
    }

    @Override
    protected boolean removeEldestEntry(final Map.Entry eldest) {
        return size() > fSize;
    }
}
とても簡単ですが、これだけです。Cacheクラスのコンストラクタではまず指定されたキャッシュサイズをフィールドに格納し、その後基底クラスのコンストラクタを呼んでいます。ここでポイントになるのが3つ目の引数で、これをtrueにすると最後にアクセス(get)されたオブジェクトがリストのトップにランクされます。デフォルトはfalseで最後に登録(put)されたオブジェクトがリストのトップにランクされます。指定されたキャッシュサイズを越えてオブジェクトが登録された場合は、リストの最後のオブジェクトが削除されます。
オブジェクトが登録される際に、基底クラスからremoveEldestEntryが呼び出されます。このメソッドをオーバーライドすることにより、キャッシュサイズの上限を指定することができます。


Cacheクラスを使ったUserList
このCacheクラスを使ったUserListの例を次に示します。
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class UserList{
    private static Cache<Integer,String> fCache = new Cache<Integer,String>(3);//キャッシュサイズ=3
   
    public static String getName(int aId){
        String mUserName=null;
        Connection conn=null;
        Statement stmt = null;
        ResultSet rs = null;
        try{
            mUserName = fCache.get(aId);
            if(mUserName!=null){
                return mUserName;
            }
            String strConn = "jdbc:mysql://localhost/invoice"
                +"?user=Mulder&password=TrustNo1"
                +"&useUnicode=true&characterEncoding=utf8";
            conn = DriverManager.getConnection(strConn);
            stmt = conn.createStatement();
            String sql = "SELECT name FROM user WHERE id=" + aId;
            rs = stmt.executeQuery(sql);
            if(rs.next()){
                mUserName = rs.getString("name");
                fCache.put(aId,mUserName);
                return mUserName+"*"; //キャッシュミスの印(テスト用)
            }
            rs.close();
            stmt.close();
            conn.close();
        }catch(SQLException e){
            e.printStackTrace();
        }
        return mUserName;
    }
}

テストのためにキャッシュのサイズを3にしていますが、実際のアプリケーションではもっと大きな値を使うのが普通だと思います。たとえば、ユーザが100人いて、その中の20人ほどがよく参照されるという場合であれば、20+αをキャッシュのサイズにするとよいでしょう。
CacheオブジェクトのgetメソッドにユーザIDを渡して、戻り値がnullだった場合にデータベースを検索して、そのユーザが存在すればユーザIDとユーザ名をCacheオブジェクトにputしてユーザ名を返します。ユーザがキャッシュに存在している間はデータベースにアクセスすることなくユーザ名を取り出すことができます。データベースにもユーザが存在しない場合はnullを返します。上のコードでは、キャッシュがヒットしなかった場合(データベースへのアクセスが発生した場合)に*印を付けていますが、これはキャッシュの効果をチェックするためのもので、実際のアプリケーションでは不要です。

UserListを使ったテストコード
このUserListを使ったテスト用のプログラムを次に示します。
public class UserListTest{
    public static void main(String[] args){
        System.out.println("1:"+UserList.getName(1));
        System.out.println("2:"+UserList.getName(1));
        System.out.println("3:"+UserList.getName(2));
        System.out.println("4:"+UserList.getName(3));
        System.out.println("5:"+UserList.getName(4));
        System.out.println("6:"+UserList.getName(1));
        System.out.println("7:"+UserList.getName(4));
    }
}
このプログラムの実行例を次に示します。

1:管理者*
2:管理者
3:竹形誠司*
4:鈴木一朗*
5:田宮二郎*
6:管理者*
7:田宮二郎
ID=1のユーザ名は「管理者」です。最初の問い合わせではまだキャッシュに登録されていないのでデータベースへのアクセスが発生したことを示す*印が表示されていますが、2度目の問い合わせでは*が表示されないのでキャッシュからユーザ名が取得されたことが分かります。キャッシュのサイズを3に指定しているので、この後3人の異なるユーザに対して問い合わせを行うと「管理者」ユーザがキャッシュから削除されます。そのため、6行目で再び問い合わせをおこなった際にはデータベースへのアクセスが発生します(*印がついています)。7行目では、5行目と同じユーザのIDを問い合せていますが、キャッシュに残っているのでデータベースへのアクセスが発生していません(*印がありません)。

DAOやORマッピングのライブラリなどを使えば自動的にキャッシュが行われるようですが、私はわりとプリミティブな実装が好きなので、これほど簡単に実装できるのであればデータのキャッシュを自前で作るのも悪くないと思います。
投稿:竹形 誠司[takegata]/2011年 05月 25日 02時 58分 /更新:2011年 05月 25日 05時 16分