EntityFrameworkCore で SQLite3 を使ってみる(その1)の続きです。
前回はデータベースファイルを作るところまででした。今回はエンティティの関連を定義していきます。 SQLite 成分がないですね...
ソースコードは GitHub で公開しています。
git リポジトリ:https://github.com/yumimatoba14/SQLiteTest.git
2. エンティティ間のリレーションを設定する
1対N、N対Nの関係をそれぞれ追加します。 1対1がないのはご容赦を。(末尾にMicrosoftのドキュメントをリンクしておきます。)
ナビゲーションプロパティにエンティティが設定されない問題
ソースコードは以下を参照してください。
Revision: 7f914753f1efd06eb47256537a4de6e3bff04065
Message:
added AddTask migration.
- added Task entity.
二つのエンティティ Workflow と Task に1対Nの関係を作ったのですが、 Workflow.Tasks の中に Task が入ってくれない現象に遭遇。
ShowWorkflow(db, 1); //※1
ShowWorkflow(db, 10); //※2
var task0 = db.Tasks.Find(1, 1);
if (task0 != null)
{
ShowTask(task0); //※3
}
foreach (var task in db.Tasks) //※4
{
ShowTask(task);
}
ShowWorkflow(db, 1); //※5
に対応する出力が以下の通り。
workflow.Name = Workflow1 //※1
workflow.Name = Workflow10 //※2
task: Workflow = 1, SubId = 1, Name = task0 //※3
task: Workflow = 1, SubId = 1, Name = task0 //※4
task: Workflow = 1, SubId = 2, Name = task1
task: Workflow = 10, SubId = 1, Name = task2
task: Workflow = 10, SubId = 2, Name = task3
task: Workflow = 10, SubId = 3, Name = task4
workflow.Name = Workflow1 //※5
task: Workflow = 1, SubId = 1, Name = task0
task: Workflow = 1, SubId = 2, Name = task1
何故か ShowWorkflow(db, 1) の2回の呼出し(※1と※5)で出力結果が異なります。 一回目の時には Workflow.Tasks の中に Task が登録されていません。 全 Task に対するループ(※4)を回した時に Task インスタンスが作成されてそれが Workflow にも設定されたのだろうと予想されます。
EntityFramework を使っていてこのような挙動は見たことがないんだけどな... ということで悩むこと数時間。
ナビゲーションプロパティへのアクセス時にオブジェクトを読み込むような機能(=遅延ロード)が動いていないと予想できます。 ナビゲーションプロパティを virtual にすると遅延ロードが効くとかの情報もあり、それに従って virtual をつけたり消したりしても結果は変わらず。
結論として、EntityFrameworkCore においては UseLazyLoadingProxies を設定することが必要なようです。(参考:関連データの遅延読み込み(Microsoft)) この機能を使うには Microsoft.EntityFrameworkCore.Proxies パッケージの導入が必要です。
修正することで出力は以下の通り。不思議のない結果になりました。
workflow.Name = Workflow1 //※1
task: Workflow = 1, SubId = 1, Name = task0
task: Workflow = 1, SubId = 2, Name = task1
workflow.Name = Workflow10 //※2
task: Workflow = 10, SubId = 1, Name = task2
task: Workflow = 10, SubId = 2, Name = task3
task: Workflow = 10, SubId = 3, Name = task4
task: Workflow = 1, SubId = 1, Name = task0 //※3
task: Workflow = 1, SubId = 1, Name = task0 //※4
task: Workflow = 1, SubId = 2, Name = task1
task: Workflow = 10, SubId = 1, Name = task2
task: Workflow = 10, SubId = 2, Name = task3
task: Workflow = 10, SubId = 3, Name = task4
workflow.Name = Workflow1 //※5
task: Workflow = 1, SubId = 1, Name = task0
task: Workflow = 1, SubId = 2, Name = task1
この変更はコミット「SHA-1: 173ce83803b7525a58600dbb172f5f97b7888a81」に含まれています。
1対Nの関連の定義
順番が前後しますが一応実装コードにも触れます。この辺りは世間に情報が多いので簡潔に。
Workflow 1 に対し N 個の Taskを持たせた構造を作りたいとします。
- Task には WorkflowId と ナビゲーションプロパティ Workflow を追加します
- Workflow にはナビゲーションプロパティ Tasks を追加します
- ナビゲーションプロパティの型は IList などにしてもよい様子
- Tasks にはコンストラクタでコンテナインスタンスをセットします。使用するインスタンスの型は HashSet の他 List あたりが使われるようです
- UseLazyLoadingProxies を設定するときはナビゲーションプロパティを virtual にします。(そうしないと実行時に例外が発生します。 実装上派生クラスを作って LazyLoading を実現しているようです。)
public class Workflow
{
public Workflow()
{
Tasks = new HashSet<Task>();
}
public virtual ICollection<Task> Tasks { get; }
}
public class Task
{
public int WorkflowId { get; set; }
public virtual Workflow Workflow { get; set; }
}
1対Nの関係を定義する場合普通 DbContext に以下のようなコードを追加しますが、今回は名前が標準的なルールに従っているためなくても動きます。
public class WfDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 一部省略
// 以下はなくても動く
#if false
modelBuilder.Entity<Workflow>()
.HasMany<Task>(x => x.Tasks)
.WithOne(x => x.Workflow)
.HasForeignKey(x => x.WorkflowId)
.IsRequired();
#endif
}
}
この辺りは ModelBuilder.Model.DebugView を見ると確認できます。
Model:
EntityType: Task
Properties:
WorkflowId (int) Required PK FK Index AfterSave:Throw
SubId (int) Required PK Index AfterSave:Throw
Name (string) Required
Remark (string)
Navigations:
Workflow (Workflow) ToPrincipal Workflow Inverse: Tasks
Keys:
WorkflowId, SubId PK
Foreign keys:
Task {'WorkflowId'} -> Workflow {'Id'} ToDependent: Tasks ToPrincipal: Workflow
Annotations:
Relational:TableName: Tasks
RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
EntityType: Workflow
Properties:
Id (int) Required PK Index AfterSave:Throw ValueGenerated.OnAdd
Name (string) Required
Navigations:
Tasks (ICollection<Task>) Collection ToDependent Task Inverse: Workflow
Keys:
Id PK
Annotations:
Relational:TableName: Workflows
RelationshipDiscoveryConvention:NavigationCandidates: System.Collections.Immutable.ImmutableSortedDictionary`2[System.Reflection.PropertyInfo,System.Type]
Annotations:
NonNullableConventionState: Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableConventionBase+NonNullabilityConventionState
ProductVersion: 3.1.32
ナビゲーションプロパティが認識されてそうなことが分かります。
N対N の関連の定義
「Task の前後にはそれぞれ0個以上のTaskがある」という構造を表すため、前のタスクと後のタスクを N 対 N の関係にします。
以下のコミットに実装があります。
Revision: facbe64c411512670251d09bc392addf648ddaac
Message:
added AddTaskRelation migration.
- added TaskRelation entity.
- 間に TaskRelation インスタンスを挟むことで 1対N の関係の組み合わせで表現します。(前の Taskと TaskRelation、後の Task と TaskRelaiton がそれぞれ1対Nの関係になる。)
- 説明していませんでしたが Task は WorkflowId と SubId の複合キーを主キーとします。(SubId は Workflow の中で一意。) TaskRelation が Task を参照するための外部キーも複合キーで指定します。 WorkflwoId は前後の関連どちらとも同じ値を使用します。
public class Task
{
public Task()
{
PrevTaskRelations = new List<TaskRelation>();
NextTaskRelations = new List<TaskRelation>();
}
public virtual ICollection<TaskRelation> PrevTaskRelations { get; }
public virtual ICollection<TaskRelation> NextTaskRelations { get; }
}
public class TaskRelation
{
[Required]
public int WorkflowId { get; set; }
[Required]
public int PrevTaskSubId { get; set; }
[Required]
public int NextTaskSubId { get; set; }
public virtual Task PrevTask { get; set; }
public virtual Task NextTask { get; set; }
}
public class WfDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TaskRelation>(r =>
{
r.HasKey(x => new { x.WorkflowId, x.PrevTaskSubId, x.NextTaskSubId });
r.HasIndex(x => new { x.WorkflowId, x.PrevTaskSubId, x.NextTaskSubId });
r.HasOne(x => x.PrevTask).WithMany(x => x.NextTaskRelations)
.HasForeignKey(x => new { x.WorkflowId, x.PrevTaskSubId });
r.HasOne(x => x.NextTask).WithMany(x => x.PrevTaskRelations)
.HasForeignKey(x => new { x.WorkflowId, x.NextTaskSubId });
});
}
}
まとめ
SQLite を用いるにあたって導入すべき EntityFrameworkCore の nuget パッケージは以下になります。(バージョンは全て 3.1.32 です。)
- Microsoft.EntityFrameworkCore.Sqlite
- Microsoft.EntityFrameworkCore.Tools
- Microsoft.EntityFramework.Proxies
当初はリレーションについて試して終わりかなと思ってました。
ですがいくつか気になるトピックも出てきたため、多分もうちょっとだけ続きます。
参考リンク
-
- こちらは EntityFrameworkCore ではなく、EF6 に関するページ。 以下のような記述があります
このプロキシは、プロパティがアクセスされたときにアクションを自動的に実行するフックを挿入するために、エンティティの一部の仮想プロパティをオーバーライドします。 たとえば、このメカニズムは、リレーションシップの遅延読み込みをサポートするために使用されます。
- こちらは EntityFrameworkCore ではなく、EF6 に関するページ。 以下のような記述があります