日本オラクル
プロダクトSC本部 テクノロジーSC部
佐藤 裕之

第2回:データアクセスことはじめ(後編)



 はじめに(後編)

前回の「データベースことはじめ(前編)」では、システムの論理的な階層の中でドメイン層をどの様に実装するかということで、PoEAAのアーキテクチャパターンを元に見てみました。今回は、パーシステンス層のアーキテクチャパターン+αを見ていきます。


 サービス層

まずパーシステンス層を見る前に、今回の話題とは直接ではないですが、間接的に関わってくる層のサービス層に関して少し触れておきたいと思います。サービス層は、ドメイン層配下のビジネスロジックをユーザインタフェース層やアプリケーション層から利用するためのインタフェースとして機能します。一般的にトランザクションロジックやセキュリティロジック、ドメインロジックのワークフロー等のドメインロジックとは直接関係ないロジックを含むのみで、大きなドメインロジックを含むことは好ましくないとされる層です。


 パーシステンス層アーキテクチャパターン

では、パーシステンス層です。ここまで、役割的には、データアクセスの実装を担わないTransaction Script、Domain Model、Table Moduleといったドメイン層のアーキテクチャパターンを見てきました。これらアーキテクチャパターンは、ドメインロジックが主体ですが、一般的にはそのドメインロジックには永続化しなければならない(データストアに格納しなければならない)フィールドが必ず存在します。これらフィールドの永続化の役割を担うのがパーシステンス層です。パーシステンス層は、データストアである、リレーショナルデータベース(RDB: Relational Database)、オブジェクト指向データベース(OODB: Object Oriented Database)、メッセージ指向ミドルウェア(MOM: Message Oriented Middleware)、TPモニタ等の外部システムとの接続の機能を提供する層です。現状、データストアとしてはリレーショナルデータベースを利用することが多いと思います。リレーショナルデータベースへのアクセスはSQL(Structured Query Language)という標準インタフェースを通して行います。SQLはご存知の通り、構造化されたリレーショナルデータを操作するための言語ですので、オブジェクト指向プログラミング言語のJavaとは、本質的には相容れないものです。そのSQLのロジックを分離しオブジェクト指向言語で開発されたドメイン層とリレーショナル技術のリレーショナルデータベース(データストア)との差異を吸収する役割もパーシステンス層が持ちます。PoEAAではデータソース層という名称で呼ばれ、そのアーキテクチャパターンとしてTable Data Gateway、Row Data Gateway、Active Record、Data Mapperの4種類が紹介されているので、各アーキテクチャパターンを見ていきます。今回のコラムではデータソース層という言葉がなんとなくしっくりこなかったのでパーシステンス層で統一しています。

  Table Data Gateway

1つ目のパーシステンス層アーキテクチャパターンはTable Data Gatewayです。一言でいうと「データベースのテーブル/ビューに対してのデータアクセスロジックをカプセル化したクラスとしてとして実装する」といえます。

Table Data Gatewayは、一つのインスタンスでデータベースのテーブルを表現します。そして、それ自身には状態を保持せず、ドメイン層からのリクエストに応じてその都度データベースにアクセスし、データをドメイン層に返すという動作をし、ドメイン層とテーブル/ビューとの架け橋として動作します。基本的には単なるデータベースのテーブルに対してアクセスするSQLをカプセス化したインタフェースとして機能します。また、その中には、insert、select、update、deleteなどの典型的なCRUD(Create Read Update Delete)操作が含まれます。

【図】Table Data Gatewayパターンの概念図

では、サンプルコードをみていきます。今回はドメイン層のアプリケーションパターンで利用したBIDテーブル一つを例に単純なTable Data Gatewayパターンの実装サンプルBidTableDataGateway.javaを見ていきます。

【コード】Table Data Gatewayサンプル:BidTableDataGateway.java
// BidTableDataGateway.java
// ----- 省略 -----
public class BidTableDataGateway {
    // SQL文
    private static final String FIND_ALL =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid";
    private static final String FIND_BY_PRIMARYKEY =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_id = ?";
    private static final String FIND_HIHGEST_BID =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_unit_price = (select max(bid_unit_price) from bid where item_id = ?)";
    private static final String INSERT =
        "insert into bid (bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount) " +
        "values (?, ?, ?, ?, ?, ?)";
    private static final String UPDATE =
        "update bid set user_name = ?, bid_date = ?, item_id = ?, bid_unit_price=?, " +
        "bid_amount = ? where bid_id = ?";
    private static final String DELETE = "delete from bid where bid_id = ?";

    // 定数
    private static final String TABLE_NAME = "bid";
    private static final String PK = "bid_id";

    // 全件検索
    public RowSet findAll() throws AuctionException {
        RowSet rowset = null;
        try {
            rowset = DBUtil.getRowSet();
            rowset.setCommand(FIND_ALL);
            rowset.execute();
        } catch (SQLException se) {
            throw new AuctionException(se);
        }
        return rowset;
    }

    // 主キーによる検索
    public RowSet findByPrimaryKey(int id) throws AuctionException {
        RowSet rowset = null;
        try {
            rowset = DBUtil.getRowSet();
            rowset.setCommand(FIND_BY_PRIMARYKEY);
            rowset.setInt(1, id);
            rowset.execute();
        } catch (SQLException se) {
            new AuctionException(se);
        }
        return rowset;
    }

    // 入札最高額を検索
    public RowSet findHighestBit(int itemId) throws AuctionException {
        RowSet rowset = null;
        try {
            rowset = DBUtil.getRowSet();
            rowset.setCommand(FIND_HIHGEST_BID);
            rowset.setInt(1, itemId);
            rowset.execute();
        } catch (SQLException se) {
            se.printStackTrace();
            new AuctionException(se);
        }
        return rowset;
    }

    // 挿入
    public void insert(String userName, Date date, int itemId,
        int bidUnitPrice, int bidAmount) throws AuctionException {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(INSERT);
            pstmt.setInt(1, DBUtil.getNextId(TABLE_NAME, PK));
            pstmt.setString(2, userName);
            pstmt.setDate(3, date);
            pstmt.setInt(4, itemId);
            pstmt.setInt(5, bidUnitPrice);
            pstmt.setInt(6, bidAmount);
            pstmt.execute();
        } catch (SQLException se) {
            se.printStackTrace();
            new AuctionException(se);
        }finally {
          DBUtil.cleanUp(con,pstmt);
        }
    }

    // 更新
    public void update(int bidId, String userName, Date date, int itemId,
        // ----- 省略 -----
    }

    // 削除
    public void delete(int bidId) {
        // ----- 省略 -----
    }
}

BidTableGatewayクラスは、Bidテーブルに対するデータアクセスロジックを集約したクラスです。BIDテーブルに対する単純なCRUD操作をカプセル化し、ドメイン層からのデータベーステーブルBIDへのインタフェースとして機能します。
Table Data Gatewayパターンは、1つのテーブルに対する、insert/select/update/deleteなどのCRUD操作を一つのクラスで管理するので非常に単純な構造になり、もしデータベース管理者がチューニングの際などにSQLを膨大なJavaソースコード上から探し出すときにも非常に簡単に探し出すことが出来ます。一方、検索系(例のコードではfindXXX()メソッド)の戻り値をどのように実装するかが問題になります。サンプルコードではRowSetをリターンするように実装しています。RowSetを利用しない場合、単純なDTO(Data Transfer Object)などを利用し戻り値をカプセル化するクラスを用意する必要があるかもしれません。
ドメイン層のアーキテクチャパターンとの相性は、テーブルを主眼に実装されるという点でTable Moduleが良いでしょう。


  Row Data Gateway

2つめのパーシステンス層アーキテクチャパターンは、Row Data Gatewayです。一言でいうと「データベースのテーブル/ビューの1つの行に対してのデータアクセスロジックをカプセル化したオブジェクトとしての動作を実装」といえます。

Row Data Gatewayは単純にデータベースの1つの行に対して1つのインスタンスが生成されます。つまり、クラス テーブル/インスタンス 行という関係に成ります。

【図】Row Data Gatewayの概念図

ではまたBidテーブルにアクセスするRow Data Gatewayのサンプルを見てみましょう。まずは、データベース内の行を表すクラスBidRowDataGatewayクラスです。

【コード】Row Data Gatewayパターンの例:BidRowDataGateway
// BidRowDataGateway.java
// ----- 省略 -----
public class BidRowDataGateway {
    // SQL文
    private static final String UPDATE =
        "update bid set user_name = ?, bid_date = ?, item_id = ?, bid_unit_price = ?, " +
        "bid_amount = ? where bid_id = ?";
    private static final String DELETE = "delete from bid where bid_id = ?";

    // フィールド
    private int bidId;
    private String userName;
    private Date bidDate;
    private int itemId;
    // ----- 省略 -----

    // コンストラクタ
    public BidRowDataGateway(int bidId, String userName, Date bidDate,
        int itemId, int bidUnitPrice, int bidAmount) {
        this.bidId = bidId;
        this.userName = userName;
        this.bidDate = bidDate;
        this.itemId = itemId;
        // ----- 省略 -----
    }

    // DBレコードデータのロード
    public static BidRowDataGateway bidLoad(ResultSet rs)
        throws SQLException {
        // Identigy Mapのチェック
        int bidId = rs.getInt(1);
        BidRowDataGateway bidRowDataGateway = RowDataRegistryIdentityMap.getBid(bidId);
        if (bidRowDataGateway != null) {
            return bidRowDataGateway;
        }

        String userName = rs.getString(2);
        Date bidDate = rs.getDate(3);
        int itemId = rs.getInt(4);
        int bidUnitPrice = rs.getInt(5);
        int bidAmount = rs.getInt(6);

        bidRowDataGateway = new BidRowDataGateway(bidId, userName, bidDate,
                itemId, bidUnitPrice, bidAmount);
        return bidRowDataGateway;
    }

    // 更新
    public void bidStore() throws AuctionException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(UPDATE);
            pstmt.setString(1, userName);
            pstmt.setDate(2, bidDate);
            pstmt.setInt(3, itemId);
            pstmt.setInt(4, bidUnitPrice);
            pstmt.setInt(5, bidAmount);
            pstmt.setInt(6,bidId);
            pstmt.executeUpdate();
        } catch (SQLException se) {
        se.printStackTrace();
            throw new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt);
        }
    }

    // アクセッサメソッド
    public int getBidId() {
        return bidId;
    }
    public void setBidId(int bidId) {
        this.bidId = bidId;
    }
    // ----- 省略
    public int getItemId() {
        return itemId;
    }
    public void setItemId(int itemId) {
        this.itemId = itemId;
    }
    // ----- 省略
}

BidRowDataGatewayクラスは、フィールド変数としてデータベースの列に対応した変数を持ち、そのフィールド変数に対するアクセッサメソッド(getter/setter)を持ちます。また、インスタンス内のフィールド変数に対応した、テーブルの列を更新するbidStore()メソッド、インスタンスに対応した行からフィールド変数へデータをロードするbidLoad()メソッドを持っています。
また、内部的に利用されているRowDataRegistryIdentityMapクラスは、簡易的なキャッシュとしての機能を持ちます。RowDataRegistryIdentityMapクラスのコードは以下の様になります。

【コード】Row Data Gatewayパターンの例:RowDataRegistryIdentityMap.java
// AuctionRegistryIdentityMap.java
// --- 省略 ---
public class RowDataRegistryIdentityMap {
    private static RowDataRegistryIdentityMap uniqueInstance = null;
    // 静的メソッドによるSingletonインスタンスの生成
    static {
        if (uniqueInstance == null) {
            uniqueInstance = new RowDataRegistryIdentityMap();
        }
    }
    // Mapの生成
    private Map map = new HashMap();
    // BidHomeの取得
    public static BidHome lookupBidHome() {
        return new BidHome();
    }
    // Mapへの格納
    public static void setBid(BidRowDataGateway bid) {
        uniqueInstance.map.put(new Integer(bid.getBidId()), bid);
    }
    // Mapからの取り出し
    public static BidRowDataGateway getBid(int id) {
        return (BidRowDataGateway) uniqueInstance.map.get(new Integer(id));
    }
}

RowDataRegistryIdentityMapクラスは、PoEAAで紹介されている、ResgistoryパターンとIdentity Mapパターンの実装です。其々の概要は以下の様に成ります。

Registryパターン
既知のオブジェクトを登録しておくパターンであり、呼び出し側のオブジェクト間で共通のオブジェクトを検索する機能を提供します。一般にいうネーミングサービスの実装です。

Identity Mapパターン
Map内に、ロード済みのオブジェクトを格納することにより、其々のオブジェクトが一度だけ読み取られるようにする。一般にいうキャッシュの実装です。

RowDataRegistoryIdentityMapは、インスタンスが1つしか生成されないシングルトンとして実装されており、一度リレーショナルデータベースから取得したデータをキーを元にHashMapに格納し、一度リレーショナルデータベースから取得したデータを毎回データベースに取得しに行かないようなキャッシュとしての役割を持っています。
では、話を戻しドメイン層側でBidRowDataGatewayクラスのインスタンスを取得する時にはどのようにすればよいでしょうか?テーブル内に行は存在するがBidRowGatawayインスタンスが存在しない場合や、テーブルに行も存在せずBidRowGatewayインスタンスも存在しない時のために、それらオペレーションを別クラスBidHomeクラスとして今回は用意してみます。

【コード】Row Data Gatewayパターンの例:BidHome.java
// BidHome.java
// ----- 省略 -----
public class BidHome {
    // SQL文
    private static final String INSERT =
        "insert into bid (bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount) " +
        "values (?, ?, ?, ?, ?, ?)";
    private static final String FIND_BY_PRIMARYKEY =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_id = ?";
    private static final String FIND_ALL =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid";
    private static final String FIND_HIGHEST_BID =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_unit_price = (select max(bid_unit_price) from bid where item_id = ?)";

    // 定数
    private static final String TABLE_NAME = "bid";
    private static final String PK = "bid_id";

    // 挿入
    public BidRowDataGateway bidCreate(String userName, Date bidDate,
        int itemId, int bidUnitPrice, int bidAmount) throws AuctionException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        BidRowDataGateway bid = null;

        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(INSERT);
            int bidId = DBUtil.getNextId(TABLE_NAME, PK);
            pstmt.setInt(1, bidId);
            pstmt.setString(2, userName);
            pstmt.setDate(3, bidDate);
            pstmt.setInt(4, itemId);
            pstmt.setInt(5, bidUnitPrice);
            pstmt.setInt(6, bidAmount);
            pstmt.execute();
            bid = new BidRowDataGateway(bidId, userName, bidDate, itemId,
                    bidUnitPrice, bidAmount);
            RowDataRegistryIdentityMap.setBid(bid);
        } catch (SQLException se) {
            throw new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt);
        }
        return bid;
    }

    // 全件検索
    public Collection findAll() {
        List bidList = new ArrayList();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(FIND_ALL);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                bidList.add(BidRowDataGateway.bidLoad(rs));
            }
        } catch (SQLException se) {
            se.printStackTrace();
            new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt, rs);
        }

        return bidList;
    }

    // 主キーによる検索
    public BidRowDataGateway bidFind(int id) throws AuctionException {
        BidRowDataGateway bidRowDataGateway = (BidRowDataGateway) RowDataRegistryIdentityMap.getBid(id);
        if (bidRowDataGateway != null) {
            return bidRowDataGateway;
        }

        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(FIND_BY_PRIMARYKEY);
            pstmt.setInt(1, id);

            rs = pstmt.executeQuery();
            rs.next();
            bidRowDataGateway = BidRowDataGateway.bidLoad(rs);

            return bidRowDataGateway;
        } catch (SQLException se) {
            throw new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt);
        }
    }

    // 入札額の高い物を検索
    public Collection findHighestBid(int itemId) {
        List bidList = new ArrayList();
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(FIND_HIGHEST_BID);
            pstmt.setInt(1, itemId);
            rs = pstmt.executeQuery();

            while (rs.next()) {
                bidList.add(BidRowDataGateway.bidLoad(rs));
            }
        } catch (SQLException se) {
            se.printStackTrace();
            new AuctionException(se);
        }
        return bidList;
    }
}

BidHomeクラスには、テーブルの行の有無に関係なく、BidRowGatewayインスタンスの生成・検索、つまり取得に関するオペレーションをまとめてました。あと、気が付いた方もいると思いますが、わざとEJBっぽいクラス・メソッドの命名規則を利用してみました。

RowDataGatewayパターンは、テーブルの1レコードを表す1インスタンスという単純な実装になります。ただし、データベースの構造や、オブジェクト同士の関連などが複雑になってくるとそれに応じて、RowDataGatewayパターン内のコードも複雑になってくると思われます。例えば、BidRowDataGatewayのフィールド変数itemIdはプリミティブなint型になっていますが本来は、ITEMテーブルの行を表すItemRowDataGatewayクラス(サンプルはありません)のインスタンスへの参照として実装すべきです。そのような場合、BidRowDataGatewayのコンストラクタ内でItemRowDataGatewayのインスタンスを生成するように実装できるかもしれません。しかしそうしてしまうと、単純にBidRowDataGatewayのインスタンスの情報のみ(つまりBIDテーブルのみ)を操作したい場合でも、無駄にItemRowDataGatewayのインスタンスが生成されてしまうので、BidRowDataGatewayのインスタンスを生成しただけではItemRowDataGatewayのインスタンスを生成しないように、何らかの対処(Lazy Loadingの実装等)が必要になるかもしれません。今回のサンプルでは、Bidテーブル1つのみを扱っていますので目立ちはしませんが、この様に本当に全ての必要な機能をRowDataGatewayパターンで実装しようとすると結構大変かと思われます。

Table Data GatewayとRow Data Gatewayを見てきましたが、Core J2EEパターンであるデータベースアクセスをカプセル化し、抽象化するパターンのDAO(Data Access Object)とどこが違うんだ?と思った方もいるかと思います。これは、言葉の定義の問題なのですが、DAO Table Data Gateway + Row Data Gatewayという関係です。


  Active Record

3つ目はActive Recordです。一言で言うと「データベースのテーブル/ビューの1つの行をラップしたオブジェクトで、データアクセスロジックやドメインロジックをカプセル化したオブジェクトとして実装」といえるかと思います。

Active Recordは、それ自身にドメインロジックを含むという点でドメイン層のDomain Modelパターンとデータベースのテーブルのレコードを表すという点でパーシステンス層のRow Data Gatewayパターンに良く似ています。
まず、Domain Modelと似ているという点ですが、Active Record自体がDomain Objectと成り得るのですが、本当?のDomain Objectと異なる点は、Active Recordはテーブルの構造を元にして基本的にテーブルの列の表現として設計される点です。一方、Domain Objectは、テーブルの構造に関係なく、オブジェクト指向のドメイン分析の結果抽出されます。
例えば、前編で説明したDomain Modelでは、Strategyパターンを利用しているものの、BIDテーブルに対応したBidクラス、Itemテーブルに対応したItemクラス、SALEテーブルに対応したSaleクラスとテーブルとクラスがほぼ一対一になっていますが、ドメインオブジェクトの設計によっては同じテーブル構造のまま、Itemを抽象クラスとして用意して、そのItemを継承したBookやComputerなどの具象クラスを作成する様な継承関係を実装するかもしれません。その場合、それら継承関係をItemテーブルという1つのテーブルに格納しなければならないかも知れません。つまり、Domain Modelが複雑になればなるほど、両者の違いは明確になってきます。そもそもドメイン層とパーシステンス層という全く役割の異なった層のパターンですので違うのが当たり前ですが・・・
あと、Row Data Gatewayと似ているという点ですが、これは単純にActive Recordがドメインロジックを含むという点かと思われます。

【図】Active Recordパターンの概念図

Active Recordパターンのコードは、Row Data Gatewayとほぼ同じです。異なる点は、それ自身にドメインロジックが含まれるという点ですので今回は省略します。


  Data Mapper

最後にData Mapperです。一言で言えば「ドメイン層とデータベーステーブル間のデータ移動を行い、ドメイン層とデータベースに依存せずに状態を保持する」といえます。
Data Mapperパターンは、ドメイン層にもデータストアにも極力依存せずにデータアクセスロジックをカプセル化します。
そんなData Mapperパターンですが、現在では開発者自身が実装することは少なくなってきているかと思われます。Data Mapperパターンの機能+αを実現するフレームワークが多く提供されているからです。OracleAS TopLinkという製品もData Mapperパターンの機能+αを実現するフレームワークの1つです。

【図】Data Mapperの概念図

Data Mapperパターンは、フレームワーク(O/Rマッピングフレームワーク、O/Rマッピングレイヤなどと呼ばれる)として既に多くの製品が世に出ています。ただ、基本的な原理を理解することにより、そのありがたみ?を多少感じることが出来ると思いますので簡単なサンプルをご紹介したいと思います。
まずは、Data Mapperの汎用的な処理を実装した抽象クラスAbstractMapperクラスです。

【コード】Data Mapperパターンの例:Abstract Mapper.java
// AbstractMapper.java
// ----- 省略 ----
abstract class AbstractMapper {
    protected Map loadedMap = new HashMap();
    protected abstract String findByPrimaryKeyStatement();
    // 主キーを元に検索
    protected Object findByPrimaryKey(int pk) throws AuctionException {
        // 簡易キャッシュのチェック
        Object result = loadedMap.get(new Integer(pk)); // キャッシュ
        if (result != null) {
            return result;
        }
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(findByPrimaryKeyStatement());
            pstmt.setInt(1, pk);
            rs = pstmt.executeQuery();
            rs.next();
            result = load(rs);
            Object obj = result;
            return obj;
        } catch (SQLException se) {
            throw new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt, rs);
        }
    }

    // フィールドのロード
    protected Object load(ResultSet rs) throws SQLException {
        int pk = rs.getInt(1);
        if (loadedMap.containsKey(new Integer(pk))) {
            return loadedMap.get(new Integer(pk));
        } else {
            // サブクラスのdoLoad()メソッド実行
            Object result = doLoad(pk, rs);
            // 簡易キャッシュへの格納
            loadedMap.put(new Integer(pk), result);
            return result;
        }
    }

    // ResultSet内のオブジェクトをロードする抽象メソッド
    protected abstract Object doLoad(int pk, ResultSet resultset)
        throws SQLException;

    // ResultSet内の全てのオブジェクトをロード
    protected List loadAll(ResultSet rs) throws SQLException {
        List result = new ArrayList();
        while (rs.next()) {
            result.add(load(rs));
        }
        return result;
    }

    // INSERT文を返す抽象メソッド
    abstract protected String insertStatement();

    // オブジェクトのフィールドをINSERTする抽象メソッド
    abstract protected ObjectHolder doInsert(Object object,
        PreparedStatement pstmt) throws AuctionException;

    // オブジェクトのフィールドをINSERT ? doInsert()メソッドの呼び出し
    public int insert(Object object) throws AuctionException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ObjectHolder holder = null;  // オブジェクト受け渡しの箱
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(insertStatement());
            holder = doInsert(object, pstmt);
            loadedMap.put(new Integer(holder.getId()), holder.getObject());
        } catch (SQLException se) {
        se.printStackTrace();
            throw new AuctionException(se);
        }

        return holder.getId();
    }
}

AbstractMapperクラスでは、Data Mapperパターンを実現する上で共通に必要な処理を抽象クラスとして実装しています。
では次はAbstractMapperの実装クラスです。このクラスは、ドメインクラスに対応して実装します。今回はドメインクラスBidクラスに対応したBidMapperクラスを作成します。

【コード】Data Mapperの例:Bid Mapper.java
// BidMapper.java
// ----- 省略 ----
public class BidMapper extends AbstractMapper {
    // SQL文
    public static final String FIND_BY_PRIMARYKEY =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_id = ?";
    private static final String FIND_HIGHEST_BID =
        "select bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount " +
        "from bid where bid_unit_price = (select max(bid_unit_price) from bid where item_id = ?)";
    private static final String INSERT =
        "insert into bid (bid_id, user_name, bid_date, item_id, bid_unit_price, bid_amount) " +
        "values (?, ?, ?, ?, ?, ?)";
    private static final String UPDATE =
        "update bid set user_name = ?, bid_date = ?, item_id = ?, bid_unit_price=?, " +
        "bid_amount = ? where bid_id = ?";

    // 定数
    private static final String TABLE_NAME = "bid";
    private static final String PK = "bid_id";

    // 主キーを元に検索するSQL文
    protected String findByPrimaryKeyStatement() {
        return FIND_BY_PRIMARYKEY;
    }

    // 主キーを元に検索
    public Object find(int bidId) throws AuctionException {
        return find(bidId);
    }

    // 入札最高額の取得
    public Collection findHighestBid(int itemId) throws AuctionException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(FIND_HIGHEST_BID);
            pstmt.setInt(1, itemId);
            rs = pstmt.executeQuery();
            return loadAll(rs);
        } catch (SQLException se) {
            throw new AuctionException(se);
        }
    }

    // INSERT文をリターン
    protected String insertStatement() {
        return INSERT;
    }

    // BidフィールドのINSERT
    protected ObjectHolder doInsert(Object object, PreparedStatement pstmt)
        throws AuctionException {
        Bid bid = (Bid) object;
        int id = 0;
        ObjectHolder holder = null;

        try {
            id = DBUtil.getNextId(TABLE_NAME, PK);
            bid.setBidId(id);
            pstmt.setInt(1, id);
            pstmt.setString(2, bid.getUserName());
            pstmt.setDate(3, bid.getBidDate());
            pstmt.setInt(4, bid.getItemId());
            pstmt.setInt(5, bid.getBidUnitPrice());
            pstmt.setInt(6, bid.getBidAmount());
            pstmt.execute();
            holder = new ObjectHolder(id, (Object) bid);
        } catch (SQLException se) {
        se.printStackTrace();
            new AuctionException(se);
        }
        return holder;
    }

    // Bidフィールドのロード
    protected Object doLoad(int bidId, ResultSet rs) throws SQLException {
        String userName = rs.getString(2);
        Date bidDate = rs.getDate(3);
        int itemId = rs.getInt(4);
        int bidUnitPrice = rs.getInt(4);
        int bidAmount = rs.getInt(5);
        return new Bid(bidId, userName, bidDate, itemId, bidUnitPrice, bidAmount);
    }

    // Bidフィールドの更新
    public void update(Bid bid) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DBUtil.getConnection();
            pstmt = con.prepareStatement(UPDATE);
            pstmt.setString(1, bid.getUserName());
            pstmt.setDate(2, bid.getBidDate());
            pstmt.setInt(3, bid.getItemId());
            pstmt.setInt(4, bid.getBidUnitPrice());
            pstmt.setInt(5, bid.getBidAmount());
            pstmt.setInt(6, bid.getBidId());
            pstmt.executeUpdate();
        } catch (SQLException se) {
        se.printStackTrace();
            new AuctionException(se);
        } finally {
            DBUtil.cleanUp(con, pstmt);
        }
    }
}

AbstractMapperのサブクラスであるBidMapperは、ドメイン層のオブジェクトであるBidクラス固有のデータアクセスロジックが含まれます。今回のサンプルはBidMapperといサブクラスを用意してBidクラス固有の処理であるSQLなどを記述しマッピングを定義していますが、O/Rマッピングフレームワーク等の製品では、これらマッピングを一般的には、マッピングメタデータファイル(一般的にはXMLファイル)等に記述し、その設定に従いSQLがO/Rマッピングフレームワークにより生成されます。つまり、O/Rマッピングレイヤ等を利用すれば、マッピングメタデータを記述するだけで、BidMappingクラスのようなマッピングロジックを記述する必要はありません。
では、ドメイン層のクラスであるBidクラスを見ていきます。

【コード】Data Mapperの例:Bid.java
// Bid.java;
// ----- 省略 -----
// Domain Modelパターンの簡単な実装
public class Bid implements Serializable {
    // フィールド
    private int bidId;
    private String userName;
    // ----- 省略 -----
    // コンストラクタ
    public Bid(int bidId, String userName, Date bidDate, int itemId,
        int bidUnitPrice, int bidAmount) {
        this.bidId = bidId;
        this.userName = userName;
        // ----- 省略 -----
    }
    public Bid(String userName, Date bidDate, int itemId,
        int bidUnitPrice, int bidAmount) {
        this.userName = userName;
        // ----- 省略 ----
    }

    // アクセッサメソッド
    public int getBidId() {
        return bidId;
    }
    public void setBidId(int bidId) {
        this.bidId = bidId;
    }
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
    // ----- 省略 ----
}

Bidクラスは、ドメイン層のクラスですが、何の変哲もないJavaクラスです。このような何の変哲もないJavaクラスのことをPOJO(Plain Old Java Object)などといったりするのですが、ドメインオブジェクトがPOJOであるというのが大きなData Mapperパターンの特徴です。これまでの説明したTable Data Gatewayパターン、Row Data Gatewayパターン、Active Recordパターンいずれを利用する場合も、これらパターンを利用するドメイン層のコード内に属性(フィールド変数)をセットするコードが入ります。属性(フィールド変数)をセットする方向性としては、Data Mapperパターンが、[ドメイン層]←[パーシステンス層]に対し、その他Table Data Gateway/Row Data Gateway/Active Recordパターンは、[ドメイン層]→[パーシステンス層]という関係になります。つまりData Mapperパターンを利用することにより、ドメイン層内には、リレーショナルデータベース(データストア)から取得した属性(フィールド変数)をセットするコードが入らず、パーシステンス層を気にせずに開発することが可能であるといえます。一方、欠点はData Mapperパターンを実装するときの複雑性になるかと思いますが、一般には、このData Mapperパターン+αを実装した製品であるO/Rマッピングレイヤを利用するので、開発者が実装する事はありません。


 パーシステンス層アーキテクチャパターンの選択

それでは、どのパーシステンス層のアーキテクチャパターンを選択すべきでしょうか?これは、ドメイン層のアーキテクチャパターンの選択に大きく依存します。PoEAAでは、以下の様な組み合が考えられるとしています。

ドメイン層アーキテクチャパターン パーシステンス層アーキテクチャパターン
Transaction Scriptパターン Row Data Gatewayパターン
Table Data Gatewayパターン
Domain Modelパターン Active Recordパターン
Data Mapperパターン
Table Moduleパターン Table Data Gatewayパターン

ドメイン層とパーシステンス層はどの様な組み合わせでもかまいませんが、より合わせやすいパターンは上記の様な組み合わせといっています。
また、少し視点を変えて各アーキテクチャパターンとシステムの論理的な階層の依存関係を表すと以下のように成るかと思われます。

【図】パーシステンス層/ドメイン層アーキテクチャパターンの依存関係

かなり抽象的かつ感覚的な図ですので、大体の感触をつかんでいただければ良いのですが、1つ言えることは、システムの論理的な階層である、ドメイン層とパーシステンス層は、其々異なる役割を持った層ですので、お互いに依存が少ないほうが好ましいと思われます。その点で言うとDomain ModelとData Mapperがお互いの層で依存関係が少なく済みます。


 O/Rインピーダンスミスマッチ

少し話題を変えてO/R(Object/Relational)インピーダンスミスマッチについて触れておきます。O/R(Object/Relational)インピーダンスミスマッチ、最近良く聞く言葉ですが、Scott Ambler著の「Agile Database Techniques」の中で、O/Rインピーダンスミスマッチ発生する技術的な側面として次の様な事を言っています。

「オブジェクト指向パラダイムは、ソフトウェア工学の原則に基づいていているが、リレーショナルテクノロジは数学的な原則に基づいている。其々の技術は、異なるパラダイムに基づいているため、シームレスに連携することはできない。」

結局、リレーショナルデータベースとオブジェクト指向言語は異なる技術なので、何か考えないと上手く連携することができないよと言っています。例えばどのような事かというのをオークションシステムで利用したItemを元に考えてみたいと思います。
オークションシステムでは、Itemは1つのテーブルもしくは、Domain Modelでは1つのクラスとして実装していました。Itemをカテゴライズしたいと考えた場合、以下の図の様な事が起こる可能性があります。

【図】O/Rインピーダンスミスマッチ

DOA(Data Oriented Approach)により、データベース設計者は、論理データモデルとしてITEMエンティティを親エンティティとしてカテゴライズされたBOOKとCLOTHESエンティティを子エンティティとしてモデリングされたとします。そこから物理データモデルに変換する段階では、単純に考えて3種類の方法が考えられると思います。1.論理データモデルのエンティティのまま、物理データモデルのエンティティ(テーブル)の親子関係を外部キーとして物理データモデルに変換する 2.全ての属性を一つのエンティティ(テーブル)として物理データモデルに変換する 3.論理データモデルの親エンティティの属性を子エンティティの属性に含め物理データモデルに変換する。そして、この3種類の中からデータベース設計者があまりこれぐらいのデータで結合(Join)が利用されるのはパフォーマンス上よろしくないかなということで、2.の1つのエンティティ(テーブル)としての物理データモデルを選択したとします。
一方、OOA(Object Oriented Approach)によりアプリケーション設計者は、Itemという抽象クラスとそれを継承するBookとClothesというクラスとしてオブジェクトモデリングを行ったとします。そうすると、Item、Book、Clothesという継承関係を持ったオブジェクトモデルのクラスをItemという一つのテーブルに対応づけるパーシステンス層を開発する必要があります。このようにリレーショナルデータの世界とオブジェクト指向の世界では、もともと考え方も技術も異なっているので、いざ連携しようとすると様々な差異が発生します。
この例は、非常に単純な一例にしか過ぎませんが、リレーショナルデータベースとオブジェクト指向技術の根本的に異なる技術ですので、この様な様々な差異が存在します。この差異のことを一般的にO/Rインピーダンスミスマッチといいます。


 パーシステンス層設計開発のベクトルとドメイン層アーキテクチャパターン

では、O/Rインピーダンスミスマッチという、オブジェクト指向技術とリレーショナル技術の潜在的な差異を吸収するためには、どのアーキテクチャパターンを選択すべきなのでしょうか?ドメイン層・パーシステンス層の開発のベクトルという観点から少し眺めてみたいと思います。
ドメイン層、パーシステンス層を開発する上で考えられるベクトル4つとOOA(Object Oriented Approach:オブジェクト指向アプローチ)、DOA(Data Oriented Approach:データ中心)の関係を表すと以下の様な形になるかと思います。

【図】データアクセス開発のベクトル

まず図の左側は、オブジェクト指向の世界とリレーショナルデータの世界から見たデータアクセスロジックを構築する際の論理的な階層を表しています。オブジェクト指向の世界とリレーショナルデータの世界は、O/Rインピーダンスミスマッチという潜在的な差異が存在するので、その差異をどこかの階層で吸収しなければなりません。その役割を担っているのが、リレーショナルデータベースへのインタフェースであるSQLのカプセル化を担っているパーシステンス層です。そのパーシステンス層を利用してシステム全体を開発する上では、大きくわけて4種類のベクトルが考えられます。

【表】開発のベクトル
開発のベクトル ベクトルの説明
1.Top-Down ドメイン層のJavaクラス(もっと上流から考えるとUMLのクラス図)から設計開発をし、パーシステンス層を開発し、データベーススキーマを開発する方法です。これはデータベーススキーマの構造がドメイン層の設計に大きく影響されます。
2.Bottom-Up データベーススキーマの設計からパーシステンス層を開発し、ドメイン層を開発する方法です。これはドメイン層の構造がデータベーススキーマの設計に大きく影響されます。
3.Meet-in-Middle ドメイン層とデータベーススキーマを別で開発し、その差異を吸収するパーシステンス層を開発する方法です。ドメイン層とデータベーススキーマは影響を受けずに個別に設計開発されるので、オブジェクト指向の世界とリレーショナルデータの世界がお互いに干渉されません。
4.Middle-Out パーシステンス層を最初に開発し、それに応じたドメイン層とデータベース隙間を設計します。パーシステンス層を最初に開発するということは、その段階でドメイン層とデータベーススキーマの設計を同時に行うということになります。

この4種類のベクトルの中で、O/Rインピーダンスミスマッチをうまく解決できそうなのはどれでしょうか?そうです、ドメイン層とデータベーススキーマの設計開発がお互いの影響を最低限に抑えることが出来る3.のMeet-in-Middleです。パーシステンス層のアーキテクチャパターンで、このMeet-in-Middleの開発のベクトルを実現しやすいのはドメイン層に依存しないで実装を行うことが出来るDataMapperパターンです。今回のData Mapperのサンプルコードで実現するのは、かなり大変そうですが、一般にData Mapperの役割は、EJB Entity Beanの機能やO/Rマッピングレイヤ(O/Rマッピングフレームワーク)というソフトウェアとして有償・無償で提供されているものを利用します。つまり、あまり自分で作りこむということはないです。これら技術に関しては次回以降取り上げます。

また、データアクセスロジックを構築する際の論理的な階層とドメイン層の対応は、以下のようになります。

【図】開発のベクトルとドメイン層アーキテクチャパターン

上記開発ベクトルの矢印は、以下の組み合わせて利用することを想定しています。
  1. Transaction Scriptパターンは、Table Data Gateway/Row Data Gateway/Active Recoedパターン
  2. Domain Modelパターンは、Data Mapperパターン
  3. Table Moduleは、Table Data Gateway/Row Data Gateway/Active Recoedパターン
ここでポイントとなるのは矢印の合わさっている場所(→←)です。1. Transaction Scriptと3.Table Modelの場合は、データベーススキーマに対応したパーシステンス層(Table Data Gateway/Row Data Gateway/Active Record)を開発し、そのパーシステンス層を呼び出すドメイン層を開発することが想定されるであろうという意味で、ドメイン層の部分で矢印(→←)が合わさっています。一方、2.Domain Modelの場合、Data Mapperパターンの利用を想定しています。ドメイン層である、Data Mapperの利用側は、フィールドをセットするコードが必要ありません。よってパーシステンス層の部分で矢印(→←)が合わさっています。
当然、パーシステンス層内で矢印が合わさっている方が、ドメイン層とデータベーススキーマが其々依存しなくなり、かつ結合しやすいものになるかと思われます。つまりO/Rインピーダンスミスマッチを解決するための最良の選択の1つであるといえます。このことからもDomain Model + Data Mapperの利点が理解できるかと思われます。


 まとめ

2回に渡り、システムの論理的な階層とその階層内のアーキテクチャパターン+αといった観点からリレーショナルデータベースアクセス全体を眺めてみました。
すこしまとめてみると、
「Javaプログラミング言語というオブジェクト指向言語とリレーショナルデータベースを利用しシステムを開発する上で必ず存在するO/Rインピーダンスミスマッチの環境下で、大規模なシステムにも適応しえるアーキテクチャは、「Domain Model」+「Data Mapper」を利用することが望ましい。しかし、Data MapperはO/Rインピーダンスミスマッチという複雑な問題を解決する実装を行う必要性のため、もし自己で開発する場合、様々なO/Rインピーダンスミスマッチを解消する要件を実装しなければならないため、ボトルネックになる可能性がある。ただ、実際はData Mapperの機能はEJB Entity Bean やO/RマッピングフレームワークなどのO/Rマッピングレイヤがあるので、それをうまく利用すれば効率的にO/Rインピーダンスミスマッチを埋めることが出来るかもしれない。」
といえるかと思います。次回以降は、これらO/Rマッピングレイヤの技術をみていきたいと思います。多少ですが、Javaデータベースアクセスを考える上での頭の柔軟体操にはなったのではないでしょうか?


 おまけ - 次回の予告

JavaOne2004以降、Javaにおけるデータアクセステクノロジの大きな変化?がはじまっていますね。

J2SE5.0(FCS)の出荷
http://java.sun.com/j2se/1.5.0/index.jsp

EJB 3.0(JSR-220) and JDO 2.0(JSR-243)の協業?
http://java.sun.com/j2ee/letter/persistence.html

これら個々のテクノロジについて次回以降ふれてみたいと思います。


【参考文献】
「Pattern of Enterprise Application Architecture」
Martin Fowler著
ISBN 0-321-12742-0
2002/11/05

「Agile Database Techniques」
Scott W. Ambler 著
ISBN 0-471-20283-5
2003/10