SQLとオブジェクト指向とO/Rマッピングの隙間で

ベンチャー社長で技術者で: オブジェクト指向言語で処理したら保守性が悪い!
http://el.jibun.atmarkit.co.jp/g1sys/2009/12/post-af96.html
SQLに依存することの危険性 ー 単体DBサーバでは終わらない時代の考え方 | 独り言v6
http://www.nonsensecorner.com/wp25/?p=4075

どちらも4年程前の記事ので、直接の言及は避けて、その延長上にメモを残して置かんと欲す…まあ、当時の状況とかもろもろの業務的な事情とか、その当時の「流行り」みたいなものがあるので、なんとも言えないところがあるのです。特に、オブジェクト脳とO/Rマッピング、さらに最近のNoSQLのあたりは数年たつと熱がおさまるもので、業務的にはうまく枯れてくれるとよいかなという分野です。どれも長く使うものですからね。

先に、先の2つの記事の欠けているところを補っておきます。

■ストアドプロシージャはロジックを含めたほうが効率が良い

私もやったことがあるのですが、一時期オブジェクト指向的にストアドプロシージャを使おうと思っていたことがあります。うまく再利用すると良いのでは?と考えていたのですが、ちまちまと小さなストアドプロシージャを作ると、先の「※なぜ、WHERE 売価 <> fn_標準価格(得意先コード, 商品コード)としてないか分かりますか?」に陥ります。答えはマッチングする件数が多くなって、ストアド fn_標準価格 を何度も呼び出してしまうから…なのですが、実はこのノウハウ自体が不要なのです。一見、共通化して fn_標準価格 がうまく隠蔽化しているように見えますが、これのために、インデックスの配置やらストアドを使ったときのクエリのノウハウやらが混在してしまい、データベースの性能をうまく引き出せなくなります。なので、ちまちまとした共通化をするよりも、がっつりとコピペをしてクエリを作ってしまったほうが、ストアド自体の性能が引き上げられるし、バッドノウハウを埋め込まなくて済みます(勿論、この記事にあるのは「例え」なので、その例えが悪かったってことなのですが)。

なので、ストアドを作るならば、100行位にわたる関数を作ったほうが、ストアド自体の本領を発揮できます。複数のクエリと内部変数と制御文を組み合わせて、普通の関数として使えるところがストアドの良いところなので、業務ロジックを含めたいのであれば、そうしたほうがよいでしょう。逆に、オブジェクト指向的なメソッドのまとまりとして作る場合には、ストアドは諦めて C# などのコードのほうにクエリを持ってきたほうがうまく動きます。これは私の経験上なので、それぞれの経験により、別々なのです。逆に言えば、ストアドをクエリから呼び出すのは極力避けたほうが良いでしょう。先の記事にあるように、最終的な SELECT の結果でストアドを使う(条件を絞ったあと)ってことになります。以前、WHERE 句に入れられて性能的にえらい目にあったという…私です。

ちなみに、ストアドは SQL を事前にコンパイルするから何度も使う時に早い、という以前の事情があったのですが、最近ではあまり変わりません。いや、1000行ぐらいのクエリなら意味があるかもしれませんが、最近のクエリ解析のほうでは、以前のクエリを内部で覚えていたりするので動的にクエリを作らない限り以前のクエリ解析のものが使われます。結果的に SQL 文を文字列で送っているのと変わらなくなります。ただし、文字列で構成するときにできるだけ変数を活用するといいです。これは変数以外のところはクエリ文が変わらないという利点があるのと、コマンドラインなどのチェックが楽になります。まあ、SQL インジェクションの問題もありますが、変数自体はプレコンパイルのクエリでもよいし、ストアドにしてもよいでしょう。ストアドにすると、ソース管理自体が DB 内とプログラム内に分離されてしまうので当時から避ける傾向にあるのですが、そこはプログラムコードを書く人の配置によりけりというところです。プレコンパイルのクエリを確認しても、なんらかの API 用のクラスを作ってひとまとめにしておけばソース管理が楽になります。単体テストもやりやすいですからね。

■老婆心ながら、SQL を知らずに O/R マッピングだけを使っていると危険

いま CakePHP を使っているのですが、O/R マッピングには色々なものがあります。.NET でも ADO.NET Entity Framework があるわけで、SQL 文を覚えなくてもプログラム内でこなすことができます。以前は、SQL を書くメリットは高速化にあった(O/R マッピングが遅いという意味で)わけですが、LINQ が内部的に SQL を作成していたり、Web API を使った場合はそもそものボトルネックはネットワーク帯域にあったりするわけで、O/R マッピング自体の速度性能だけが重視される時代は終わった気がします。勿論、内部インフラを高速化する必要がある場合は、なんらかのマッパーやSQLを調節するわけですが、8割以上のデータベース検索は、漠然とした O/R マッピングの機能で十分まかなえます。特に、CakePHP や RoR(ごめん使ったことがない)、Lightswitch などで作られたマスター定義の画面は、実にあっという間にできるので「生産性」が高いことこの上ないです。10年程前は、ちまちまと DB のマスター画面を作っていたのですが、顧客が操作を覚えてくれさえすれば、これらのチープなマスター定義の画面で十分です。逆に言えば、「顧客が操作を覚えてくれなかった」り、これらの O/R マッピングツールの自動生成では追いつけない画面は、なんらかの UI インターフェースを作らないと駄目ってことです。

O/R マッピングの join の仕方は色々あるのですが、やや面倒臭いところがあります。CakePHP の join は簡単な部分しか使ってなくて、面倒なところは query を使ってしまっている私なのですが、なんらかの SELECT を高速に回そうと思うと SQL 文を書いたほうが手早く書けます。SQL というのはそういう言語なのです。なので、O/R マッピングの join に直す場合は、SQL 文を書いた後に O/R マッピングの join を見直す、という手順にすると作業時間が少なくて済みます。このあたりは、相互に行き来できるほうが楽だったりします。よく、Access の ER 図を使って、テーブル間の連結を書いた後に SQL 文を書き出させて、それに WHERE 句を追加する、また Access の ER 図に戻す、という手順で学習をしていました。SQL 文は「文字」で構成されているために手早く書くには向いているのですが、全体を掴もうとすると難しいところがあります。しかし、サブクエリを使う場合には、あらかじめサブクエリを作ってチェックしてから FORM に入れ込むとか、そういう書き方ができます。さらっとした SQL 文であっても、1時間以上も時間が掛かったりします。これは、SQL 文自体に情報量が多いからなんですね。複雑怪奇なことをやる LINQ も似た位の時間が掛かってしまう(いや、複雑怪奇すぎるものは LINQ では無理だったりしますが)のですが、そんなときも SQL に立ち返ると以外とすんなりできたりします。

なので、いろいろな O/R マッピングがあるのですが、SQL 文は一通り覚えておくと作業効率が違いますし、試行錯誤がしやすくなります。なので、特定の O/R マッピング「だけ」を覚えてるのは避けておいたほうがよいでしょう。

■インピーダンスミスマッチの愚考

さて、本題に戻って、データベース(SQL) ?> O/R マッピング ?> オブジェクト指向 の変換を考え直してみます。オブジェクト脳と O/R マッピングが流行ったあたりなので、今となっては「うまく忘れ去れてている」ことを希望しているのですが、その中に出て来た「インピーダンスミスマッチ」という考え方は、あまり意味を持ちません。確かに、ミクロなレベルで、日付型だとか文字列だとか微妙な差異があってデータ変換的にはやらないといけないことなのですが、実は随分「些末なこと」です(と最近私は気づきました)。

例えば、オブジェクト指向的に次のように Person クラスを定義したとしましょう。

class Person {
  public int ID { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
}

このような場合、DB 的には次のようなマッチングを想定している訳です。

create table Person {
  ID int not null,
  Name varchar(256) not null,
  Age int not null, 
  PRIMARY KEY ( ID )
}

素直に DB とオブジェクト思考のマッピングができていますね。ID は更新用にプライマリーキーにあになる必要があります。大抵の「インピーダンスミスマッチ」は、このあたりの要素のレベルの議論になるのですが、実は現実はもっと大雑把/複雑です。

create table Person {
  ID int not null,
  Name varchar(256) not null,
  Birthday datetime, 
  PRIMARY KEY ( ID )
}

データベースのほうは、こんな風に誕生日(Birthday)が設定されているのです。当然ですよね。年齢は年ごとに代わるので年齢(Age)があるよりも、誕生日を入れておく方が永続化のデータベースとしては正しい設計です(勿論、当時の年齢を残すためにAgeを使う場合もありますが)。更に、誕生日が入力されないこともあるので、NULLが許容されています。

これを素直にオブジェクト思考にマッピングするときに、Birthday のプロパティを加えることができるのですが…ここで問題です。オブジェクト思考(プログラムのUI)のほうでは「年齢」が欲しいわけです。確かに、誕生日からいちいち計算することもできるのですが、それを Person クラスで使う側に設定させるのは変な話ですよね。オブジェクト指向のクラスがデータベースに寄りすぎています。

class Person {
  public int ID { get; set; }
  public string Name { get; set; }
  public int Age { 
    get { 
        if ( Birthday == null ) {
            return 0;
        } else {
            int age = DateTime.Now - Birthday.Year ;
            if ( Datetime.Now.AddYears(-age) &lt; Birthday ) {
                age--;
            }
            retrun age;
        }
    }
  public DateTime? Birthday { get; set; }
}

こんな感じに、年齢(age)プロパティを書き換えます。Ageプロパティをnull許容にするかどうかは好みの問題で変えるとして、オブジェクト指向のクラスをもっと UI 層に寄せるならば、こんな書き方ができるはずです。これも「インピーダンスミスマッチ」の一種です。

もちろん、この作り方とは違って、O/R マッピング専用のクラス(DataPersonクラス)を作っておいて、UI で使う場合には Persosn クラスを使う、という方法もあります。大抵はそんな感じになっているのハズなんですが、やたら層ばっかりが多くなるし、DB が変更になったり、相互変換手順が多くなったりして面倒ですよね。バグの温床になりそうです。O/R マッパーを完全自動化するのであれば、DataPerson ?> Person タイプの二重変換が必要ですし、また、「完全に自動化」するのであれば、O/R マッパー自体のコードは手を入れないという方法もあります。実際、Entitiy Framework の場合には、完全自動化されているので手を入れません。別のメソッドを使ってクラスを拡張するし、そのほうがやりやすいでしょう。

なので、オブジェクト指向的なクラスを UI により寄り添う形にした場合は、必ずミスマッチが発生します。というか、発生しなほうがおかしいでしょう?ってな訳で、必ず「相互変換が必要」として考えるのが良いかと思います。逆に言えば、O/R マッピングを使っているから、クラスを自動生成できるし、そのまま UI に使えるから便利、という「直観的な」流れに固執すると危ないかな、ということです。

■しかし、状況によりけりで、O/R マッピングを使い分ける

とはいえ、巷に O/R マッピングのツールはあるし、作業効率上それを使ったほうが手早くできるし、間違いも少ないです。実際、Excel アプリ的なツールであれば、Lightswitch で簡単にできます(逆に言えば、それ以上のことをしようとすると難しいかも…ってことです)。それぞれの適用範囲があるので、それに逸脱しないように道具を使い分けていくのがベターでしょう。

SQL もできる。O/R マッピングツールも持っている。オブジェクト指向で画面を作れる(MVVMのバインドが作れる)という3つの道具が揃った段階で、それぞれの守備範囲をざっと書いておきます。経験則と、その時代のツールの状況次第なので、随時変わるかもしれませんが、現状ではという但し書きで。

  • ひとつのテーブルを扱うCRUD(マスター定義画面)は、O/R マッピングのツールで一気に作る。利用者は、それぞれの O/R マッパーの操作に慣れて頂く。
    → 利用者がIT素人の場合には、UI のみ自作。内部は、O/R マッパーで。
  • 2つ程度のテーブルの join ならば O/R マッピングで解決
    → 3つ以上ならば、SQL 文を書いたほうが無難。
  • テーブル定義とUIの表現が大きく異なる場合は、オブジェクト指向寄りで作成
    → 「データ」と「表現(プレゼンテーション)」の違いになるので、主にプレゼンテーション層を作るオブジェクト指向寄りにクラスを作ると、画面のテストがやりやすい。
    → 1対1のような場合は、O/R マッピングの CURD 画面にしてしまうのも良い。

まあ、ゲーム的のハードなホスティングの場合は別なんでしょうが、中小企業のWEB廻りとか社内の業務アプリなんかは、この方針でやると「手が抜ける/バグが少ない」っていうメリットがあります。

カテゴリー: 開発 パーマリンク

SQLとオブジェクト指向とO/Rマッピングの隙間で への1件のコメント

  1. masuda のコメント:

    端的に言えば、データコンバートする回数の話かもしれません。たとえば、LINQ の場合、結果をリストに直すのに、ToList() という拡張メソッド(だっけ?)を使うけど、これを必要な回数だけ繰り返す、という感じですかね。
    SQL.ToORMap().ToUI()
    UI.ToORMap().ToSQL()
    の相互変換。
    途中にデータコンバート部分が入ると、
    SQL.ToORMap().ToConv().ToUI()
    と長くなる感じ。ToConv の中身は、Select/Whereの組み合わせや、1次元配列を2次元配列に直すとか、そういう仕組みを入れる。
    今は亡き(?)、UMLのMDAに近いものがあります。

コメントは停止中です。