++C++; // 未確認飛行 C MVVMパターンを使ったクロス・ターゲット開発 C#たんと学ぶ わりと硬派なソフトウェア開発講座 番外編「Windows Phone 7」

Top総合 目次C# によるプログラミング入門

[雑記] O/R インピーダンスミスマッチ

このエントリーをはてなブックマークに追加

目次

キーワード

概要

Ver. 3.0

LINQ を用いることで、 IEnumerable や XML、リレーショナルデータベースなど、 様々なデータソースに対して、共通の構文で問い合わせなどの操作を行うことができます。

その中でも、リレーショナルデータベースへの問い合わせを可能とする LINQ to SQL や Entity Framework は、 オブジェクト指向プログラミングとリレーショナルデータベースの間の溝(インピーダンスミスマッチ)を埋める技術として、非常に面白いものになっています。

O/R インピーダンスミスマッチ

インピーダンスミスマッチ(impedance mismatch)という言葉は、元々は電気工学の言葉で、 直訳するなら「抵抗の不一致」ということになります。 抵抗の異なる素材の間に電磁波を通そうとすると、 境界面で反射が起こって、電気的なエネルギーを効率よく伝達できないんですが、 そういう状況を思い浮かべての比喩表現です。

図1: 異なる素材間の抵抗の不一致

( ちなみに、電磁波とか言われてもよく分からないという人向けに蛇足的に説明すると、 音の反射も、音波が物質中を伝わる際の抵抗(音響インピーダンス)の異なる物質の境界面で起こります。 兎に角、 物質間で反射が起きてエネルギーがうまく伝わらない状況というのがインピーダンスミスマッチです。 )

要するに、コンセプトの異なる2つの分野を繋ごうとする際に起こる困難をさして、 インピーダンスミスマッチという言葉を使います。

特に近年もっともよく話題に上がるのは、 オブジェクト指向プログラミング(OOP)とリレーショナルデータベース(RDB)の間の不一致で、 O/R インピーダンスミスマッチ(O/R は Object/Relational の略)と呼ばれるものです。

ここでは、 OOP と RDB でそれぞれどういうコンセプトでデータを表すかを説明した上で、 どういうミスマッチがあるのか、 LINQ でどう解決されるのかを説明したいと思います。

OOP のクラスと RDB のテーブル

ここでは例として、本のシリーズと作家のデータベースを考えます。 この例では、 シリーズは名前と出版社と作者を、 作家は名前・誕生日・ウェブサイト URL を持つものとします。

作家はいくつかのシリーズを持っていますし、シリーズにはそれぞれ作者がいるわけですが、 まあまず、最初はその両者の間の関連性はおいておいて別々に考えます。 (この段階では OOP と RDB の間の差は顕著には現れません。)

OOP(クラス)

まず、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(テーブル)

一方、RDB では、 以下のように、テーブルとしてデータの構造定義・格納します。

表1: Authors テーブル

NameBirthdayUrl
赤松健1968/07/05http://www.ailove.net/main.html
久米田康治1967/09/05http://websunday.net/backstage/kumeta.html
島本和彦1961/04/26http://simamoto.zenryokutei.com/
藤田和日郎1964/05/24http://websunday.net/backstage/fujita.html

表2: Series テーブル

NamePublisher
魔法先生ネギま!講談社
ラブひな講談社
さよなら絶望先生講談社
かってに改蔵小学館
アニメ店長一迅社
新吼えろペン小学館
ゲキトウ講談社
からくりサーカス小学館
うしおととら小学館

まあ、本当は、出版社の情報も別テーブルに持ちたいところですが、 ここでは話を簡単にするために Series テーブル中に含めています。

最初にも少し触れましたが、 この時点では OOP と RDB には大きな差は生まれません。 見た目こそ違いますが、 いずれも、1行1行データが書かれているだけです。

OOP の階層的データ構造と RDB のテーブル結合

まあ、前節のように、データテーブルが独立しているうちは OOP と RDB にはそれほど大きな差は生まれません。 問題は、2つのテーブルの関係性を表すときに生じます。

引き続き、作家とシリーズのデータベースの例で説明しましょう。 作家はいくつかのシリーズを持っていますし、シリーズにはそれぞれ作者がいます。

OOP(クラスの階層化)

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(テーブル間の関係)

一方、RDB では、階層的にデータを持つことはできません。 データ上は、以下のように、ID 情報(Series テーブルの Author_Id 列)だけを持っておきます。

表3: Authors テーブル

IdNameBirthdayUrl
1赤松健1968/07/05http://www.ailove.net/main.html
2久米田康治1967/09/05http://websunday.net/backstage/kumeta.html
3島本和彦1961/04/26http://simamoto.zenryokutei.com/
4藤田和日郎1964/05/24http://websunday.net/backstage/fujita.html

表4: Series テーブル

NamePublisherAuthor_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 には、階層的データ構造とテーブル結合という方法論の差があります。

O/R マッパー

前節のおさらいになりますが、 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は、様々な種類のデータに対して、統一的な問い合わせを行うための仕組みです。 この「さまざまな種類のデータ」にはデータベースも含まれています。 すなわち、O/R マッピング用の LINQ が存在します。

歴史的背景から、LINQ O/R マッパーには、.NET Framework 標準搭載のものだけで、2系統あります。

  • LINQ to SQL: いわば、LINQ(IQueryable)の参考実装(C# コンパイラー チームが作成したもの)で、ある意味「簡易実装」な O/R マッパー。 今後の機能改善はされない予定。
  • ADO.NET Entity Framework: ちゃんとデータベース フレームワーク(ADO.NET)チームが開発している O/R マッパー。 以下、単に Entity Framework と表記します。

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 がベースとなっています。

LINQ の利用

エンティティ

まず、データベース上のテーブルに相当するクラス(これをエンティティ(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 版)

IQueryable とクエリ式

例えば、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'%先生%'

まとめ

  • オブジェクト指向プログラミング(OOP)言語とリレーショナルデータベース(RDB)の間には、
    • OOP: 階層的データ構造
    • RDB: テーブル結合
    という方法論の差があります(O/R インピーダンスミスマッチ)。
  • Entity Framework を利用する際に使う物:
    • エンティティ: データベースのテーブルに相当するクラスを定義。
    • 関連性: 他のエンティティを参照するプロパティ(ナビゲーション プロパティ)を定義することでテーブル間の関係性を定義。
    • データベース コンテキスト: 定義したエンティティを使ってデータベースのテーブル生成/参照するためのクラスを定義。

 ⊞  (LINQ to SQL 版)

次章の 「[雑記] LINQ to SQL 実践編」 では、 Visual Studio を使ってデータベースのテーブル定義 → LINQ to SQL クラス化 → クエリ式を使ったプログラム作成という一連の作業を具体的に説明します。

[お問い合わせ](q)