目次

概要

Ver. 3.0

[雑記] O/R インピーダンスミスマッチ」では、オブジェクト指向とリレーショナルデータベースの間のデータ構造の差、 階層構造とテーブル結合の差について話をしました。 これに加えて、オブジェクト指向独特の概念として、クラスの「継承」というものがあります。

ここでは、Entity Framework を使って、クラスの継承階層をテーブルにマッピングする例を紹介します。

クラスの継承階層

継承」や「多態性」で説明したように、オブジェクト指向の基本的な概念の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 によって作られるデータベースは以下のようになります。

TablePerHierarchyContext によって作られるデータベース
TablePerHierarchyContext によって作られるデータベース

これに対して、 TablePerTypeContext によって作られるデータベースは以下のようになります。

TablePerHierarchyContext によって作られるデータベース
TablePerHierarchyContext によって作られるデータベース

データの参照

作成したデータを参照してみましょう。

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 版

(LINQ to SQL 版)

継承構造を RDB のテーブルで表現

前節で説明したような継承構造を RDB 上で表現するには、 型識別用の情報を格納した列(discriminator: discriminate は「区別・識別する」)を作ります。

引き続き、図形(shape, rectangle, circle)を例にとって説明すると、 例えば、図2か図3のようなテーブルを定義することになります。

継承構造をテーブル化(1)
継承構造をテーブル化(1)

継承構造をテーブル化(2)
継承構造をテーブル化(2)

この例の場合、Type 列が discrimitator になります。

LINQ to SQL における継承構造の O/R マッピング

クラスの継承階層」で説明したクラスの継承構造と、 「継承階層を RDB のテーブルで表現」で説明したテーブル構造を対応付けるため、 LINQ to SQL では、継承構造をもつクラスに InheritanceMapping 属性をつけ、 discriminator にしたいプロパティの Column 属性に IsDiscriminator = true をつけます。

例えば、図2のようにしたい場合には、以下のようなコードを書きます。

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;

namespace LinqToSqlSample
{
  public sealed class ShapeDataContext : DataContext
  {
    public ShapeDataContext(string connection) : base(connection) {}

    public Table<Shape> Shapes;
  }

  public enum ShapeType : int
  {
    Invalid,
    Rectangle,
    Circle,
  }

  [Table(Name = "Shape")]
  [InheritanceMapping(Code = "0", Type = typeof(Shape), IsDefault = true)]
  [InheritanceMapping(Code = "1", Type = typeof(Rectangle))]
  [InheritanceMapping(Code = "2", Type = typeof(Circle))]
  public class Shape
  {
    [Column(AutoSync = AutoSync.OnInsert, IsDbGenerated = true, IsPrimaryKey = true)]
    public int ID;

    [Column(IsDiscriminator = true)]
    public ShapeType Type;

    public virtual float GetArea() { return 0; }
  }

  public class Rectangle : Shape
  {
    [Column(CanBeNull = true)]
    public float Width;

    [Column(CanBeNull = true)]
    public float Height;

    public override float GetArea()
    {
      return this.Width * this.Height;
    }
  }

  public class Circle : Shape
  {
    [Column(CanBeNull = true)]
    public float Radius;

    public override float GetArea()
    {
      return (float)(Math.PI * this.Radius * this.Radius);
    }
  }
}

あるいは、図3のようにしたい場合には、以下のようなコードを書きます。

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;

namespace LinqToSqlSample
{
  public sealed class ShapeDataContext : DataContext
  {
    public ShapeDataContext(string connection) : base(connection) {}

    public Table<Shape> Shapes;
  }

  public enum ShapeType : int
  {
    Invalid,
    Rectangle,
    Circle,
  }

  [Table(Name = "Shape")]
  [InheritanceMapping(Code = "0", Type = typeof(Shape), IsDefault = true)]
  [InheritanceMapping(Code = "1", Type = typeof(Rectangle))]
  [InheritanceMapping(Code = "2", Type = typeof(Circle))]
  public class Shape
  {
    [Column(AutoSync = AutoSync.OnInsert, IsDbGenerated = true, IsPrimaryKey = true)]
    public int ID;

    [Column(IsDiscriminator = true)]
    public ShapeType Type;

    [Column(Name = "a", CanBeNull = true)]
    protected float a;

    [Column(Name = "b", CanBeNull = true)]
    protected float b;

    public virtual float GetArea() { return 0; }
  }

  public class Rectangle : Shape
  {
    public float Width
    {
      get { return this.a; }
      set { this.a = value; }
    }

    public float Height
    {
      get { return this.b; }
      set { this.b = value; }
    }

    public override float GetArea()
    {
      return this.Width * this.Height;
    }
  }

  public class Circle : Shape
  {
    public float Radius
    {
      get { return this.a; }
      set { this.a = value; }
    }

    public override float GetArea()
    {
      return (float)(Math.PI * this.Radius * this.Radius);
    }
  }
}

要するに、IsDiscriminator = true のついた列の値の基づいて、 InheritanceMapping 属性の情報を元にどの派生クラスになるか決定されます。

以下のようなコードで動作確認ができます。

var db = new ShapeDataContext("shape.sdf");

if (!db.DatabaseExists())
{
  db.CreateDatabase();

  db.Shapes.InsertOnSubmit(new Rectangle { Width = 2, Height = 3 });
  db.Shapes.InsertOnSubmit(new Circle { Radius = 1 });
  db.Shapes.InsertOnSubmit(new Rectangle { Width = 1, Height = 2 });
  db.Shapes.InsertOnSubmit(new Circle { Radius = 2 });
  db.Shapes.InsertOnSubmit(new Rectangle { Width = 2, Height = 1 });
  db.Shapes.InsertOnSubmit(new Circle { Radius = 0.5F });
  db.Shapes.InsertOnSubmit(new Rectangle { Width = 0.5F, Height = 0.5F });
  db.Shapes.InsertOnSubmit(new Circle { Radius = 0.1F });

  db.SubmitChanges();
}

foreach (var s in db.Shapes)
{
  Console.Write("{0}, area = {1}\n", s.Type, s.GetArea());
}
Rectangle, area = 6
Circle, area = 3.141593
Rectangle, area = 2
Circle, area = 12.56637
Rectangle, area = 2
Circle, area = 0.7853982
Rectangle, area = 0.25
Circle, area = 0.03141593

更新履歴

ブログ