連載:C# 3.0入門

最終回 LINQ to SQL/落ち穂拾い

株式会社ピーデー 川俣 晶
2008/12/05
Page1 Page2 Page3

LINQ to SQLのサンプル

 この連載は、あくまでC# 3.0入門である。LINQ to SQLは、C# 3.0の外部に存在する独立した技術であり、Visual Basicなどでも利用されるものである。そのため、この連載では簡単なサンプルを提示し、C# 3.0プログラマーとLINQ to SQLのかかわりを示す程度にとどめる。後者はすでに述べたので、あとは簡単なサンプルを1つだけ提示して終わろう。とはいえ、C#プログラマーとして見るべきところは多い。

 ここでは以下のようなテーブル「PointList」が存在するという前提で始めよう。


PointListテーブルの設計


PointListテーブルの内容

 以降では、このテーブルに含まれる、pointが80以上の人を検索してみよう。

 まず、Visual Studio 2008でコンソール・アプリケーションのプロジェクトを作成する。次に、このプロジェクトに「LINQ to SQLクラス」を追加する。これはテンプレートから選ぶだけなので簡単だろう。

 追加すると即座にO/Rデザイナが開かれて、以下のような表示になる。


LINQ to SQLクラス追加時に開くO/Rデザイナ

 これを使って、O/Rマッピングを作成してみよう。やり方は簡単で、サーバ・エクスプローラ上の[PointList]という項目を中央の領域へドラッグ&ドロップするだけである。


生成されたO/Rマッピング

 これでコードを書く準備はできた。Program.csに以下のようなコードを書いてみよう。

using System;
using System.Linq;

namespace ConsoleApplication78
{
  class Program
  {
    static void Main(string[] args)
    {
      var db = new DataClasses1DataContext();

      var query = from n in db.PointList
                  where n.point >= 80
                  select n;

      foreach (var m in query)
      {
        Console.WriteLine("{0} {1} {2}points",
          m.name,m.date.ToString("yyyy/MM/dd"),
          m.point);
      }
      // 出力
      // 花子     2009/02/28 90points
      // 三郎     2008/12/31 100points
    }
  }
}
リスト1 LINQ to SQLによるデータベースの検索

 ここで、DataClasses1DataContextクラスはO/RマッピングとしてO/Rデザイナが自動生成してくれたクラス名である。これを使えば、db.PointListと書くだけでPointListテーブルに対するマッピングを参照でき、それはクエリ式でクエリできる。

 ちなみに、ここでは「m.date.ToString("yyyy/MM/dd")」の部分に注目しよう。この書式指定付きToStringメソッドはDateTime構造体に固有のものである。このようなコードが記述可能であるのは、PointListテーブルのdate列がDateTime型のプロパティにマッピングされているからである。つまり、SQLの型とC#の型がマッピングによって連携しており、型に関するトラブルが飛躍的に起こりにくくなっている。

 さて、ここで注意すべき点がある。このクエリ式は実際にはC#プログラムとしては実行されていない点である。つまり、「n.point >= 80」という式は、C#プログラムとして実行されることはない。

 常識的なC#プログラマーの感覚でいえば「そんなばかな」と思うかもしれないが、それは事実である。ではどのようにしてそれを証明できるだろうか。

 その方法は簡単で、変数dbをnewしている行の次に、以下の行を挿入するだけである。

db.Log = Console.Out;

 これで、どのようなSQL文がSQL Serverに送信されているかが手に取るように分かる。

SELECT [t0].[id], [t0].[name], [t0].[date], [t0].[point]
FROM [dbo].[PointList] AS [t0]
WHERE [t0].[point] >= @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [80]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1

花子     2009/02/28 90points
三郎     2008/12/31 100points
リスト1でSQL Serverに送信されているSQL文

 この3行目は、pointが「@p0」以上である場合という条件を示し、4行目で、@p0は80であることが示されている。つまり、このクエリの中には「n.point >= 80」という条件そのものが埋め込まれ、SQL Server側で処理されているのである。

 このことは、クエリがサーバ側で処理され、結果だけが返ってくることを示す。大量のデータをまず取得して検索を行うようなネットワークの無駄は排除されている。

 一方で、このような仕掛けが成立する理由は、前回説明したとおり、「式本体を持つラムダ式は、式ツリーに変換できる」というC# 3.0の機能性にある。LINQのクエリ式を式ツリーに変換したうえで、それをさらにSQL文に翻訳しているわけである。

 このことは、式ツリーに翻訳できてもSQL文に翻訳できない式は扱えないことを示す。例えば、条件判定がローカル上のメソッドに依存するような式はSQL文に翻訳できない。SQL Server側に同等のメソッドが存在しないからである。

 例えば、Programクラスに以下のメソッドを追加してみよう。

private static bool check(int n)
{
  return n >= 80;
}

 そして、クエリを以下のように書き直してみよう。

var query = from n in db.PointList
            where check(n.point)
            select n;

 このプログラムは構文的には正しいのでコンパイル可能である。しかし、実行すると以下のような例外を出して止まる。

System.NotSupportedException はハンドルされませんでした。
メソッド 'Boolean check(Int32)' には、サポートされる SQL への変換がありません。

 SQL Server側からcheckメソッドは直接呼び出せないし、仮にネットワークを越えて呼び出したとしても、時間がかかりすぎてクエリ速度が非現実的に遅くなるだけだろう。

 ただし、以下のようなメソッドを用意した場合はクエリできる。

private static int get80()
{
  return 80;
}

 クエリは以下のように記述する。

var query = from n in db.PointList
            where n.point >= get80()
            select n;

 この場合、get80メソッドはローカル上で計算可能であり、その計算結果である「80」をサーバへ送ることでクエリは成立する。

LINQ to SQLとメソッド構文

 リスト1は、クエリ式を以下のように書き直せば、メソッド構文になる。

var query = db.PointList.Where((n) => n.point >= 80);

 このように書き直しても、結果は同じである。同じクエリが送信され、同じ結果になる。クエリ式はメソッド形式の糖衣構文だからである。メソッド形式で書くと、このラムダ式がローカルで実行されないことが直感的に分かりにくいかもしれないが、これも式ツリー経由でSQL文に翻訳されるわけである。

 ちなみに、このWhereメソッドは、LINQ to Objectで使われたWhereメソッドとは別物のSystem.Linq.QueryableクラスのWhereメソッドである。このメソッドは以下のシグネチャを持つ。第2引数の型がExpression型であることから、このメソッドが確かに「式ツリー」を受け取っていることが分かるだろう。

Where<TSource>(IQueryable<TSource>, Expression<Func<TSource, Boolean>>)
System.Linq.QueryableクラスのWhereメソッドのシグネチャ

LINQ to SQLのまとめ

 LINQ to SQLは、SQL Serverの知識を持たずにプログラムを記述可能とする魔法のつえではない。SQLの細かい構文を調べるためにドキュメントの迷宮をさまよう必要からは解放されるが、RDBの基礎を学ばずに使えるものではない。

 さらにいえば、LINQ to SQLが常に最も効率的なクエリを生成してくれるわけでもない。例えば、安易なコードを書けば、参照することのない値まで含めて選択するSELECT句が生成されることも珍しくはない。もちろん、それは非効率的なクエリということになる。

 それにもかかわらず、筆者はLINQ to SQLを「救いの神」と見る。その理由は、この構造がアセンブラに対するC言語のはやりとよく似た構造を持っているからだ。

 かつて、パソコンの世界ではBASIC言語とアセンブリ言語(機械語)が2大主流開発言語だった時代がある。そして、この2つを駆逐して主要開発言語の座を獲得したのがC言語である。C言語は機械語の細かい機能を知らずとも効率的なコードを記述できるが、使いこなすためには機械語の基礎知識が必要とされた。何しろ、C言語が持つポインタという機能は、機械語が扱う生のメモリ空間を直接操作するものである。そして、特に初期のCコンパイラにおいては、安易なソース・コードを書けば非効率的な実行ファイルが容易に生成された。それにもかかわらず、C言語は急速に普及した。支払うものに対して得られるものが圧倒的に多かったからだ。

 LINQ to SQLも同様に、支払うものよりも得るものが圧倒的に多いという理由で利用する価値があると考える。つまり、LINQ to SQLを使うためにはRDBの基礎や、LINQ to SQLそのものの使い方を学ぶコストを支払う必要があるとしても、それによって得られるものは極めて大きいと考えられるからだ。また、生成されるSQL文を調べつつ、それを見ながらソース・コードを改善していく作業もありだろう。それだけの手間を費やしても、得られるものの方が多いのではないだろうか。

 取りあえずSQL初心者の立場であれば、ドキュメントの迷宮をさまよいながらゼロから効率的なSQL文のクエリの記述を試みるよりも、LINQ to SQLが生成したSQL文のクエリを元にして改善できる個所を考える方がずっと楽ではないかと思う。


 INDEX
  C# 3.0入門
  最終回 LINQ to SQL/落ち穂拾い
    1.SQL Serverのワナ/LINQ to SQLという突破口
  2.LINQ to SQLのサンプル/LINQ to SQLとメソッド構文/まとめ
    3.部分メソッド定義/C# 3.0コンパイラ/連載の終わりに
 
インデックス・ページヘ  「C# 3.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間