パフォーマンス考慮

DBFluteが取り組んでいる (実行時の)パフォーマンス考慮 について説明します。

パフォーマンス考慮の重要性

いくら実装のパフォーマンスが良くても、実行時(ランタイム)のパフォーマンスが良くなければ、それは採用できないO/Rマッパです。 O/Rマッパは、DBアクセス実装の道具というだけでなく、アプリケーションの一部品として組み込まれるものだからです。

リフレクションを使わない

"O/Rマッパは、リフレクションを使うから遅い" と言われることがあります。 大抵のO/Rマッパは、Entityクラスにデータをマッピングする際にリフレクションを利用するからです。 現実的には、時代と共にリフレクションコストはかなり低いものになってきていますが、昔の印象に引きずられていることもよくあります。

DBFluteでは、データのマッピングの際に、リフレクションを利用しません。 自動生成ドリブンがあるがゆえに、データをマッピングするプログラム自体も自動生成 することができるからです。ConditionBean、外だしSQL、どちらにおいても、データのマッピングは自動生成されたマッピング処理が利用されます。

実質的にマッピングコストが(少しでも)低くなっていることも重要であると同時に、リフレクションを使ってないということ自体が、 特に昔の印象に引きずられていることから発生する(精神的な)懸念を払拭するためにも重要なことです。

SQLの組み立てに徹する

ConditionBeanは、プログラム上で(タイプセーフに)SQLを組み立てられる目的指向のAPIですが、 これは、決してSQL(RDBMS)を隠蔽するものではなく、あくまでSQLを(安全に)組み立てるためのもの に徹しています。"せっかくプログラム上で組み立てるのだから、SQL(RDBMS)への意識は隠蔽して、オブジェクトデータベースっぽくアクセスしたい" という声も頂きますが、DBFluteはその意見には賛成ではありません。ディベロッパーの "データが取れればいいや" という(魔が差してしまった)意識は極力排除して、あくまで(RDBMSに対して)DBアクセスをしているんだ という意識を持ってもらうことを重視しています。

何を取得しているのかを明示的に

ConditionBeanは、関連テーブルの取得に関しては、必ずプログラム上で明示的に指定することが求められます。 関連テーブルをデフォルトで取得してしまうO/Rマッパを見掛けますが、 DBFluteは、どこでも指定してない関連テーブルを勝手に取得するというようなことはありません。

e.g. 何を取得しているのかを明示的に(ConditionBean) {MEMBER, MEMBER_STATUS} @Java
MemberCB cb = new MemberCB(); // 会員を(基点として)取得
cb.setupSelect_MemberStatus(); // 会員ステータスを取得

(暗黙の)LazyLoad機能はない

(暗黙の)LazyLoad機能は一切備えておらず、get メソッドを呼び出したら、知らないうちに裏でSQLが発行されている、というようなことは絶対にありません。 one-to-manyの検索をする場合も、格好悪いと感じられるかもしれませんが、明示的に取得を宣言します。 "何を取得(検索)したのか?" がプログラム上に明確に現れるようにするためです。

e.g. one-to-manyも明示的に(ConditionBean) {MEMBER, PURCHASE} @Java
MemberCB cb = new MemberCB();
List<Member> memberList = memberBhv.selectList(cb);

// 購入を(明示的に)取得 (この時点で検索処理が実行される)
memberBhv.loadPurchaseList(cb, new ConditionBeanSetupper() {
    public void setup(PurchaseCB subCB) {
        subCB.query().addOrderBy_PurchaseCount_Desc();
    }
});

for (Member member : memberList) {
    ... = member.getPurchaseList(); // load したから取得できる
}
  • 関連するmany側のデータは一括取得(バッチフェッチ)するため、n+1問題は発生しない

SQLのログは見やすく

ConditionBeanで組み立てられたSQLは、人が見やすい形に整形されてログに出力 され、かつ、実行されます。ディベロッパーが、"自分がやったことはこういうこと(SQL)なんだ" というのが直感的にわかるようにしています。

e.g. ConditionBeanの実行ログ {MEMBER} @Console
- /===========================================================================
-                                                       MemberBhv.selectList()
-                                                       =====================/
- MemberAdminPage.initialize():43 --> ...
- select dfloc.MEMBER_ID as MEMBER_ID, dfloc.MEMBER_NAME as MEMBER_NAME, dfloc.MEMBER_ACCOUNT as MEMBER_ACCOUNT, dfloc.MEMBER_STATUS_CODE as MEMBER_STATUS_CODE, dfloc.FORMALIZED_DATETIME as FORMALIZED_DATETIME, dfloc.BIRTHDATE as BIRTHDATE, dfloc.REGISTER_DATETIME as REGISTER_DATETIME, dfloc.REGISTER_USER as REGISTER_USER, dfloc.REGISTER_PROCESS as REGISTER_PROCESS, dfloc.UPDATE_DATETIME as UPDATE_DATETIME, dfloc.UPDATE_USER as UPDATE_USER, dfloc.UPDATE_PROCESS as UPDATE_PROCESS, dfloc.VERSION_NO as VERSION_NO, dfrel_0.MEMBER_STATUS_CODE as MEMBER_STATUS_CODE_0, dfrel_0.MEMBER_STATUS_NAME as MEMBER_STATUS_NAME_0, dfrel_0.DISPLAY_ORDER as DISPLAY_ORDER_0
-   from MEMBER dfloc
-     left outer join MEMBER_STATUS dfrel_0 on dfloc.MEMBER_STATUS_CODE = dfrel_0.MEMBER_STATUS_CODE
-  where dfloc.MEMBER_NAME like 'S%' escape '|'
-  order by dfloc.MEMBER_ID asc
- ===========/ [00m00s016ms - Selected list: 6 first={1,Stojkovic,Pixy,FML,2007-12-01 02:01:10.0,1965-03-02 15:00:00.0,2008-01-23 13:38:25.989,replace-schema,replace-schema,2008-01-23 13:38:25.991,replace-schema,replace-schema,1}]

また、実際に発行されるときはバインド変数が利用されますが、ログにおいては表示用SQLということで条件値が埋め込まれた状態で表示されます。 これには、単に見やすいというだけでなく、いざこのSQLの実行計画を確認したいという場合にすぐにSQLのツールで実行できるというメリットがあります。

カラムレベルで調整可能

プログラム上でSQLを組み立てるAPIの弱点としてよく挙げられるものとして、"テーブル単位で全てのカラムを取得してしまう" ということがありますが、ConditionBeanでは、取得するカラムを(タイプセーフに)調整できる ようになっています。

e.g. 会員名称だけを取得(ConditionBean) @Java
MemberCB cb = new MemberCB();
cb.specify().columnMemberName(); // 会員名称を指定

2Way-SQLによる管理

いざとなれば外だしSQL、というようにいくらでも自由にパフォーマンスチューニングできるネイティブなSQLを書けるようにしています。 しかも、書けるというだけでなく、2Way-SQL という形式で書けるようにしていることで、安全性というだけでなく、パフォーマンス考慮にもプラスになっています。

例えば、実行計画を確認するような場合、2Way-SQLであれば、すぐにそのままSQLツールなどで実行計画を確認することができます。 実際に本番想定のデータが入っているのであれば、実際に実行してみてリスポンスを確かめてみることもできます。 2Way-SQLであることが、パフォーマンスチューニングにおいても大いに力を発揮するのです。

e.g. 2Way-SQLなので、そのまま実行することができる @OutsideSql
select member.MEMBER_ID
     , member.MEMBER_NAME
  from MEMBER member
 where member.BIRTHDATE = /*pmb.birthdate*/'1960-04-12'

更新対象カラムの明示的指定

DBFluteにおける更新処理では、update 文の set 句に指定するカラムを明示的に指定します。そのテーブル全てのカラムを列挙するのではなく、Entityの set メソッド(setter)が呼ばれたものだけを列挙して更新します。

これにより、(明らかに)同じ値での更新処理を行わない というのと (全ての項目を埋めるための)事前の検索(select)が要らない、という二つのメリットがあります。 オンライン処理ではあまり気にするほどではありませんが(特に排他制御処理をする場合は、どのみち事前検索が必要になることもあるため)、 大量件数のレコードを処理するバッチ処理などにおいては、このメリットがかなり効いてきます。

e.g. 会員名称だけを更新 @Java
Member member = new Member();
member.setMemberId(3);
member.setMemberName("Savicevic"); // 会員名称だけ変更
membherBhv.update(member); // 事前検索なしでも更新できる
e.g. 会員名称だけがset句に列挙される @SQL
update MEMBER set MEMBER_NAME = 'Savicevic'
 where MEMBER_ID = 3

初期化コストの調整

どんなフレームワークにも、初期化コストは付きものです。そして、その初期化コストをどこで払うのかは、環境によって異なります。 開発環境、そして、本番環境と単純なモデルで分けて考えると以下のようになります。

開発環境
呼び出された処理だけその場で初期化
本番環境
アプリ起動時にできるだけ初期化

DBFluteのデフォルトは、前者に適したものになっています。 開発時の単体テストなどにおいて、(そのとき)利用しないメソッドの初期化処理が入らないように考慮しています。

一方で、本番環境に関しては、明示的に(ConditionBean関連メソッドの)一括初期化をする処理を提供し、 アプリ起動時にその処理を(アプリにて)呼び出してもらうことで、本番環境に適した挙動にすることができます。

現場フィット - 一括初期化

但し、リッチクライアントアプリやバッチアプリに関して言えば、本番環境においても、開発環境と同じタイミングでの初期化が望ましいと考えられることがありますので、 一括初期化の利用は、そのアプリケーションの特徴と相談して決定します。