Ver. 3.0
「[雑記] O/R インピーダンスミスマッチ(LINQ to SQL の概念説明)」 では、オブジェクト指向とリレーショナルデータベースの間のデータ構造の差、 階層構造とテーブル結合の差について話をしました。 これに加えて、オブジェクト指向独特の概念として、クラスの継承というものがあります。
ここでは、LINQ to SQL がどのようにして、クラスの継承構造を RDB にマッピングしているかを説明します。
「継承」 や 「多態性」 で説明したように、オブジェクト指向の基本的な概念の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 上で表現するには、 型識別用の情報を格納した列(discriminator: discriminate は「区別・識別する」)を作ります。
引き続き、図形(shape, rectangle, circle)を例にとって説明すると、 例えば、図2か図3のようなテーブルを定義することになります。
この例の場合、Type 列が discrimitator になります。
「クラスの継承構造」
で説明したクラスの継承構造と、
「継承構造を 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