はじめに

こんにちは。テクノロジー&エンジニアリングセンターの羽部です。

NablarchではDBアクセス機能としてJDBCラッパー  UniversalDao の2種類を提供しており、UniversalDaoの利用を推奨しています。ですがUniversalDaoは万能ではなく、必要に応じてJDBCラッパーを併用します。「併用します」が曲者で、使い分けを誤ると混乱のもとになります。

そこで今回は、両者の使い分けの基本に触れた後、UniversalDaoでできるDB操作を紹介します。

UniversalDaoとJDBCラッパーの使い分けの基本

UniversalDaoとJDBCラッパーの使い分けの基本的な考え方は以下の通りです。

  • UniversalDaoでできることはUniversalDaoを使い、できないことだけJDBCラッパーを使う。

DB操作(SQLの実行)に関して、UniversalDaoは次のことができます。

  • 単純なCRUD。
    • 登録、主キー指定の検索/更新/削除、特定のテーブルの全レコード取得(更新、削除は楽観的排他制御に対応)。
  • 任意のSELECT文の実行。

この後、これらについて見ていきます。

上記以外のDB操作をしたい時は、JDBCラッパーを使います。 代表的な例では、主キーや楽観的排他制御のバージョン番号以外の条件を指定したUPDATE文やDELETE文があります。

なおUniversalDaoも、最終的にDBにアクセスする際は内部でJDBCラッパーを使っています。このため、UniversalDaoでのDB操作とJDBCラッパーで同じことをした時の挙動は変わりません。

単純なCRUD

UniversalDaoは、以下の単純なCRUDは実装者がSQLを書かなくても実行できます。 実行時に引数として渡すエンティティクラスにJPAアノテーションをつけておきます。UniversalDaoは、このJPAアノテーションをもとに実行時、SQLを構築します。

※以下のコード例で、Userはエンティティクラスとします。

  • 登録
    // userは登録する値が設定されたエンティティクラスのインスタンス。
    UniversalDao.insert(user);
  • 一括登録
    List<User> users;
    // users作成処理
    ~
    // 一括登録
    UniversalDao.batchInsert(users);
  • 主キーを指定した検索
    User user = UniversalDao.findById(User.class, 1L);
  • 特定のテーブルの全レコードの取得
    EntityList<User> users = UniversalDao.findAll(User.class);
  • 主キーを指定した更新
    // userは更新する値が設定されたエンティティクラスのインスタンス。
    UniversalDao.update(user);
  • 主キーを指定した一括更新
    List<User> users;
    // users作成処理
    ~
    // 一括更新
    UniversalDao.batchUpdate(users);
  • 主キーを指定した削除
    // userは削除対象の主キーが設定されたエンティティクラスのインスタンス。
    UniversalDao.delete(user);
  • 主キーを指定した一括削除
    List<User> users;
    // users作成処理
    ~
    // 一括削除
    UniversalDao.batchDelete(users);

ここで「一括」とついているものは、サンプルコードの通りWHEREを指定して条件に当てはまるものをまとめて処理する、というものではありません。

任意のSELECT文の実行

UniversalDaoは、任意のSELECT文を実行することもできます。テーブルを結合させることもできます。

単純にSELECT文を実行するだけではなく、件数を取得するなど以下のようなバリエーションがあります。

以下、それぞれ見ていきます。

検索結果を全て取得する

メソッドの詳細はこちら

単純にSELECT文の実行結果を全て取得したい場合に使用します。

// 検索条件を引き渡すためのBeanを設定する
// SQL「FIND_BY_AUTHOR」にBookエンティティのAUTHORカラムがバインド変数として
// 記述されている場合を想定する
Book condition = new Book();
condition.setAuthor("Martin Fowler");

EntityList<Book> books = UniversalDao.findAllBySqlFile(
                                        Book.class,
                                        "FIND_BY_AUTHOR",
                                        condition);
FIND_BY_AUTHOR=
SELECT * FROM BOOK WHERE AUTHOR = :author

検索結果の最初の1件だけ取得する

メソッドの詳細はこちら

主キーは指定しないけれど、検索結果が1件だけとわかっている場合に使います。例えばGROUP BYを併用しないでSUM関数を使用した場合です。

// 検索条件を引き渡すためのBeanを設定する
// SQL「FIND_ALL_PRICE」にBookエンティティのAUTHORカラムがバインド変数として
// 記述されている場合を想定する
Book condition = new Book();
condition.setAuthor("Martin Fowler");

EntityList<Book> books = UniversalDao.findAllBySqlFile(
                                        Book.class,
                                        "FIND_ALL_PRICE",
                                        condition);
FIND_ALL_PRICE=
SELECT SUM(PRICE) FROM BOOK WHERE AUTHOR = :author

検索結果の件数を取得する

メソッドの詳細はこちら

SELECT文(COUNT関数なし)を実行した時にヒットする件数が取得できます。SELECT文(COUNT関数なし)から件数取得用のSQL(COUNT関数あり)を作成し、実行します。

検索結果件数を取得するために、わざわざCOUNT関数を組み込んだSELECT文を用意する必要はありません。

// 検索条件を引き渡すためのBeanを設定する
// SQL「FIND_BY_AUTHOR」にBookエンティティのAUTHORカラムがバインド変数として
// 記述されている場合を想定する
Book condition = new Book();
condition.setAuthor("Martin Fowler");

long count = UniversalDao.countBySqlFile(
                                Book.class,
                                "FIND_BY_AUTHOR",
                                condition);
FIND_BY_AUTHOR=
SELECT * FROM BOOK WHERE AUTHOR = :author

条件に当てはまる検索結果が存在するか確認する

メソッドの詳細はこちら

対象のレコードが存在するか確認したい時に使います。例えば、データ相関チェック(入力された部署コードが部署マスタに存在するかなどのチェック)で使用します。

// 検索条件を引き渡すためのBeanを設定する
// SQL「FIND_BY_ID」にBushoエンティティのIDカラムがバインド変数として
// 記述されている場合を想定する
Busho condition = new Busho();
condition.setId(1L);

if (UniversalDao.exists(
                        Busho.class,
                        "FIND_BY_ID",
                        condition)) {
                            // 存在した場合の処理
                        } else {
                            // 存在しなかった場合の処理
                        }
FIND_BY_ID=
SELECT * FROM BUSHO WHERE ID = :id

この機能があるので、レコードの存在チェックとしてNoDataExceptioncatchするようなロジックは書かないようにしましょう。

検索結果を遅延ロードする

Nablarchの解説書はこちら

処理の内容によっては、大量の検索結果を扱うことがあります。例えばバッチ処理で処理対象データが数万件に及ぶときや、ウェブアプリケーションのダウンロード処理でダウンロード対象データが大量にある場合などです。このような場合、「検索結果を全て取得する」を使って結果を一度に全て受け取ってしまうとメモリ不足に陥る可能性があります。

こんな場合に使うのが遅延ロードです。遅延ロードを使うと、内部的には検索結果のレコードが要求されるたびにそのデータをメモリに読み込みます。このため、一度に使用するメモリ量を抑えることができます。

遅延ロードを使用したい時は、検索結果を返すメソッドの前にdeferメソッドを呼び出すだけです。

重要な注意点が1つあります。それは、処理後必ずDeferredEntityListcloseメソッドを呼び出すことです(以下のコード例では、try-with-resources文を使ってcloseを呼び出しています)。これは内部でサーバサイドカーソルを使用しているので、リソース解放が必要なためです。

// try-with-resourcesを使ってcloseを呼び出している。
try (
    DeferredEntityList<User> users
        = (DeferredEntityList<User>) UniversalDao
                                        .defer()
                                        .findAll(User.class)
) {
    for (User user : users) {
        // userを使った処理。
    }
}

ページングを行う

Nablarchの解説書はこちら

ウェブアプリケーションの一覧検索画面では、検索結果を全て表示せず、例えば10件ずつ表示することが良くあります。11件目以降が見たければ、「次のページ」リンクをクリックするといった作りです。これをページングと呼んでいます。

UniversalDaoはページングをサポートしています。これは、例えば1ページ10件表示で3ページ目を表示したいから21~30件目を取得しよう、などと計算したり読み飛ばしたりといった処理が不要です。 1ページあたりの表示件数と何ページ目を表示したいか指定してやれば、必要な結果が返ってきます。

コード例を記載しましょう。1ページ10件表示で3ページ目のデータを取得したい場合です。 このコードで、21~30件目の10件のレコードが取得できます。

EntityList<User> users = UniversalDao
                            .per(10)
                            .page(3)
                            .findAll(User.class);

ポイントは3つです。

  • perメソッドに1ページあたりの表示件数を指定する。この場合は10。
  • pageメソッドに何ページ目を表示したいか指定する。この場合は3。
  • perメソッドとpageメソッドを呼び出した後に、検索結果を返すメソッドを呼び出す。この場合はfindAll。呼び出すのは検索結果を返すメソッドならいいので、findAllBySqlFile等でも可。

おわりに

いかがだったでしょうか。実装者の方はもちろん、特に設計者の方に読んでいただき、例えば内部設計時の参考にしていただけると幸いです。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。