0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EntityFrameworkCore で SQLite3 を使ってみる(その2)

Posted at

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 が入ってくれない現象に遭遇。

Program.cs
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 を実現しているようです。)
Workflow.cs
public class Workflow
{
    public Workflow()
    {
        Tasks = new HashSet<Task>();
    }

    public virtual ICollection<Task> Tasks { get; }
}
Task.cs
public class Task
{
    public int WorkflowId { get; set; }

    public virtual Workflow Workflow { get; set; }
}

1対Nの関係を定義する場合普通 DbContext に以下のようなコードを追加しますが、今回は名前が標準的なルールに従っているためなくても動きます。

WfDbContext.cs
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 を見ると確認できます。

ModelBuilder_DebugView.png

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 は前後の関連どちらとも同じ値を使用します。
Task.cs
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; }
}
TaskRelation.cs
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; }
}
WfDbContext.cs
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

当初はリレーションについて試して終わりかなと思ってました。
ですがいくつか気になるトピックも出てきたため、多分もうちょっとだけ続きます。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?