tail.プレフィクスがいらない子扱いされている件
C♯というか、.NETのExpressionで末尾最適化がかかった形のモノが出来ないかどうか調べていて詰まったので*1F♯の末尾再帰ってコンパイルの時どうなってるのか調べてみたら表題の通りで。
最も今回の例はとても単純なサンプルとなっているので、条件によっては結果が異なる可能性も十分に考えられるので、その点はご留意の程。
まず最初に以下のような、末尾再帰じゃない再帰関数を作ってみた。
//末尾再帰じゃない再帰関数 let rec recursiveSum n= if n=0 then 0 else n+(recursiveSum (n-1));;
こいつをコンパイルして、ILを見たところ、以下のようなモノが出来上がっていた。
.method public static int32 recursiveSum ( int32 n ) cil managed { // Method begins at RVA 0x2074 // Code size 21 (0x15) .maxstack 5 IL_0000: nop IL_0001: ldarg.0 IL_0002: brtrue.s IL_0006 IL_0004: br.s IL_0008 IL_0006: br.s IL_000a IL_0008: ldc.i4.0 IL_0009: ret IL_000a: ldarg.0 IL_000b: ldarg.0 IL_000c: ldc.i4.1 IL_000d: sub IL_000e: call int32 Program::recursiveSum(int32) IL_0013: add IL_0014: ret } // end of method Program::recursiveSum
これを、デコンパイルして、C♯で表現すると以下のようになる。
public static int recursiveSum(int n) { if (n == 0) { return 0; } return n + Program.recursiveSum(n - 1); }
ごく普通に、再帰してる感じ。
他方、末尾再帰させた場合はどうなるだろうかってことで。
//末尾再帰対応版 let rec tailRecursiveSum accum n= if n=0 then accum else tailRecursiveSum (accum+n) (n-1);;
こいつのコンパイル結果は以下の通り。
.method public static int32 tailRecursiveSum ( int32 accum, int32 n ) cil managed { .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) // Method begins at RVA 0x2050 // Code size 22 (0x16) .maxstack 5 // loop start IL_0000: nop IL_0001: ldarg.1 IL_0002: brtrue.s IL_0006 IL_0004: br.s IL_0008 IL_0006: br.s IL_000a IL_0008: ldarg.0 IL_0009: ret IL_000a: ldarg.0 IL_000b: ldarg.1 IL_000c: add IL_000d: ldarg.1 IL_000e: ldc.i4.1 IL_000f: sub IL_0010: starg.s n IL_0012: starg.s accum IL_0014: br.s IL_0000 // end loop } // end of method Program::tailRecursiveSum
頭に付いている、CompilationArgumentCountsAttributeは、どーやら、カリー化指定の為のカスタム属性っぽい。
それよっか、注目すべきは、先の末尾再帰じゃ無い方が、call命令を使って、自分を文字通り“再帰的に”呼び出していたのに対して、末尾再帰版の方は、呼び出しが無くなっている。
これを、C♯でデコンパイルすると、以下の通り。
//属性は省略しています。 public static int tailRecursiveSum(int accum, int n) { while (n != 0) { int arg_12_0 = accum + n; n--; accum = arg_12_0; } return accum; }
調べてみるまで、何となく、tail.プレフィクス付けて再帰してるのかなぁと思っていただけに意外な反面、tail.プレフィクスに関するコトをWebで見てみる限り、こうなるかなぁとも思ってみたり見なかったり*2
意外だったので。メモ書き程度に。。。
Implementation of Enumerable.Cast
中身見たら、えらく感動したので、備忘録的に。
何が凄いか
例外の吐き方がまず秀逸。無駄なコト一切してない。また、二段構えにしてるのも秀逸。
実装
public static IEnumerable<TResult> MyCast<TResult>(this IEnumerable source) { IEnumerable<TResult> enumerable = source as IEnumerable<TResult>; if (enumerable != null) { return enumerable; } if (source == null) { throw new ArgumentNullException("source"); } return CreateEnumerable<TResult>(source); } private static IEnumerable<TResult> CreateEnumerable<TResult>(IEnumerable source) { foreach (var elem in source) { yield return (TResult)elem; } yield break; }
概ねこんな感じ。 キモは
- as演算子は、何をどーしたところで絶対例外をはかないので、まず先行して処理する。
- 結果がnullじゃなきゃ、うまくいったのでそのまま返す。
- 結果がnullなら、sourceがnullの可能性があるのでまずチェックする。
- nullなら、例外を吐く。
- sourceがnullじゃなくて、as評価結果がnullの場合、全体としてのキャストに失敗しているので、自前でIEnumerable
をでっち上げる。 - CreateEnumerableを呼び出すことで実現している。
ExpressionTree内で動的にメンバプロパティ/フィールドを呼び出す方法
前回は、戻り値無しのMemberMethodの動的呼び出しをExpressionTreeの中でしようとしたとき、どーすれば良いのか?がテーマでした。
で、今回は、じゃあProperty/Fieldを呼び出すにゃどーすれば良いのか?がお題目。
dynamicで書いてみた場合
dynamicで今日のお題を書いた場合、以下のような感じになる。
static void SetProperty(dynamic d,int value) { d.Value = value; } static int InvokeDelegate(dynamic d) { return d.Value; }
まぁ、どーってこと無く、Valueプロパティ/フィールドに値を設定したり、取得したりしてるだけ。
Property/Field setterの呼び出し方
こちらは、昨日とやってることはほぼ変わらない。
static Expression<Action<object, int>> SetPropertyExpr() { //こんな感じの式木を作ろうとしてる。 //Expression<Action<dynamic, int>> SetPropertyExpr = (target, arg) => target.Value = arg; var binder = Binder.SetMember(CSharpBinderFlags.ResultDiscarded, "Value", typeof(Program) , new[]{CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None,"target"), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType,"arg")}); var targetExpr = Expression.Parameter(typeof(object), "target"); var paramExpr=Expression.Parameter(typeof(int),"arg"); var dynamicExpr = Expression.MakeDynamic(typeof(Action<CallSite, object, int>), binder, targetExpr, paramExpr); return Expression.Lambda<Action<object, int>>(dynamicExpr,targetExpr, paramExpr); }
違いは、CallSiteBinderを生成するとき、Binder.SetMemberメソッドを使っている部分位の差。
Propety/Field getterの呼び出し方
こちらは、若干事情が変わってくる。なぜなら今までやったことの無かった結果を取得する必要が出てくるため。で、値を取得しようとする場合、1回の操作で値を取得することが出来ない典に注意する必要がある。*1
処理の流れとして、
- object型で戻ってくる値を取得する
- 目的の型に変換する
てな感じで二段構えにする必要がある*2。
で、以下は、そのサンプル
static Expression<Func<object, int>> GetProertyExpr() { //こんな感じの式木を作ろうとしてる。 //Expression<Func<dynamic, int>> GetPropertyExpr = (target) => target.Value; var getBinder = Binder.GetMember(CSharpBinderFlags.None, "Value", typeof(Program), new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, "Target") }); var convBinder = Binder.Convert(CSharpBinderFlags.None, typeof(int), typeof(Program)); var targetExpr = Expression.Parameter(typeof(object), "target"); var getExpr = Expression.Dynamic(getBinder, typeof(object), targetExpr); var convExpr = Expression.Dynamic(convBinder, typeof(int), getExpr); return Expression.Lambda<Func<object, int>>(convExpr, targetExpr); }
このExpressionTreeを簡単に視覚化してみると、以下のような感じになる。
実行する
とりあえず、木が2本出来たので、そいつを動かしてみようってコトで。
サンプルとなるクラスは以下の通り。
//Member fieldに対する操作 class TargetField { public int Value { get; set; } } //Member propertyに対する操作 class TargetProperty { public int Value; }
フィールドとプロパテイに対して、取得・設定が出来るか確かめるため、対象となるクラスを2つ用意した。
続いて、サンプルドライバは以下の通り
static void Main(string[] args) { var setExpr = SetPropertyExpr(); var setContainer_ = setExpr.Compile(); var getExpr = GetProertyExpr(); var getContainer = getExpr.Compile(); var t0 = new TargetField(); var t1 = new TargetProperty(); Console.WriteLine("t0's Value is " + t0.Value); Console.WriteLine("t1's Value is " + t1.Value); Console.WriteLine(); Console.WriteLine(); setContainer_(t0, 100); Console.WriteLine("t0's Value is " + getContainer(t0)); setContainer_(t1, 200); Console.WriteLine("t1's Value is " + getContainer(t1)); }
なんてコト無く、値の設定と取得した値をConsoleへ出力しているだけの簡単なシロモノ。
で、実行結果はいかのとーり
t0's Value is 0 t1's Value is 0 t0's Value is 100 t1's Value is 200
最初の2行は、直接呼び出しで、値を動的操作にて変更した後は、動的に呼び出して表示させている。
これは結構使えそうかもしれないかなとおもってみたり。
ExpressionTree内で動的にメンバメソッドを呼び出す方法
通常のλを使った場合、以下のようなコードはコンパイルエラーになる。
Expression<Action<dynamic, int>> expr = (target, arg) => target.Call(arg);
こんな風に書くと以下のようなコンパイラエラーが発生してそもそもコンパイルすらしてくれない。
式ツリーは、動的な操作を含むことはできません
それじゃ、どーやっても出来ないのか?やる方法はあるのか?ってコトが今回のお題。
結論から言えば、ExpressionTreeを手組していけば一応出来た。一口で動的呼びだしと言っても、それこそ、プロパティに対するモノ、デリゲートに対するInvoke、MemberMethodの呼び出し等々あるわけで、今回は戻り値無しのMemberMethodの呼び出しをしてみた。
じゃ、どー作るかというと、以下の通り
static Expression<Action<object, int>> DynamicCall() { //CallSiteBinderの構築 var binder = Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Call", null, typeof(Program), new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null) }); //パラメータの構築 var targetParamExpr = Expression.Parameter(typeof(object), "target"); var argParamExpr = Expression.Parameter(typeof(int), "arg"); //動的操作の生成 var dynamicInvokeExpr=Expression.MakeDynamic(typeof(Action<CallSite,object,int>), binder, targetParamExpr,argParamExpr); //Lambdaを作成 return Expression.Lambda<Action<object,int>>(dynamicInvokeExpr,targetParamExpr,argParamExpr); }
Expressionクラスには、DynamicメソッドとMakeDyhnamicメソッドがあるけど、今回は引数無しなので、MakeDynamicメソッドを利用して生成した。また、恐らく、戻ってくるDynamicExpressionとは、CallSite相当なんではないかと予想してる。
Expression.Callや、Expression.Invokeが実行するべきEntityをExpression.Invokeなら、Expressionで、Expression.Callなら、MethodInfoで指定するように、Expression.MakeDynamicやExpression.DynamicはCallSiteBinderで指定しているのでは無いかなと思います。
で、これの使い方は以下の通り。
メンバを呼び出す対象となるクラス
class TargetClass0 { public void Call(int arg) { //実際呼び出されたか確認するため Console.WriteLine("TargetClass0 is Called value:" + arg.ToString()); } } class TargetClass1 { public void Call(int arg) { //実際呼び出されたか確認するため Console.WriteLine("TargetClass1 is Called value:" + arg.ToString()); } }
Mainメソッド
static void Main(string[] args) { var Dynamic = DynamicCall().Compile(); Dynamic(new TargetClass0(), 100); Dynamic(new TargetClass1(), 200); }
実行結果
TargetClass0 is Called value:100 TargetClass1 is Called value:200
こんな感じで、実行時に動的呼び出しできる。ちなみに、Call(int arg)が存在しないクラスのインスタンスを引数として指定した場合は、RuntimeBinderExceptionが発生する。
次回は、戻り値を持ったメンバメソッドアクセス当たりをやってみようかと思います。
ExpressionTreeとClojure
コトの始まり
ふつーのDelegateでClojureが実現可能なのは、知ってたし、中でどーなってるのか大体理解してたけど、ExpressionTreeの場合、そもそも出来るのか?出来たとして、どーなるのか?て部分が気になったので何となく調べてみた。
使ったサンプルコード
以下は、利用したサンプルコード、身もふたも無いほどにシンプルw
using System; using System.Linq.Expressions; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { int EncX = 100; Expression<Func<int>> exp = () => EncX; EncX = 200; var body = exp.Compile(); Console.WriteLine("Return value:" + body().ToString()); EncX = 500; Console.WriteLine("Return value:" + body().ToString()); } } }
Outputはいかのとーり
ReturnValue:200 ReturnValue:500
Outputを見ればわかるとおり、Enclojure側の値が変化すれば、ちゃんと追随して変化してることがわかる。
で、ばらしてみた
ばらしてみたところ、予想通り、不可視のprivate classをコンパイラが作成して、EncXをそっち側に逃がしていた。ただ、AnonymousMethodでClojureの構築を行った場合、AnonymousMethod側にも、コンパイラが勝手に、ローカル変数を取って、そいつを参照させるみたいな形だったワケだけど、ExpressionTreeの場合はどーなってるんだろうという疑問が残る。
気になるので、ILで見てみると、以下の通り。(一部抜粋)
.locals init ( [0] class [System.Core]System.Linq.Expressions.Expression`1<class [mscorlib]System.Func`1<int32>> exp [1] class [mscorlib]System.Func`1<int32> body [2] class ConsoleApplication2.Program/<>c__DisplayClass0 CS$<>8__locals1 <-こいつが仕込みの隠しローカル [3] int32 CS$0$0000 ) //ここで隠しローカルを初期化してる。 IL_0000: newobj instance void ConsoleApplication2.Program/<>c__DisplayClass0::.ctor() IL_0005: stloc.2 IL_0006: nop IL_0007: ldloc.2 //ここで、EncX=100;としてる。 IL_0008: ldc.i4.s 100 IL_000a: stfld int32 class ConsoleApplication2.Program/<>c__DisplayClass0::EncX //指定した覚えの無いConstantExpressionを構築してる(仕込みその2) IL_000f: ldloc.2 IL_0010: call class [System.Core]System.Linq.Expressions.ConstantExpression [System.Core]System.Linq.Expressions.Expression::Constant(object) //↑で作ったConstantExpression(中身は自動生成されたクラス)のEncXに相当するフィールドに対するMemberExpressionを構築してる。 IL_0015: ldtoken int32 class ConsoleApplication2.Program/<>c__DisplayClass0::EncX IL_001a: call class [mscorlib]System.Reflection.FieldInfo [mscorlib]System.Reflection.FieldInfo::GetFieldFromHandle(valuetype [mscorlib]System.RuntimeFieldHandle) IL_001f: call class [System.Core]System.Linq.Expressions.MemberExpression [System.Core]System.Linq.Expressions.Expression::Field (class [System.Core]System.Linq.Expressions.Expression, class [mscorlib]System.Reflection.FieldInfo) //以下略 } // End of method Program.Main
うまいことしてるなぁって思った。ConstantでTreeの中に取り込んでしまって、そいつに対するMemberAccessを構築してる。てっきり、Variable当たりでなんかしてるかなぁと思ったけど、そうするより遙かにスマートだし、どっちにしろ、VariableをAssignするとなると、右辺のExpressionが必要になり、なんとかして、取り込む必要がある。となれば当然ConstantExpressionが必要になるので、はなっからVariable使わないんだなぁと。IL眺めながら妙に納得してしまったりw
今回は、非常に単純なサンプルだったので、全ての場合がそうなるのかちょっと言い切れない部分は残りますが、一応調べたコトを備忘録的に。。。
外部キー制約その2
てーことで、前回id:NetSeed:20110421はForeignKeyAttributeを使って、リレーションシップの作成をしてみました。
で、今回は、これをちょいと手直ししていきたいと思います。
前回の問題点
前回の方法では、若干冗長な"ManufacturerId"がプロパティとして定義されている必要がありました。しかし、本来このプロパティとナビゲーションプロパティのManufacturerは同じEntityを示すことになりますから、ManufacturerIdプロパティをどーにかして排除できないか?と言うのが今回のお題となります。
まずは属性とプロパティをなくしてしまう
この作業の手始めに、Blandクラスを以下のように若干手直しします。
public class Bland { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public virtual Manufacturer Manufacturer { get; set; } [Required] public string Name { get; set; } }
前回との違いは、
- ManufacutersIdプロパティを除去した
- ForeignKeyAttributeの除去
となります。
当然、この状態で動かしてしまうと、ManufacutereとBland間のリレーションが存在しなくなってしまいますから、何とかしなけりゃまずいわけです。
SampleContextをいじる
で、次にすべきは、属性でリレーションを定義する代わりに、別の方法で定義する方法となります。これは、DbContextクラスを継承したSampleContextクラスの中で行う作業となります。
具体的には、OnModelCreatingメソッドをオーバーライドすることになります。
で、書き換えたSampleContextクラスは以下の通りです。
public class SampleContext : DbContext { public SampleContext(string con) : base(con) { } public DbSet<Manufacturer> Manufacturers { get; set; } public DbSet<Bland> Blands { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Bland>() .HasRequired(x => x.Manufacturer) .WithMany(x => x.Blands) .Map(x => x.MapKey("ManufacturersId")); .WillCascadeOnDelete(true); base.OnModelCreating(modelBuilder); } }
OnModelCreatingが何を実際やっているかというと、名前通り、Code-FirstからDBを定義する前に、呼ばれるメソッドとなり、引数で受け取ったDbModelBuilderに追加的なコンフィギュレーションを突っ込むことで様々なコトが出来るようになります。
今回は、リレーションシップのコンフィグを入れ込んでいる形になります。順を追って説明すると
- BlandのEntityTypeConfigurationを取得する
- EntityTypeConfiguration.HasRequiredメソッドでナビゲーションプロパティを指定する
- RequiredNavigationPropertyが戻ってくるので、1:*となるようにRequiredNavigationProperty.WithManyを指定し、Manufacturer側のナビゲーションプロパティを指定する
- そうすることで、ForeignKeyNavigationPropertyConfigurationが戻るので、これをDBのフィールド”ManufacuturersId”にマップする。
- 最後に、戻ってきたCascadableNavigationPropertyConfigurationの、WillCascadeOnDeleteをtrue指定にすることで、Manufacturer側の要素が削除されたら、関連するBlandも一緒に削除するように定義する。
と言う流れになります。
属性を付けるより厄介な作業となりますが、逆にキメの細やかな制御がこれで可能になります。
また、次回以降のテーマとなりますが
- 1:1の関係の構築
- 1:0又は1の関係の構築
等も可能になります。
基本的な外部キー制約を付けてみる
今回は、属性を使う形でので外部キー制約と、そのNavigationあたりを。
今回のモデル
こいつのテーブルスキーマは以下の通り。
--ManufacturersTable CREATE TABLE [Manufacturers] ( [Id] int NOT NULL IDENTITY (1,1) , [Name] nvarchar(4000) NOT NULL ); GO ALTER TABLE [Manufacturers] ADD CONSTRAINT [PK__Manufacturers__0000000000000006] PRIMARY KEY ([Id]); GO --BlandsTable CREATE TABLE [Blands] ( [Id] int NOT NULL IDENTITY (1,1) , [ManufacturersId] int NOT NULL , [Name] nvarchar(4000) NOT NULL ); GO ALTER TABLE [Blands] ADD CONSTRAINT [PK__Blands__0000000000000012] PRIMARY KEY ([Id]); GO ALTER TABLE [Blands] ADD CONSTRAINT [Bland_Manufacturer] FOREIGN KEY ([ManufacturersId]) REFERENCES [Manufacturers]([Id]) ON DELETE CASCADE ON UPDATE NO ACTION; GO
で、今回のお題は、このスキーマをCode-Firstで作っちまおうってコトと、そのナビゲーションをどうするかってテーマ。
Code-Firstでモデルを作成する
まずはいつも通り、モデルの作成から。まずは、Manufacuteresで使う、Manufacturerモデルから
public class Manufacturer { //Primary key [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Required(AllowEmptyStrings = false)] public string Name { get; set; } //このEntityに関連づけられているBlandを取得するためのProperty public virtual ICollection<Bland> Blands { get; set; } }
Idと、Nameは良いとして、Blandsプロパティがどう使われるのかは、後ほど。
続いてBlandsテーブルで利用するBlandモデル
public class Bland { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [ForeignKey("Manufacturer")] public int ManufacturersId { get; set; } //ちなみに、こうも書ける(但し排他的) //[ForeignKey("ManufacturersId")] public virtual Manufacturer Manufacturer { get; set; } [Required] public string Name { get; set; } }
重要なのは、外部制約を付けたいプロパティ又はそのナビゲーションプロパティのどちらかに、ForeignKeyAttributeを付ける必要があると言うコト。今回のように、外部制約を付けたいプロパティにこの制約を付けた場合、属性のctorに渡す文字列は、関連するナビゲーションプロパティの名前を指定する。また、逆に、ナビゲーションプロパティにこの属性を付ける場合は、外部制約を付けたいプロパティの名前を指定する。また、ナビゲーションプロパティには必ずvirtualを付与する必要がある。
コンテキストの作成と実行
で、コンテキストを毎度ながら、作るここはいつもと変わらない。
public class SampleContext : DbContext { public SampleContext(string con) : base(con) { } public DbSet<Manufacturer> Manufacturers { get; set; } public DbSet<Bland> Blands { get; set; } }
次に、こいつを利用するわけだけど、これがまたすばらしすぎるんだw
サンプルのコンテンツが時計ばっかりなのは、すぐ思い浮かぶのが時計なのでw*1
public static void Main() { /* * サンプルコード中の製品名、又は固有名は、各社の商標又は登録商標です。 * サンプルコード中では、TM,〓マークを明記しておりません。 */ //ce4.0にデフォルトを設定 Database.DefaultConnectionFactory = new System.Data.Entity.Infrastructure.SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); //実行毎に新しいDBを作ることを指定 Database.SetInitializer(new DropCreateDatabaseAlways<SampleContext>()); using (var ctx = new SampleContext("Sample.sdf")) { //Manufacturers経由の追加。 var seiko = ctx.Manufacturers.Add(new Manufacturer { Name = "SEIKO" }); var citizen = ctx.Manufacturers.Add(new Manufacturer { Name = "CITIZEN" }); var hoge = ctx.Manufacturers.Add(new Manufacturer { Name = "hoge" }); //SampleContext.Bladns経由の追加。 var spilit = ctx.Blands.Add(new Bland { Name = "SPIRIT", Manufacturer = seiko }); var brightz = ctx.Blands.Add(new Bland { Name = "BRIGHTZ", Manufacturer = seiko }); var forma = ctx.Blands.Add(new Bland { Name = "FORMA", Manufacturer = citizen }); var xC = ctx.Blands.Add(new Bland { Name = "xC", Manufacturer = citizen }); var piyo = ctx.Blands.Add(new Bland { Name = "piyo", Manufacturer = hoge }); ctx.Blands.Add(new Bland { Name = "foo", Manufacturer = hoge }); ctx.Blands.Add(new Bland { Name = "bar", Manufacturer = hoge }); //SampleContext.Manufacturers経由の追加 seiko.Blands.Add(new Bland { Name = "Grand Seiko" }); citizen.Blands.Add(new Bland { Name = "THE CITIZEN" }); //Blands経由のManufacuturerの追加 var gShock = ctx.Blands.Add(new Bland { Name = "G-SHOCK", Manufacturer = new Manufacturer { Name = "CASIO" } }); ctx.SaveChanges(); //Manufacuter毎に列挙してみる。 foreach (var manufacture in ctx.Manufacturers) { Console.WriteLine("ManufacturerName:{0}", manufacture.Name); foreach (var bland in manufacture.Blands) { Console.WriteLine("\tBlandName:{0}", bland.Name); } Console.WriteLine(); Console.WriteLine(); } //hogeを丸ごとけしてみる ctx.Manufacturers.Remove(hoge); ctx.SaveChanges(); Console.WriteLine("After Delete..."); //もう一度Manufacuter毎に列挙してみる。 foreach (var manufacture in ctx.Manufacturers) { Console.WriteLine("ManufacturerName:{0}", manufacture.Name); foreach (var bland in manufacture.Blands) { Console.WriteLine("\tBlandName:{0}", bland.Name); } Console.WriteLine(); Console.WriteLine(); } //じゃあ、Piyoはどーなってるのか? //EntityはNullじゃないけど、ナビゲーションプロパティがNullになる。 Console.WriteLine(piyo == null); Console.WriteLine(piyo.Id); Console.WriteLine(piyo.ManufacturersId); Console.WriteLine(piyo.Name); Console.WriteLine(piyo.Manufacturer == null); } }
このように、Manufacutereを先に登録して、それを元にBlandを登録しても、Manufacutere.Blandsコレクション経由でBlandをAddしても、BlandとManufacutereを同時に登録しても問題ないのがすばらしいところ。
また、ともすれば複雑になりがちな、外部参照制約先のEntityや、逆参照も器であるPropertyをvirtual付きで作っておけば後はよろしくやってくれる。
また、hogeのように、参照先を削除した場合は、先に提示したスキーマ通り、一緒にBlandも消えてくれる。
で、なんでUpdateがNO ACTIONなのかというと、KEY属性を付けたプロパティは変更不可なため。*2
このように、非常に自然な形でデータベースを取り扱えるし、面倒な部分はほぼお任せでやってくれるのがEFのすばらしいところじゃないかと、サンプル書きながら思いました。