다음을 통해 공유


ASP.NET MVC 애플리케이션에서 Entity Framework를 사용하여 상속 구현(10개 중 8개)

작성자 : Tom Dykstra

Contoso University 샘플 웹 애플리케이션은 Entity Framework 5 Code First 및 Visual Studio 2012를 사용하여 ASP.NET MVC 4 애플리케이션을 만드는 방법을 보여 줍니다. 자습서 시리즈에 대한 정보는 시리즈의 첫 번째 자습서를 참조하세요.

참고

resolve 수 없는 문제가 발생하면 완료된 장을 다운로드하고 문제를 재현해 보세요. 일반적으로 코드를 완료된 코드와 비교하여 문제에 대한 해결 방법을 찾을 수 있습니다. 몇 가지 일반적인 오류 및 해결 방법은 오류 및 해결 방법을 참조하세요.

이전 자습서에서는 동시성 예외를 처리했습니다. 이 자습서에서는 데이터 모델에서 상속을 구현하는 방법을 보여 줍니다.

개체 지향 프로그래밍에서 상속을 사용하여 중복 코드를 제거할 수 있습니다. 이 자습서에서는 강사와 학생 모두에게 공통적인 속성(예: LastName)이 포함된 Person 기본 클래스에서 클래스가 파생되도록 InstructorStudent 클래스를 변경합니다. 웹 페이지를 추가하거나 변경하지는 않지만 일부 코드를 변경하고 이러한 변경 내용이 데이터베이스에 자동으로 반영됩니다.

계층별 테이블 및 형식별 테이블 상속

개체 지향 프로그래밍에서는 상속을 사용하여 관련 클래스로 더 쉽게 작업할 수 있습니다. 예를 들어 Instructor 데이터 모델의 및 Student 클래스는 School 여러 속성을 공유하므로 중복 코드가 발생합니다.

중복 코드가 강조 표시된 학생 및 강사 클래스를 보여 주는 스크린샷

InstructorStudent 엔터티에서 공유하는 속성에 대해 중복 코드를 제거하려고 한다고 가정해 보겠습니다. 다음 그림과 같이 이러한 공유 속성만 포함하는 기본 클래스를 만든 Person 다음, 및 Student 엔터티가 해당 기본 클래스에서 상속되도록 할 Instructor 수 있습니다.

Person 클래스에서 파생된 Student 및 Instructor 클래스를 보여 주는 스크린샷

데이터베이스에 이 상속 구조를 여러 가지 방법으로 나타낼 수 있습니다. 학생과 강사에 관한 정보를 하나의 테이블에 포함하는 Person 테이블을 포함할 수 있습니다. 일부 열은 강사()HireDate에만 적용할 수 있으며, 일부는 학생(EnrollmentDate)에만 적용되며, 일부는 둘 다(LastName, FirstName)에만 적용될 수 있습니다. 일반적으로 각 행이 나타내는 형식을 나타내는 판별 자 열이 있습니다. 예를 들어, 판별자 열은 강사에 대해 "Instructor"를, 학생에 대해 "Student"를 포함할 수 있습니다.

Person 엔터티 클래스의 상속 구조를 보여 주는 스크린샷

단일 데이터베이스 테이블에서 엔터티 상속 구조를 생성하는 이 패턴을 TPH( 테이블 단위 계층 ) 상속이라고 합니다.

다른 방법은 데이터베이스를 상속 구조와 유사하게 만드는 것입니다. 예를 들어 Person 테이블에 이름 필드만 있고 날짜 필드가 있는 별도의 InstructorStudent 테이블을 포함할 수 있습니다.

Person 엔터티 클래스에서 파생된 새 강사 및 학생 데이터베이스 테이블을 보여 주는 스크린샷

각 엔터티 클래스에 대한 데이터베이스 테이블을 만드는 이 패턴을 TPT( 형식별 테이블 ) 상속이라고 합니다.

TPT 패턴으로 인해 복잡한 조인 쿼리가 발생할 수 있으므로 TPH 상속 패턴은 일반적으로 TPT 상속 패턴보다 Entity Framework에서 더 나은 성능을 제공합니다. 이 자습서에서는 TPH 상속을 구현하는 방법을 보여 줍니다. 다음 단계를 수행하여 이 작업을 수행합니다.

  • 클래스를 Person 만들고 에서 파생Person되도록 및 Student 클래스를 변경 Instructor 합니다.
  • 데이터베이스 컨텍스트 클래스에 모델-데이터베이스 매핑 코드를 추가합니다.
  • 프로젝트 PersonID전체에서 및 StudentID 참조를 로 변경 InstructorID 합니다.

Person 클래스 만들기

참고: 이러한 클래스를 사용하는 컨트롤러를 업데이트할 때까지 아래 클래스를 만든 후에는 프로젝트를 컴파일할 수 없습니다.

Models 폴더에서 Person.cs를 만들고 템플릿 코드를 다음 코드로 바꿉니다.

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

namespace ContosoUniversity.Models
{
   public abstract class Person
   {
      [Key]
      public int PersonID { get; set; }

      [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
      [StringLength(50, MinimumLength = 1)]
      [Display(Name = "Last Name")]
      public string LastName { get; set; }

      [Column("FirstName")]
      [Display(Name = "First Name")]
      [StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
      public string FirstMidName { get; set; }

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

Instructor.cs에서 Person 클래스를 Instructor 파생하고 키 및 이름 필드를 제거합니다. 해당 코드는 다음 예제와 같이 나타납니다.

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

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

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

Student.cs를 비슷하게 변경합니다. 클래스는 Student 다음 예제와 같습니다.

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

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

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

모델에 사람 엔터티 형식 추가

SchoolContext.cs에서 엔터티 형식에 Person 대한 속성을 추가 DbSet 합니다.

public DbSet<Person> People { get; set; }

계층당 하나의 테이블 상속을 구성하기 위해 Entity Framework에 필요한 모든 작업입니다. 보듯이 데이터베이스를 다시 만들 Person 때 및 Instructor 테이블 대신 테이블이 Student 있습니다.

InstructorID 및 StudentID를 PersonID로 변경

SchoolContext.cs의 Instructor-Course 매핑 문에서 를 로 MapRightKey("PersonID")변경 MapRightKey("InstructorID") 합니다.

modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));

이 변경은 필요하지 않습니다. 다대다 조인 테이블의 InstructorID 열 이름을 변경합니다. 이름을 InstructorID로 두면 애플리케이션이 여전히 올바르게 작동합니다. 완성된 SchoolContext.cs는 다음과 같습니다.

using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ContosoUniversity.DAL
{
   public class SchoolContext : DbContext
   {
      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; }
      public DbSet<Person> People { get; set; }

      protected override void OnModelCreating(DbModelBuilder modelBuilder)
      {
         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

         modelBuilder.Entity<Course>()
             .HasMany(c => c.Instructors).WithMany(i => i.Courses)
             .Map(t => t.MapLeftKey("CourseID")
                 .MapRightKey("PersonID")
                 .ToTable("CourseInstructor"));
      }
   }
}

다음으로 Migrations 폴더의 타임스탬프를 적용한 마이그레이션 파일을 제외하고 프로젝트 전체에서 및 로 변경 StudentIDInstructorIDPersonIDPersonID 해야 합니다. 이렇게 하려면 변경해야 하는 파일만 찾아서 연 다음, 열린 파일에서 전역 변경을 수행합니다. 마이그레이션 폴더의 유일한 파일은 Migrations\Configuration.cs입니다.

  1. 중요

    먼저 Visual Studio에서 열려 있는 모든 파일을 닫습니다.

  2. 찾기 및 바꾸기 -- 편집 메뉴에서 모든 파일 찾기 클릭한 다음 가 포함된 InstructorID프로젝트의 모든 파일을 검색합니다.

    찾기 및 바꾸기 창을 보여 주는 스크린샷 강사 ID, 현재 프로젝트, 대/소문자 일치 및 단어 전체 일치 확인란 및 모두 찾기 단추가 모두 강조 표시되어 있습니다.

  3. 각 파일에 대해 한 줄을 두 번 클릭하여 Migrations 폴더의 time-stamp>_.cs 마이그레이션 파일을 제외한<결과 찾기 창에서 각 파일을 엽니다.

    결과 찾기 창을 보여 주는 스크린샷 타임스탬프를 마이그레이션하는 파일은 빨간색으로 교차됩니다.

  4. 파일에서 바꾸기 대화 상자를 열고 조회열려 있는 모든 문서로 변경합니다.

  5. 파일에서 바꾸기 대화 상자를 사용하여 모두 InstructorID 로 변경PersonID.

    찾기 및 바꾸기 창을 보여 주는 스크린샷 사용자 ID가 텍스트로 바꾸기 필드에 입력됩니다.

  6. 가 포함된 프로젝트의 모든 파일을 찾습니다 StudentID.

  7. 각 파일에 대해 한 줄을 두 번 클릭하여 Migrations 폴더의 <time-stamp>_*.cs 마이그레이션 파일을 제외한 결과 찾기 창에서 각 파일을 엽니다.

    결과 찾기 창을 보여 주는 스크린샷 타임스탬프를 마이그레이션하는 파일이 교차됩니다.

  8. 파일에서 바꾸기 대화 상자를 열고 조회열려 있는 모든 문서로 변경합니다.

  9. 파일에서 바꾸기 대화 상자를 사용하여 모두 StudentID 로 변경합니다PersonID.

    찾기 및 바꾸기 창을 보여 주는 스크린샷 파일에서 바꾸기, 모든 열린 문서, 대/소문자 일치 및 단어 전체 일치 확인란 및 모두 바꾸기 단추가 강조 표시되어 있습니다.

  10. 프로젝트를 빌드합니다.

기본 키 이름을 지정하는 패턴의 classnameID단점을 보여 줍니다. 클래스 이름을 접두사로 지정하지 않고 기본 키 ID 이름을 지정한 경우 이제 이름을 변경할 필요가 없습니다.)

마이그레이션 파일 만들기 및 업데이트

PMC(패키지 관리자 콘솔)에서 다음 명령을 입력합니다.

Add-Migration Inheritance

Update-Database PMC에서 명령을 실행합니다. 마이그레이션에서 처리 방법을 모르는 기존 데이터가 있으므로 이 시점에서 명령이 실패합니다. 다음과 같은 오류가 표시됩니다.

ALTER TABLE 문이 FOREIGN KEY 제약 조건 "FK_dbo 충돌했습니다. Department_dbo. Person_PersonID". "ContosoUniversity" 테이블 "dbo" 데이터베이스에서 충돌이 발생했습니다. Person", 열 'PersonID'.

마이그레이션< timestamp>_Inheritance.cs 및 메서드를 Up 다음 코드로 바꿉니다.

public override void Up()
{
    DropForeignKey("dbo.Department", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.OfficeAssignment", "InstructorID", "dbo.Instructor");
    DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
    DropForeignKey("dbo.CourseInstructor", "InstructorID", "dbo.Instructor");
    DropIndex("dbo.Department", new[] { "InstructorID" });
    DropIndex("dbo.OfficeAssignment", new[] { "InstructorID" });
    DropIndex("dbo.Enrollment", new[] { "StudentID" });
    DropIndex("dbo.CourseInstructor", new[] { "InstructorID" });
    RenameColumn(table: "dbo.Department", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.OfficeAssignment", name: "InstructorID", newName: "PersonID");
    RenameColumn(table: "dbo.Enrollment", name: "StudentID", newName: "PersonID");
    RenameColumn(table: "dbo.CourseInstructor", name: "InstructorID", newName: "PersonID");
    CreateTable(
        "dbo.Person",
        c => new
            {
                PersonID = c.Int(nullable: false, identity: true),
                LastName = c.String(maxLength: 50),
                FirstName = c.String(maxLength: 50),
                HireDate = c.DateTime(),
                EnrollmentDate = c.DateTime(),
                Discriminator = c.String(nullable: false, maxLength: 128),
                OldId = c.Int(nullable: false)
            })
        .PrimaryKey(t => t.PersonID);

    // Copy existing Student and Instructor data into new Person table.
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, StudentId AS OldId FROM dbo.Student");
    Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, HireDate, null AS EnrollmentDate, 'Instructor' AS Discriminator, InstructorId AS OldId FROM dbo.Instructor");

    // Fix up existing relationships to match new PK's.
    Sql("UPDATE dbo.Enrollment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Enrollment.PersonId AND Discriminator = 'Student')");
    Sql("UPDATE dbo.Department SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Department.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.OfficeAssignment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = OfficeAssignment.PersonId AND Discriminator = 'Instructor')");
    Sql("UPDATE dbo.CourseInstructor SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = CourseInstructor.PersonId AND Discriminator = 'Instructor')");

    // Remove temporary key
    DropColumn("dbo.Person", "OldId");

    AddForeignKey("dbo.Department", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.OfficeAssignment", "PersonID", "dbo.Person", "PersonID");
    AddForeignKey("dbo.Enrollment", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    AddForeignKey("dbo.CourseInstructor", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
    CreateIndex("dbo.Department", "PersonID");
    CreateIndex("dbo.OfficeAssignment", "PersonID");
    CreateIndex("dbo.Enrollment", "PersonID");
    CreateIndex("dbo.CourseInstructor", "PersonID");
    DropTable("dbo.Instructor");
    DropTable("dbo.Student");
}

update-database 명령을 다시 실행합니다.

참고

데이터를 마이그레이션하고 스키마를 변경할 때 다른 오류가 발생할 수 있습니다. resolve 수 없는 마이그레이션 오류가 발생하는 경우 Web.config파일에서 연결 문자열 변경하거나 데이터베이스를 삭제하여 자습서를 계속할 수 있습니다. 가장 간단한 방법은 Web.config 파일에서 데이터베이스의 이름을 바꾸는 것입니다. 예를 들어 다음 예제와 같이 데이터베이스 이름을 CU_test 변경합니다.

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
      Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf" 
      providerName="System.Data.SqlClient" />

새 데이터베이스를 사용하면 마이그레이션할 데이터가 없으며 명령이 update-database 오류 없이 완료될 가능성이 훨씬 높습니다. 데이터베이스를 삭제하는 방법에 대한 지침은 Visual Studio 2012에서 데이터베이스를 삭제하는 방법을 참조하세요. 이 방법을 사용하여 자습서를 계속 진행하려면 배포된 사이트가 자동으로 마이그레이션을 실행할 때 동일한 오류가 발생하므로 이 자습서의 끝에 있는 배포 단계를 건너뜁니다. 마이그레이션 오류를 해결하려는 경우 가장 좋은 리소스는 Entity Framework 포럼 또는 StackOverflow.com 중 하나입니다.

테스트

사이트를 실행하고 다양한 페이지를 사용해 보세요. 모든 항목이 이전과 같이 작동합니다.

서버 ExplorerSchoolContext를 확장한 다음 테이블을 확장하면 학생강사 테이블이 Person 테이블로 대체된 것을 볼 수 있습니다. Person 테이블을 확장하면 학생강사 테이블에 있는 모든 열이 표시됩니다.

서버 Explorer 창을 보여 주는 스크린샷 데이터 연결, 학교 컨텍스트 및 테이블 탭이 확장되어 Person 테이블이 표시됩니다.

Person 테이블을 마우스 오른쪽 단추로 클릭한 후 테이블 데이터 표시를 클릭하여 판별자 열을 표시합니다.

Person 테이블을 보여 주는 스크린샷 판별자 열 이름이 강조 표시됩니다.

다음 다이어그램은 새 School 데이터베이스의 구조를 보여 줍니다.

School 데이터베이스 다이어그램을 보여 주는 스크린샷

요약

계층당 테이블 상속은 이제 , StudentInstructor 클래스에 Person대해 구현되었습니다. 이 상속 구조 및 기타 상속 구조에 대한 자세한 내용은 Morteza Manavi의 블로그에서 상속 매핑 전략을 참조하세요. 다음 자습서에서는 리포지토리 및 작업 패턴 단위를 구현하는 몇 가지 방법을 살펴보겠습니다.

다른 Entity Framework 리소스에 대한 링크는 ASP.NET 데이터 액세스 콘텐츠 맵에서 찾을 수 있습니다.