概要
Ver. 3.0
「[雑記] O/R インピーダンスミスマッチ」では、オブジェクト指向とリレーショナルデータベースの間のデータ構造の差、 階層構造とテーブル結合の差について話をしました。 これに加えて、オブジェクト指向独特の概念として、クラスの「継承」というものがあります。
ここでは、Entity Framework を使って、クラスの継承階層をテーブルにマッピングする例を紹介します。
- サンプル プログラム:EntityFrameworkSample.zip
クラスの継承階層
「継承」や「多態性」で説明したように、オブジェクト指向の基本的な概念の1つに継承というものがあります。
例えば、矩形や円などの図形を考えたとき、これらの図形には「面積を求められる」という共通の性質があります。 このような場合、共通の性質を基底クラスにまとめてしまうのがオブジェクト指向のやり方です。 この様子を図示したものと、サンプルコードを以下に示します。
public abstract class Shape
{
public abstract float GetArea();
}
public class Rectangle : Shape
{
public float Width { get; set; }
public float Height { get; set; }
public override float GetArea()
{
return this.Width * this.Height;
}
}
public class Circle : Shape
{
public float Radius { get; set; }
public override float GetArea()
{
return (float)(Math.PI * this.Radius * this.Radius);
}
}
継承階層を RDB のテーブルで表現
前節で説明したような継承階層を RDB 上で表現するにはいくつか方法がありますが、 ここでは2つほど紹介します。
テーブルの共有
1つ目の方法は、クラスの継承階層で1つのテーブルを共有します。 (table per hierarchy と呼びます。) テーブルの各行がどの型かを判別するための列(discriminator: discriminate は「区別・識別する」)を作ります。
シンプルですが、型によって使われない列が出るという問題もあります。
別テーブルを作成
もう1つは、クラスごとに別のテーブルを作ります。 (table per type と呼びます。)
共有テーブルのように無駄な列ができることはありませんが、 複数のテーブルが見かけ上、1つのテーブルであるかのように見せる仕組みが必要になります。
Entity Framework における継承構造の O/R マッピング
Entity Framework では、データベース コンテキストを作る際に、 DbContext クラスの OnModelCreating メソッドをオーバーライドすることで、 継承階層のテーブル化方法をカスタマイズできます。
table per hierarchy にしたい場合は以下のように書きます。
public class TablePerHierarchyContext : DbContext
{
public DbSet<Shape> Shapes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Shape>()
.Map<Rectangle>(x => x.Requires("type").HasValue("R"))
.Map<Circle>(x => x.Requires("type").HasValue("C"));
}
}
一方、 table per type にしたい場合は以下のように書きます。
public class TablePerTypeContext : DbContext
{
public DbSet<Shape> Shapes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Rectangle>().ToTable("Rectangle");
modelBuilder.Entity<Circle>().ToTable("Circle");
}
}
サンプル データ作成
作成した2つのデータベース コンテキストを使って、サンプル データを作成してみましょう。
private static void Create()
{
using (var db = new TablePerHierarchyContext())
{
Create(db.Shapes);
db.SaveChanges();
}
// ↑↓見ての通り、コンテキストが違う以外は全く一緒。
using (var db = new TablePerTypeContext())
{
Create(db.Shapes);
db.SaveChanges();
}
}
private static void Create(System.Data.Entity.DbSet<Shape> shapes)
{
shapes.Add(new Rectangle { Width = 10, Height = 20 });
shapes.Add(new Rectangle { Width = 15, Height = 12 });
shapes.Add(new Circle { Radius = 1.5f });
shapes.Add(new Circle { Radius = 3 });
}
TablePerHierarchyContext によって作られるデータベースは以下のようになります。
これに対して、 TablePerTypeContext によって作られるデータベースは以下のようになります。
データの参照
作成したデータを参照してみましょう。
private static void Query()
{
using (var db = new TablePerTypeContext())
{
Query(db.Shapes);
}
// ↑↓見ての通り、コンテキストが違う以外は全く一緒。
using (var db = new TablePerHierarchyContext())
{
Query(db.Shapes);
}
}
private static void Query(System.Data.Entity.DbSet<Shape> shapes)
{
foreach (var x in shapes)
{
Console.WriteLine("{0}: {1}", x.GetType().Name, x.GetArea());
}
}
Table Per Hierarchy Rectangle: 200 Rectangle: 180 Circle: 7.068583 Circle: 28.27433 Table Per Type Circle: 7.068583 Circle: 28.27433 Rectangle: 200 Rectangle: 180
LINQ to SQL 版