technoshop

杉並区和泉のソフトウェアハウス、株式会社テラソフトの技術ブログです。主に.NET MVCとKnockoutJSの情報をまとめます。

Entity Framework Code First でのリレーションシップ

こんにちは。ザクです。遅めの夏休みをとってリフレッシュしてきました。こう涼しいと夏休み感はゼロですね。

今日はCode Firstでモデル定義する際のリレーションシップの設定方法について書きます。こちらRailsほどちゃんとした情報がなくて大変困ったやつです。Microsoftさんにおかれましては、善処していただきたく存じます。

One-to-Manyリレーションシップ

簡単なのから行きます。1:Nの関係の場合です。


Creating an Entity Framework Data Model for an ASP.NET MVC Application (1 of 10) | The ASP.NET Site

前回紹介したチュートリアルで作成したモデルが早速そうなっています。StudentとEnrollmentの関係がそうですね。モデルのダイアグラム(Visual Studio 2012から自動生成できます。方法はまた後日説明します)を見ると、Student側に「1」、Enrollment側に「*」がついています。つまりStudentが親で、Enrollmentが子という関係です。では、コードをみてみましょう。

public class Student
{
    public int StudentId { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
        
    public virtual ICollection<Enrollment> Enrollments { get; set; }
}

CodeFirstではモデルを作成する際、最初のintの名前にモデル名が入っていたら、規約に従いそのintを主キーにします。規約から外れた名前をキーに付けたい場合は、[Key]属性をそのintの上に付けることで主キーにできます。

最後の行のvirtualが関係を定義している部分です。ダイアグラムではこの部分はモデルの「ナビゲーションプロパティ」と呼ばれる下の部分に入ります。(ナビゲーションプロパティについては追って説明します)ICollectionなので、コレクションになっており、次の<>の中にモデル名、最後にこのモデル上での名前をつけます。規約にしたがい、モデル上の名前は、モデルを複数形にします。これで、Student(親)側の定義ができました。

次に子供のEnrollment側のコードです。

public enum Grade
{
    A, B, C, D, F
}

public class Enrollment
{
    public int EnrollmentId { get; set; }
    public int CourseId { get; set; }
    public int StudentId { get; set; }
    public Grade? Grade { get; set; }
        
    public virtual Course Course { get; set; }
    public virtual Student Student { get; set; }
}

こちらには鍵になるintが複数定義されています。最初のintだけは自分自身のidでそれ以外は、外部キー(FK)になります。親を持つモデルには、必ず親となるモデルのキーが定義されていなければなりません。

virtualの部分も異なります。こちらはコレクションになっていません。親は一つなので当然です。この場合virtualの後には、モデル名、このモデル上の名前を書きます。規約に従いモデル上の名前には、親のモデル名を単数形で書いてください。

以上で、One-to-Manyの関係が作れます。

Many-to-Manyリレーションシップ

次にわかりやすいのは、N:Nです。

N:Nの定義の方法は2種類あります。2つのテーブルをリンクするテーブルを自分で定義して作るか、Code Firstに作らせるかです。前者の方は、N:Nのリンクテーブルの中にもプロパティを持たせたいときに使います。後者の方は、純粋なN:N用のリンクテーブルを作るときに使います。

まず前者のケースから。これはチュートリアルのEnrollmentモデルが参考になります。親の鍵を2つ持ち、virtualのナビゲーションプロパティも2つ持ちます。(上のEnrollmentを参考)このテーブルはGradeという、このモデルで使うプロパティを持つので、このように作っています。

次は純粋なN:Nリンクテーブルです。こちらはモデルを定義せずに、DALファイルの中にテーブル間の関係を定義するコードを書きます。

先にこの関係に使うモデルを定義しましょう。Instructorモデルを作ります。(namespaceに注意してください)

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcApplication1.Models
{
    public class Instructor
    {
        public int InstructorId { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

次にCourseモデルを作ります。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcApplication1.Models
{
   public class Course
   {
      [DatabaseGenerated(DatabaseGeneratedOption.None)]
      [Display(Name = "Number")]
      public int CourseId { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      [Display(Name = "Department")]
      public int DepartmentId { get; set; }

      public virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }

最後にDALを修正します。

public class SchoolContext : DbContext
{
  : base("DefaultConnection")
  {
  }
  public DbSet<Course> Courses { get; set; }
  public DbSet<Department> Departments { get; set; }
  public DbSet<Enrollment> Enrollments { get; set; }
  public DbSet<Instructor> Instructors { get; set; }
  public DbSet<Student> Students { get; set; }
  public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
     modelBuilder.Entity<Course>()
         .HasMany(c => c.Instructors).WithMany(i => i.Courses)
         .Map(t => t.MapLeftKey("CourseId")
             .MapRightKey("InstructorId")
             .ToTable("CourseInstructor"));
  }
}

DALに記述するコードはこんな風になります。DALを作った時にしたようにDbSetにCourseとInstructorモデルの定義を追加したら、その下のOnModelCreatingの中で、Entityの定義をしてやります。ここでは、Courseから見てInstructorモデルを.HasManyでつなぎ、Instructorから見たCourseを.WithManyでつないでいます。.Map以下でそれぞれが持つFKと出来上がったテーブルの名前を定義します。

N:Nの定義はこんな感じです。

One-to-Zero or Oneリレーションシップ

最後に1:1です。1:1の定義方法も2種類ですが、普通のOne-to-Oneはまたこんど説明します。説明が多くなるので、よく使う簡単なZero or Oneの方を先に説明します。

チュートリアルでは、InstructorとOfficeAssignmentモデルがこれにあたります。先に作ったInstructorの中にOfficeAssignmentのナビゲーションプロパティがあったと思います。ここでは、OfficeAssignmentを作りましょう。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcApplication1.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorId { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

1:1の子テーブルの特徴は、自身のモデル名がついたIdを持たないことです。代わりに親のIdを主キーとして持ちます。InstructorIdに2つの属性[Key]と[ForeignKey("Instructor")]がついていますね。つまり、このモデルはInstructorIdを主キーであり、親のInstructorモデルへのFKでもあるということです。ナビゲーションプロパティは、1:Nの時と同様に子モデルなので、1つの親を単数形で定義します。1:1の簡単な方の定義は以上です。

ここまでできたらチュートリアルを参考にシードデータも更新しましょう。(チュートリアルでは、Departmentという1:Nのモデルも定義しているので、こちらを作っておくのをお忘れなく。コピペしたデータにこのモデル用のデータも入っているのでエラーが出ますよ)

マイグレーション再び!

シードデータも用意できたので、ここで作ったモデルを作成する早速マイグレーションファイルを生成します。コマンドはadd-migrationでしたね。

add-migration Chapter4
update
-database

いろいろ変えちゃったので、マイグレーションの名前はチュートリアルに合わせて「Chapter4」にしときましょうか。本当は、マイグレーションの内容に合わせてちゃんと書いたほうがいいですね。

f:id:technoshop:20140924135939j:plain

こんな風にテーブル間のリレーションシップも自動で定義されます。

f:id:technoshop:20140924140053j:plain

CourseInstructorのデータもちゃんと入っていますね。