Top ≫ 総合 目次 ≫ C# によるプログラミング入門
Ver. 3.0
LINQ を用いることで、 IEnumerable や XML、リレーショナルデータベースなど、 様々なデータソースに対して、共通の構文で問い合わせなどの操作を行うことができます。
その中でも、リレーショナルデータベースへの問い合わせを可能とする LINQ to SQL や Entity Framework は、 オブジェクト指向プログラミングとリレーショナルデータベースの間の溝(インピーダンスミスマッチ)を埋める技術として、非常に面白いものになっています。
インピーダンスミスマッチ(impedance mismatch)という言葉は、元々は電気工学の言葉で、 直訳するなら「抵抗の不一致」ということになります。 抵抗の異なる素材の間に電磁波を通そうとすると、 境界面で反射が起こって、電気的なエネルギーを効率よく伝達できないんですが、 そういう状況を思い浮かべての比喩表現です。
( ちなみに、電磁波とか言われてもよく分からないという人向けに蛇足的に説明すると、 音の反射も、音波が物質中を伝わる際の抵抗(音響インピーダンス)の異なる物質の境界面で起こります。 兎に角、 物質間で反射が起きてエネルギーがうまく伝わらない状況というのがインピーダンスミスマッチです。 )
要するに、コンセプトの異なる2つの分野を繋ごうとする際に起こる困難をさして、 インピーダンスミスマッチという言葉を使います。
特に近年もっともよく話題に上がるのは、 オブジェクト指向プログラミング(OOP)とリレーショナルデータベース(RDB)の間の不一致で、 O/R インピーダンスミスマッチ(O/R は Object/Relational の略)と呼ばれるものです。
ここでは、 OOP と RDB でそれぞれどういうコンセプトでデータを表すかを説明した上で、 どういうミスマッチがあるのか、 LINQ でどう解決されるのかを説明したいと思います。
ここでは例として、本のシリーズと作家のデータベースを考えます。 この例では、 シリーズは名前と出版社と作者を、 作家は名前・誕生日・ウェブサイト 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; }
で、例えば、各作家の著作一覧を取得したければ以下のように書きます。 階層的にデータを取得するために、2重ループなどを書きます。
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 情報(Series テーブルの Author_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 | Author_Id |
|---|---|---|
| 魔法先生ネギま! | 講談社 | 1 |
| ラブひな | 講談社 | 1 |
| さよなら絶望先生 | 講談社 | 2 |
| かってに改蔵 | 小学館 | 2 |
| アニメ店長 | 一迅社 | 3 |
| 新吼えろペン | 小学館 | 3 |
| ゲキトウ | 講談社 | 3 |
| からくりサーカス | 小学館 | 4 |
| うしおととら | 小学館 | 4 |
そして、問い合わせの際に、 ID を元に2つのテーブルを結合してから所望のデータを取り出します。
例えば、OOP の例と同じく、 各作家のシリーズ一覧を取得したければ、以下のような SQL 文を書きます。
SELECT [a].[Name] AS [AuthorName], [s].[Name] FROM [Authors] AS [a] INNER JOIN [Series] AS [s] ON [a].[Id] = [s].[Author_Id]
このように、OOP と RDB には、階層的データ構造とテーブル結合という方法論の差があります。
前節のおさらいになりますが、 OOP では階層的データ構造を、
class Author { public string Name; public DateTime Birthday; public string Url; public List<Series> Series; }
RDB ではテーブル結合という方法を用いて関連性のあるデータにアクセスします。
SELECT [a].[Name] AS [AuthorName], [s].[Name] FROM [Authors] AS [a] INNER JOIN [Series] AS [s] ON [a].[Id] = [s].[Author_Id]
近年、プログラミング言語からリレーショナルデータベースにアクセスする機会が増え、 この OOP と RDB の方法論の差、 すなわち、このページの冒頭で話をした O/R インピーダンスミスマッチを解消したいという要望が強くなっています。 下位のデータベースを意識せず、普通に OOP の作法でプログラミングするだけで RBD とのデータのやり取りがしたいわけです。
このような、OOP の作法でデータベース アクセスするための仕組みを O/R マッパー(O/R mapper)と呼びます。
LINQは、様々な種類のデータに対して、統一的な問い合わせを行うための仕組みです。 この「さまざまな種類のデータ」にはデータベースも含まれています。 すなわち、O/R マッピング用の LINQ が存在します。
歴史的背景から、LINQ O/R マッパーには、.NET Framework 標準搭載のものだけで、2系統あります。
LINQ to SQL の方が成熟が早かった(LINQ 自体と同時に開発していたので当然)ため、 当初は LINQ to SQL の方がよく利用されていました。 今(※2011年執筆)では Entity Framework もだいぶ成熟しているので、 こちらを使うべきでしょう。
ただし、LINQ to SQL の方が仕組みがシンプルな分、移植がしやすく、 Entity Framework が使えない環境でも LINQ to SQL なら使える(移植できる)ということもあります。 例えば、Windows Phone 7 向けの O/R マッパーは LINQ to SQL がベースとなっています。
まず、データベース上のテーブルに相当するクラス(これをエンティティ(entity: 本質、実体)と呼びます)を定義します。
※以下、ADO.NET Entity Framework 4.1(2011年時点の最新版)を使った説明をします。 (「過去の履歴」として、LINQ to SQL 版の説明も残してあります。)
Entity Framework では、何の変哲もないただのクラスを使ってデータベースのテーブルを生成/参照できます。
前節から引き続き、作家・シリーズ テーブルを例に取って説明しましょう。 まず、テーブル間の関係を抜きにすると、以下のような感じになります。
using System.ComponentModel.DataAnnotations; using System.Collections.Generic; namespace CodeFirst.Models { public class Author { public int Id { get; set; } [Required] [StringLength(100)] public string Name { get; set; } public DateTime? Birthday { get; set; } [StringLength(512)] public string Url { get; set; } } public class Series { public int Id { get; set; } [Required] [StringLength(512)] public string Name { get; set; } } }
次に、 定義したエンティティ クラスを使ってデータベースを生成/テーブル参照するためのクラスを作ります。 Entity Framework では、以下のように、DbContext クラスを継承したクラスを作ります。
using System.Data.Entity; namespace CodeFirst.Models { public class ComicDatabase : DbContext { public DbSet<Author> Authors { get; set; } public DbSet<Series> Series { get; set; } } }
⊞ (LINQ to SQL 版)
例えば、Author テーブルに対するクエリは以下のように書けます。
using (var db = new ComicDatabase()) { var q = from a in db.Authors 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/
⊞ (LINQ to SQL 版)
この時、クエリ式の結果(この例では変数 q)の型は IQueryable インターフェイスになります。
IQueryable は、このようなクエリ式から SQL 文を生成し、データベースサーバに問い合わせを行います。 ちなみに、IQueryable を ToString すると、生成された SQL 文を確認することができます。
var q = from a in db.Author where a.Name == "島本和彦" || a.Name == "赤松健" select a; Console.WriteLine(q.ToString());
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Kana] AS [Kana], [Extent1].[Birthday] AS [Birthday], [Extent1].[Url] AS [Url] FROM [dbo].[Authors] AS [Extent1] WHERE [Extent1].[Name] IN (N'島本和彦',N'赤松健')
それでは次に、 Author と Series エンティティ間の関係性を記述します。
Entity Framework を使うと、ただ単に他のエンティティを参照するプロパティを定義するだけで、 データベース テーブルの関係性を表現できます。 例えば、先ほどの Author / Series クラスに以下のような修正を加えます。
public class Author
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
public DateTime? Birthday { get; set; }
[StringLength(512)]
public string Url { get; set; }
public virtual IList<Series> Series { get; set; }
}
public class Series
{
public int Id { get; set; }
[Required]
[StringLength(512)]
public string Name { get; set; }
public Author Author { get; set; }
}
このような、エンティティ間の参照関係を表すプロパティをナビゲーション プロパティ(navigation property)と呼びます。 ナビゲーション プロパティは、データベース上は ID 情報だけ記録され、 参照時に適宜、テーブルの JOIN が行われます。
⊞ (LINQ to SQL 版)
これで、Author.Series や Series.Author の値が必要になった際に、 自動的にテーブル結合を行うような SQL 文が生成されます。
using (var db = new ComicDatabase()) { 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 1 AS [C1], [Extent1].[Name] AS [Name], [Extent2].[Name] AS [Name1] FROM [dbo].[Series] AS [Extent1] LEFT OUTER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[Author_Id] = [Extent2].[Id] WHERE [Extent1].[Name] LIKE N'%先生%'
⊞ (LINQ to SQL 版)
次章の 「[雑記] LINQ to SQL 実践編」 では、 Visual Studio を使ってデータベースのテーブル定義 → LINQ to SQL クラス化 → クエリ式を使ったプログラム作成という一連の作業を具体的に説明します。