Ver. 3.0
LINQ を用いることで、 IEnumerable や XML、リレーショナルデータベースなど、 様々なデータソースに対して、共通の構文で問い合わせなどの操作を行うことができます。
その中でも、リレーショナルデータベースへの問い合わせを可能とする LINQ to SQL は、 オブジェクト指向プログラミングとリレーショナルデータベースの間の溝(インピーダンスミスマッチ)を埋める技術として、非常に面白いものになっています。
インピーダンスミスマッチ(impedance mismatch)という言葉は、元々は電気工学の言葉で、 直訳するなら「抵抗の不一致」ということになります。 抵抗の異なる素材の間に電磁波を通そうとすると、 境界面で反射が起こって、電気的なエネルギーを効率よく伝達できないんですが、 そういう状況を思い浮かべての比喩表現です。
( ちなみに、電磁波とか言われてもよく分からないという人向けに蛇足的に説明すると、 音の反射も、音波が物質中を伝わる際の抵抗(音響インピーダンス)の異なる物質の境界面で起こります。 兎に角、 物質間で反射が起きてエネルギーがうまく伝わらない状況というのがインピーダンスミスマッチです。 )
要するに、コンセプトの異なる2つの分野を繋ごうとする際に起こる困難をさして、 インピーダンスミスマッチという言葉を使います。
特に近年もっともよく話題に上がるのは、 オブジェクト指向プログラミング(OOP)とリレーショナルデータベース(RDB)の間の不一致で、 O/R インピーダンスミスマッチ(O/R は Object/Relational の略)と呼ばれるものです。
で、LINQ to SQL はこの O/R インピーダンスミスマッチの解決策の1つとして期待されているんですが、 LINQ to SQL を説明する文章の多くは、 「O/R インピーダンスミスマッチを解決します」とは書いてあっても、 そもそも O/R インピーダンスミスマッチがどういうものなのかまでは書いていなかったりします。 要するに、 OOP と RDB に関する知識がそこそこあって、その違いを認識していることが前提のものが多いです。
対して、ここでは、 OOP と RDB でそれぞれどういうコンセプトでデータを表すかを説明した上で、 LINQ to SQL がどうやってその間を繋いでいるかを説明したいと思います。
ここでは例として、本のシリーズと作家のデータベースを考えます。 この例では、 シリーズは名前と出版社と作者を、 作家は名前・誕生日・ウェブサイト URL を持つものとします。
作家はいくつかのシリーズを持っていますし、シリーズにはそれぞれ作者がいるわけですが、 まあまず、最初はその両者の間の関連性はおいておいて別々に考えます。 (この段階では OOP と RDB の間の差は顕著には現れません。)
まず、OOP の例として C# のコードを挙げますが、 C# の場合、以下のようなクラスを定義して、 List や Dictionary を使ってデータを格納します。
class Author { public string Name; public DateTime Birthday; public string Url; } class Series { public string Name; public string Publisher; }
List<Author> authors = new List<Author> { new Author { Name = "赤松健", Birthday = new DateTime(1968, 07, 05), Url = "http://www.ailove.net/main.html" }, new Author { Name = "久米田康治", Birthday = new DateTime(1967, 09, 05), Url = "http://websunday.net/backstage/kumeta.html" }, new Author { Name = "島本和彦", Birthday = new DateTime(1961, 04, 26), Url = "http://simamoto.zenryokutei.com/" }, new Author { Name = "藤田和日郎", Birthday = new DateTime(1964, 05, 24), Url = "http://websunday.net/backstage/fujita.html" }, }; List<Series> series = new List<Series> { new Series { Name = "魔法先生ネギま!", Publisher = "講談社" }, new Series { Name = "ラブひな", Publisher = "講談社" }, new Series { Name = "さよなら絶望先生", Publisher = "講談社" }, new Series { Name = "かってに改蔵", Publisher = "小学館" }, new Series { Name = "アニメ店長", Publisher = "一迅社" }, new Series { Name = "新吼えろペン", Publisher = "小学館" }, new Series { Name = "ゲキトウ", Publisher = "講談社" }, new Series { Name = "からくりサーカス", Publisher = "小学館" }, new Series { Name = "うしおととら", Publisher = "小学館" }, };
一方、RDB では、 以下のように、テーブルとしてデータの構造定義・格納します。
表1: Authors テーブル
| Name | Birthday | Url |
|---|---|---|
| 赤松健 | 1968/07/05 | http://www.ailove.net/main.html |
| 久米田康治 | 1967/09/05 | http://websunday.net/backstage/kumeta.html |
| 島本和彦 | 1961/04/26 | http://simamoto.zenryokutei.com/ |
| 藤田和日郎 | 1964/05/24 | http://websunday.net/backstage/fujita.html |
表2: Series テーブル
| Name | Publisher |
|---|---|
| 魔法先生ネギま! | 講談社 |
| ラブひな | 講談社 |
| さよなら絶望先生 | 講談社 |
| かってに改蔵 | 小学館 |
| アニメ店長 | 一迅社 |
| 新吼えろペン | 小学館 |
| ゲキトウ | 講談社 |
| からくりサーカス | 小学館 |
| うしおととら | 小学館 |
まあ、本当は、出版社の情報も別テーブルに持ちたいところですが、 ここでは話を簡単にするために Series テーブル中に含めています。
最初にも少し触れましたが、 この時点では OOP と RDB には大きな差は生まれません。 見た目こそ違いますが、 いずれも、1行1行データが書かれているだけです。
まあ、前節のように、データテーブルが独立しているうちは OOP と RDB にはそれほど大きな差は生まれません。 問題は、2つのテーブルの関係性を表すときに生じます。
引き続き、作家とシリーズのデータベースの例で説明しましょう。 作家はいくつかのシリーズを持っていますし、シリーズにはそれぞれ作者がいます。
OOP では、通常、階層的なデータ構造を持っています。 作家が複数のシリーズを持っているなら、作家クラスは以下のように書かれます。
class Author { public string Name; public DateTime Birthday; public string Url; public List<Series> Series; }
また、シリーズに作者があるなら、シリーズクラスは以下のようになります。 (もちろん、本当は1つの本に複数の作者(原作、作画、コンテ構成など)があったりしますが、 ここでは単純化のために、作家は1人だけとします。)
class Series { public string Name; public string Publisher; public Author Author; }
で、例えば、各作家の著作一覧を取得したければ以下のように書きます。
foreach (Author a in authors) { Console.Write("{0}\n", a.Name); foreach (Series s in a.Series) { Console.Write(" - {0}\n", s.Name); } }
また、各シリーズの著者を取得するには以下のようにします。
foreach (Series s in series) { Console.Write("{0}, {1}\n", s.Name, s.Author.Name); }
一方、RDB では、階層的にデータを持つことはできません。 データ上は、以下のように、ID 情報だけを持っておきます。
表3: Authors テーブル
| Id | Name | Birthday | Url |
|---|---|---|---|
| 1 | 赤松健 | 1968/07/05 | http://www.ailove.net/main.html |
| 2 | 久米田康治 | 1967/09/05 | http://websunday.net/backstage/kumeta.html |
| 3 | 島本和彦 | 1961/04/26 | http://simamoto.zenryokutei.com/ |
| 4 | 藤田和日郎 | 1964/05/24 | http://websunday.net/backstage/fujita.html |
表4: Series テーブル
| Name | Publisher | AuthorId |
|---|---|---|
| 魔法先生ネギま! | 講談社 | 1 |
| ラブひな | 講談社 | 1 |
| さよなら絶望先生 | 講談社 | 2 |
| かってに改蔵 | 小学館 | 2 |
| アニメ店長 | 一迅社 | 3 |
| 新吼えろペン | 小学館 | 3 |
| ゲキトウ | 講談社 | 3 |
| からくりサーカス | 小学館 | 4 |
| うしおととら | 小学館 | 4 |
そして、問い合わせの際に、 ID を元に2つのテーブルを結合してから所望のデータを取り出します。
例えば、OOP の例と同じく、 各作家のシリーズ一覧を取得したければ、以下のような SQL 文を書きます。
SELECT [t0].[Name] AS [AuthorName], [t1].[Name] FROM [dbo].[Authors] AS [t0], [dbo].[Series] AS [t1] WHERE [t1].[AuthorId] = [t0].[Id]
SELECT [t1].[Name] AS [AuthorName], [t0].[Name] FROM [dbo].[Series] AS [t0] INNER JOIN [dbo].[Authors] AS [t1] ON [t1].[Id] = [t0].[AuthorId]
1つ目の SQL 文章は、2つのテーブルから WHERE 句で ID の一致する物だけを選んでいます。 2つ目のものは、INNER JOIN 句で2つのテーブルを結合しています。
このように、OOP と RDB には、階層的データ構造とテーブル結合という方法論の差があります。
前節のおさらいになりますが、 OOP では階層的データ構造を、
class Author { public string Name; public DateTime Birthday; public string Url; public List<Series> Series; }
RDB ではテーブル結合という方法を用いて関連性のあるデータにアクセスします。
SELECT [t1].[Name] AS [AuthorName], [t0].[Name] FROM [dbo].[Series] AS [t0] INNER JOIN [dbo].[Authors] AS [t1] ON [t1].[Id] = [t0].[AuthorId]
近年、プログラミング言語からリレーショナルデータベースにアクセスする機会が増え、 この OOP と RDB の方法論の差、 すなわち、このページの冒頭で話をした O/R インピーダンスミスマッチを解消したいという要望が強くなっています。 下位のデータベースを意識せず、普通に OOP の作法でプログラミングするだけで RBD とのデータのやり取りがしたいわけです。 で、それが LINQ to SQL の目指すところとなります。
LINQ to SQL がどのようにして O/R インピーダンスミスマッチを解消しているのか、 詳しい手順の説明は 「[雑記] LINQ to SQL 実践編」 に譲って、 ここではまずは概要を説明します。
まず、データベース上のテーブルに相当するクラス(これをエンティティ(entity: 本質、実体)と呼びます)を定義します。 エンティティクラスには Table 属性を、 エンティティのメンバー(テーブルの列に相当)には Column 属性を付けます。
例えば、前節までの説明で使った作家・シリーズテーブルの場合、 以下のような感じになります。
using System.Data.Linq.Mapping; [Table(Name = "Authors")] public class Author { [Column(AutoSync = AutoSync.OnInsert, IsPrimaryKey = true, IsDbGenerated = true)] public int Id; [Column] public string Name; [Column] public DateTime? Birthday; [Column] public string Url; } [Table(Name = "Series")] public class Series { [Column(AutoSync = AutoSync.OnInsert, IsPrimaryKey = true, IsDbGenerated = true)] public int Id; [Column] public string Name; [Column] public int AuthorId; }
LINQ to SQL では、これらの属性をみて、 クラスのメンバーアクセスを RDB への SQL 問い合わせに変換します。
この例では、Author と Series の Id の Column 属性には、AutoSync などのパラメータが付いています。 これは、「データベース側で自動的に生成されるユニークな ID で、自動的にデータベース側と同期します」という意味になります。 通常、重複のない一意的な整数値がほしい場合、このような設定をします。
次に、データベースサーバに接続するためのクラス(DataContext)を作ります。
先ほどの Author と Series テーブルにアクセスするためには、 以下のようなクラスを作ります。
using System.Data.Linq; public class ComicDataContext : DataContext { public ComicDataContext(string connectionString) : base(connectionString) { } public Table<Author> Author; public Table<Series> Series; }
DataContext を継承するクラスに、Table 型のメンバーを書くだけです。 各 Table の初期化は、親クラスの DataContext のコンストラクタ中で、 リフレクション機能を使って行われます。 なので、最低限、コンストラクタと Table メンバーだけ書けば LINQ to SQL で利用可能です。
例えば、Author テーブルに対するクエリは以下のように書けます。
var db = new ComicDataContext(ConnectionString); var q = from a in db.Author where a.Name == "島本和彦" || a.Name == "赤松健" select a; foreach (var a in q) { Console.Write("{0}, {1:yyyy/M/d}, {2}\n", a.Name, a.Birthday, a.Url); }
赤松健, 1968/7/5, http://www.ailove.net/main.html 島本和彦, 1961/4/26, http://simamoto.zenryokutei.com/
Table クラスは IQueryable インターフェースを実装していて、 このクエリ式は IQueryable に対する操作になります。 「クエリ式」 で説明したように、 C# 3.0 のクエリ式は、実際には Select や Where などといった名前のメソッド(あるいは拡張メソッド)呼び出しになります。 この例の場合、以下のメソッド呼び出しと同じ意味です。
IQueryable<Author> q = db.Author.Where( a => a.Name == "島本和彦" || a.Name == "赤松健");
IQueryable は、このようなメソッド呼び出しを通して、 SQL 文を生成し、データベースサーバに問い合わせを行います。 ちなみに、IQueryable を ToString すると、生成された SQL 文を確認することができます。
var q = from a in db.Author where a.Name == "島本和彦" || a.Name == "赤松健" select a; Console.Write("{0}\n", q);
SELECT [t0].[Id], [t0].[Name], [t0].[Kana], [t0].[Birthday], [t0].[Url] FROM [dbo].[Authors] AS [t0] WHERE ([t0].[Name] = @p0) OR ([t0].[Name] = @p1)
それでは次に、 Author と Series テーブルの間の関連性を記述します。 やり方は簡単で、以下のように、Association 属性の付いたメンバーを定義するだけです。
using System.Data.Linq.Mapping; [Table(Name = "Authors")] public class Author { [Column(AutoSync = AutoSync.OnInsert, IsPrimaryKey = true, IsDbGenerated = true)] public int Id; [Column] public string Name; [Column] public DateTime? Birthday; [Column] public string Url; [Association(OtherKey = "AuthorId")] public EntitySet<Series> Series; } [Table(Name = "Series")] public class Series { [Column(AutoSync = AutoSync.OnInsert, IsPrimaryKey = true, IsDbGenerated = true)] public int Id; [Column] public string Name; [Column] public int AuthorId; [Association(Storage = "_Author", ThisKey = "AuthorId")] public Author Author { get { return this._Author.Entity; } set { this._Author.Entity = value; } } private EntityRef<Author> _Author; }
EntitySet や EntityRef クラスのデータへのアクセスは、 必要になったときに初めてデータベースサーバに問い合わせを行います。 すなわち、初回アクセス時にのみサーバからデータをロードし、 取得済みのデータがすでにあるならその値を返します。
Author は複数の Series を持っているので EntitySet、 Series は(今回の例では)ただ1人の Author を持つので EntityRef を使います。
OOP における階層的データ構造は、 RDB ではテーブル結合で行うわけですが、 結合の際のキーを Association 属性のパラメータに与えます。 この例では、 Author の主キー(IsPrimaryKey = true)である Id と Series の AuthorId の値によってテーブルを結合するので、 Author 側には OtherKey = "AuthorId" を、 Series 側には ThisKey = "AuthorId" を指定します。
これで、Author.Series や Series.Author の値が必要になった際に、 自動的にテーブル結合を行うような SQL 文が生成されます。
var db = new ComicDataContext(ConnectionString); var q = from s in db.Series where s.Name.Contains("先生") select new { Title = s.Name, Author = s.Author.Name }; foreach (var s in q) { Console.Write("{0}, {1}\n", s.Title, s.Author); } Console.Write("\n{0}\n", q);
魔法先生ネギま!, 赤松健 さよなら絶望先生, 久米田康治 SELECT [t0].[Name], [t1].[Name] AS [Name2] FROM [dbo].[Series] AS [t0] INNER JOIN [dbo].[Authors] AS [t1] ON [t1].[Id] = [t0].[AuthorId] WHERE [t0].[Name] LIKE @p0
次章の 「[雑記] LINQ to SQL 実践編」 では、 Visual Studio を使ってデータベースのテーブル定義 → LINQ to SQL クラス化 → クエリ式を使ったプログラム作成という一連の作業を具体的に説明します。