DBアクセスを定番化しよう DBFlute入門

第1回DBFluteの概要

はじめに

JavaにおけるDBアクセス周りの実装は、アプリケーション開発において非常に問題の多い領域です。数多くのO/Rマッパが登場していますが、現場の要件を満たせるものはまだまだ少ないかと思います。

そのような状況の中、DBFluteは誕生しました。本稿では、このDBFluteが一体どういったもので、現場の問題に対してどのように解決をしていくか、基本から応用まで紹介していきます。

今回のターゲットとする言語はJavaですが、実はDBFluteはC#版も存在します。ほぼ同じ仕様で実現されていますので、文法的な違いだけを読み替えることでC#ユーザの方にもお楽しみ頂ける内容かと思います。

前提とするテーブル構造

これからの説明で登場するサンプルの実装では、図1のテーブル構造を前提として話を進めていきます。

図1 サンプルのテーブル構造(詳細は、こちらをご覧ください)
図1 サンプルのテーブル構造

DBFluteの概要(1) 役割

DBFluteとは何でしょうか? まずは、DBFluteの役割というものを見ていきましょう。

S2Daoのクラスを自動生成するツール

S2Daoは、Seasarファウンデーションが提供するO/Rマッパです。非常に評判が良く、多くのアプリケーションで利用されています。S2Daoは優れたフレームワークですが、そこで利用するEntityクラスやDaoインターフェイスを自動生成するような機能はないため、DBFluteはそこを補完します。これがDBFluteの一番の根底にある役割です。つまり、DBFluteは、実際の存在するデータベースからテーブル構造の情報を取得し, S2Daoのクラスを自動生成します

DBFluteを知るためには、S2Daoを知ることが必要です。本稿ではS2Daoの詳細を割愛しますが、DBFluteを使う上で意識しないといけない機能を簡単に説明しておきます。

S2Daoは非常に素晴らしい機能を持っています。それは、SQL文を2WAY-SQLという形式で記述することが可能であるという点です。

S2Daoでは、外だしのSQLファイル(以降、外だしSQLと呼びます)にSQL文を記述します。その時、バインド変数をSQLのコメント形式で記載し、かつ、テスト値を共存させることが可能なのです。

2WAY-SQLのサンプルは、次のリスト1のようになります。

リスト1 2WAY-SQLのサンプル
select *
      from BOOK book
     where book.BOOK_ID = /*bookId*/6
       and book.BOOK_NAME like /*bookName*/'S2Dao' || '%'

これにより、⁠アプリケーションで実行するときもツールで実行するときも同じSQLを利用」するということが実現可能になっています。これは一見地味ですが、現場の開発者が望んでいた機能です。

開発者は、自分で書いたSQLをすぐにSQL*Plusなどのツールで実行したいものです。なぜなら、SQLをアプリケーションに組み込んで実行するには環境的な時間が掛かるからです。

2WAY-SQLを使えば、SQLの間違い(簡易文法ミス)の検知が非常に早い段階でわかり、実行までの環境的な時間のコストを節約できます。開発者も非常に気持ちよく作業をすることが可能です。

拡張ツール

DBFluteはただの自動生成ツールではなく、S2Daoを拡張する役割も持っています。正確には、⁠S2Daoを拡張するクラス群を自動生成」します。

S2Daoは、関連クラスの自動生成機能以外にも、現場で利用するには足りない部分があります。それは、S2DaoはSQLの自動生成機能が弱く、実質ほとんどのSQLを外だしSQLとして実装することになってしまうということです。

しかし、それではDB変更に弱く、SQL特有のケアレスミスも多発して効率がよくありません。その不足部分を補完する拡張をDBFluteが提供します。

その他Behaviorと呼ばれるクラスも存在し、高水準なDBアクセスプログラミングを提供します。

開発支援ツール

DBFluteは、自動生成ツールという役割を超えて、DB周りの実装・DB管理の支援を行います。具体的には、次の支援などを実行できます。

  • S2Daoの2WAY-SQLを利用した外だしSQLの一括テスト実行
  • データベース上の既存FK・テーブルをDROPした後のDDL文実行

DBFluteの概要(2) 主な機能

では、DBFluteの主な機能を見ていきましょう。

自動生成機能

DBFluteのメイン機能は自動生成です。図2を見てください。

既にデータベース上にテーブルスキーマが出来上がっていることを前提とし、自動生成管理者が「JDBCタスク」を実行します。すると、DBFluteはJDBC経由でDBに接続し、スキーマ情報を取得しその情報をXMLファイルに出力します。このXMLを「スキーマファイル」と呼び、自動生成の中間ファイルとなります。

その後、自動生成管理者が「Generateタスク」を実行します。すると、DBFluteは先ほど作成したスキーマファイルを読み込み、その情報をもとに「DBFluteのS2Dao拡張」⁠Dao」⁠Entity」⁠Behavior」⁠ConditionBean」などのクラスを自動生成します。

図2 DBFlute簡易概念図
図2 DBFlute簡易概念図

自動生成されるクラスは、ジェネレーションギャップパターンを採用して自動生成されます。

通常、自動生成されたクラスにアプリケーション独自の拡張を加えたいものです。

ジェネレーションギャップパターンを採用していない自動生成の場合は、変更・追加した処理が再自動生成時に上書きされてしまいます。実質、独自の拡張はできないと考えてよいでしょう。

そこで、自動生成されるクラスをスーパークラスとサブクラスにわざと分けて生成し、再自動生成時にはスーパークラスのみを上書きするやり方を取ることで、サブクラスにアプリケーション独自の拡張を加えられるようになります。

DBFluteは「ジェネレーションギャップパターンによる自動生成」を提供します。

そのため、仕様変更でデータベースに変更が入る場合も、⁠データベースを変更⁠⁠→⁠再自動生成」と作業することで、アプリケーション独自の拡張を残したまま自動生成クラスに変更を反映することが可能です。

自動生成する主なクラスをカテゴリごとに分けた表がリスト2となります。主なクラスは全てジェネレーションギャップとして生成していることが示されています。

リスト2 生成クラスカテゴリ一覧
クラスカテゴリ概要ジェネレーション
ギャップ
利用頻度[1]
BehaviorDaoとEntityの定番処理を行うObject。
DaoS2DaoのDaoインターフェース。
Entityテーブルに対応するドメインエンティティ。
ConditionBeanSQL組み立てObject。

※1) プログラマが直接利用することが多いものは○、全く利用しないものは×、その間は△

DAOとEntityに関しては、S2Daoの自動生成ツールとして特に違和感はないでしょう。

しかし、BehaviorとConditionBeanが「おや?」と思われる方がいらっしゃるかと思います。また、DAOの利用頻度が△で、代わりにBehaviorが○であることに気付かれるかと思います。

そうです。ここがまさしくDBFluteが提供する「拡張」と言える部分なのです。

拡張機能(Behavior)

Behaviorとは、⁠DaoとEntityの定番処理を行うObject」と言えます。

論よりモデル、図3を見てください。検索処理を行う例のフローが表現されています。

図3 検索フロー
図3 検索フロー

画面プロセスであるPageクラスから後述するConditionBeanを使ってSQLを組み立て、Behaviorに渡して検索をしています。

また、図4を見てください。更新処理を行う例のフローが表現されています。

図4 更新フロー
図4 更新フロー

PageクラスからEntityに更新値を設定して、Behaviorに渡して更新をしています。

このようにDBFluteではBehaviorを利用してDBアクセスをします。

参照系メソッドの一覧は、リスト3を確認してください。

リスト3 Behavior参照系メソッド一覧
検索タイプメソッド存在チェック(※1)重複チェック(※2)
件数 int selectCount(MemberCB cb)--
1件 Member selectEntity(MemberCB cb)×
Member selectEntityWithDeletedCheck(MemberCB cb)
Member selectByPKValueWithDeletedCheck(Integer memberId)
n件ListResultBean<Member> selectList(MemberCB cb)--
ページング PagingResultBean<Member> selectPage(MemberCB cb)--

※1)結果が0件の場合は EntityAlreadyDeletedException が発生します。

※2)結果が2件以上の場合は EntityDuplicatedException が発生します。

基本的な検索機能を持ったメソッドが並んでいますが、特徴的な項目として「存在チェック⁠⁠・⁠重複チェック」というものがあります。

通常、1件検索の結果が0件であったら、それは次のことを表します。

  • 条件が間違っているかもしれない
  • データが間違っているかもしれない
  • 他のユーザが削除したかもしれない(すれ違い)

S2Daoの1件検索メソッドは、nullを返します。そのため、上記のようなことが発生した場合、何も考慮しなければNullPointerExceptionが発生する可能性があります。正しくハンドリングするためには、nullを判定して、それ相応の例外を明示的に発生させなければなりませんリスト4⁠。

リスト4 S2Daoの1件検索でのnullハンドリング
MemberCB cb = new MemberCB();
cb.query().setMemberId_Equal(3); // 会員IDが「3」であること

Member member =  memberBhv.selectEntity(cb);
if (member == null) {
    throw new XxxException(...);
}
Integer memberId = member.getMemberId();
...

しかし、1件検索はシステムの至るところで存在し、かつ、面倒で忘れやすいものです。

Behaviorでは、selectEntityWithDeletedCheck()がその存在チェックを内包しますリスト5⁠。

リスト5 Behaviorの存在チェックありの1件検索
MemberCB cb = new MemberCB();
cb.query().setMemberId_Equal(3); // 会員IDが「3」であること

// 該当レコードが無い場合は、
// 例外(EntityAlreadyDeletedException)が発生
Member member =  memberBhv.selectEntityWithDeletedCheck(cb);

// memberがnullであることはありえないので、
// NullPointerExceptionが発生する恐れはない。
Integer memberId = member.getMemberId();
...

これは、非常に地味ですが、実際の開発現場で効果を発揮します。少なくともNullPointerExceptionではなく、明示的な例外(明示的なメッセージを含む)を発生させることで、いざ例外が発生したときの原因究明のスピードが格段に違います。

また、1件検索の結果が2件以上であったら、それは次のことを表します。

  • 条件が間違っているかもしれない
  • データが間違っているかもしれない
  • DBの制約が抜けているかもしれない

S2Daoの1件検索メソッドは、例えば3件取得した場合に先頭の1件を返します。そのため、上記のようなことが発生した場合、間違いに気付かずにシステムが動いてしまう可能性があります。これは開発プロジェクトでは致命的なものです。

Behaviorでは、selectEntity()やselectEntityWithDeletedCheck()などがその重複チェックを内包しますリスト6⁠。

リスト6 Behaviorの重複チェックありの1件検索
MemberCB cb = new MemberCB();
cb.query().setMemberId_Equal(3); // 会員IDが「3」であること

// 2件以上取得した場合は、
// 例外(EntityDuplicatedException)が発生
Member member =  memberBhv.selectEntityWithDeletedCheck(cb);
Integer memberId = member.getMemberId();
...

こちらも、非常に地味ですが、実際の開発現場で効果を発揮します。間違っているものは、しっかり間違いとして扱う処理がBehaviorには既に組み込まれているのです。

更新系メソッドの一覧は、次のリスト7を見てください。

リスト7 Behavior更新系メソッド一覧
更新件数更新
タイプ
メソッド
Modified
Only(※3)
排他制御あり(※1)排他制御なし(Nonstrict)(※2)
1件INSERT void insert(Member member)-
UPDATEvoid update(Member member)void updateNonstrict(Member member)
DELETEvoid delete(Member member)void deleteNonstrict(Member member)-
void deleteNonstrictIgnoreDeleted(Member member)-
INSERT or UPDATEvoid insertOrUpdate(Member member)void insertOrUpdateNonstrict(Member member)
n件INSERTint[] batchInsert(List<Member> memberList)-
UPDATEint[] batchUpdate(List<Member> memberList)int[] batchUpdateNonstrict(List<Member> memberList)×
DELETEint[] batchDelete(List<Member> memberList)int[] batchDeleteNonstrict(List<Member> memberList)-

※1)排他制御の結果として対象のRecordが存在しない場合に EntityAlreadyUpdatedException が発生します。この例外はS2Daoの排他例外であるorg.seasar.dao.NotSingleRowUpdateRuntimeExceptionを継承しています。

※2)updateNonstrict()とdeleteNonstrict()は、対象のEntityが存在しない場合にEntityAlreadyDeletedExceptionが発生します。

※3)Setterが呼び出された項目だけをUpdate文のSet句に列挙して更新します。

細かく役割ごとに分かれています。主な特徴として、排他制御なしのメソッドが明示的に用意されている点です。

実際の開発において、排他制御なしで更新しなければならないときがあります。主には夜間バッチ処理による更新です。

S2Daoでは、排他制御の設定をしてしまうと、同じDAOからは排他制御なしで更新することができなくなります。更新処理直前に検索をして更新日時やバージョンNOを取得して更新をすれば、実質的に排他制御なしで更新することも可能ですが、スピードが求められる夜間バッチなどで、その事前検索はパフォーマンスに影響してきます。実際にはその方法はやらない方が良いでしょう。

BehaviorではNonstrictという名前の付いたメソッドを使えば排他制御なしで更新することが簡単に実現可能で、かつ、事前検索が不要です。

また、Behaviorの全ての更新は、Setterが呼び出されたものだけをUpdate文のSet句に設定して更新する「ModifiedOnly」を採用しています。不要な列を更新することはありません。

他にも細かい配慮があり、紹介し切れないので割愛させて頂きますが、このように、DBFluteではBehaviorを利用することで、より高水準なDBアクセスプログラミングを提供します。

拡張機能(ConditionBean)

DBFluteは、先述の通りS2Daoの不足部分を補うための機能を備えます。

その一つがConditionBeanと呼ばれるものです。ConditionBeanとは、⁠定番Queryのタイプセーフ実装を提供するObject」と言えます。

論よりコード、リスト8を見てください。

リスト8 ConditionBeanのサンプル
MemberCB cb = new MemberCB();
cb.setupSelect_MemberStatus(); //[1]
cb.query().setMemberName_PrefixSearch("Billy"); //[2]
cb.query().addOrderBy_MemberFormalizedDate_Desc();//[3]
cb.query().addOrderBy_MemberId_Asc(); //[4]
List memberList = memberBhv.selectList(cb); //[5]

// memberBhvはDIコンテナにインジェクションされる

[1]は、会員(Member)に対して会員ステータス(MemberStatus)を結合しSelect句に設定することを示します。

[2]は、会員名が「Billy」で始まるもので絞り込むという条件を設定しています。

[3]は、更新日時の降順でソートするという条件を設定しています。

[4]は、更新日時が同じ値の場合に会員IDの昇順でソートするという条件を設定しています。

[5]は、たBehaviorにConditionBeanを渡して検索処理を実行しています。

これが実行されると、発行されるSQLはリスト9のようになります。

リスト9 ConditionBeanのサンプル
select member.MEMBER_ID, member.MEMBER_NAME, ...
     , memberStatus.MEMBER_STATUS_CODE, ...
  from MEMBER member
    left outer join MEMBER_STATUS memberStatus
      on member.MEMBER_STATUS_CODE = memberStatus.MEMBER_STATUS_CODE
 where member.MEMBER_NAME like 'Billy%'
 order by member.UPDATE_DATE desc, member.MEMBER_ID asc

このように、シンプルで定型的(定番的)なSQLをプログラム上でタイプセーフに実装することができます。これにより、スペルミスやSQLの文法ミスよるバグはなくなります。

SQLの得意な数人の開発プロジェクトでは、そこまで効果を発揮しませんが、ある程度の人数でスキルもバラバラな開発プロジェクトでは、このConditionBeanが大いに力を発揮します。

まとめ

DBFluteが何なのか、おおよそご理解頂けましたでしょうか?

今回触れたのは、本当に概要です。もっと詳しくどのように利用できるのか、徐々に説明していきたいと思います。

それでは次回は、DBFluteの環境を構築して実際に使ってみましょう。

おすすめ記事

記事・ニュース一覧