자습서: 상속 구현 - ASP.NET MVC 및 EF Core 사용

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

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

이 자습서에서는 다음을 수행합니다.

  • 데이터베이스에 상속 매핑
  • Person 클래스 만들기
  • 강사 및 학생 업데이트
  • 모델에 개인 추가
  • 마이그레이션 만들기 및 업데이트
  • 구현 테스트

필수 조건

데이터베이스에 상속 매핑

School 데이터 모델의 InstructorStudent 클래스는 동일한 여러 속성을 가지고 있습니다.

Student and Instructor classes

InstructorStudent 엔터티에서 공유하는 속성에 대해 중복 코드를 제거하려고 한다고 가정해 보겠습니다. 또는 강사 또는 학생의 이름인지 여부를 신경쓰지 않고 이름의 형식을 지정할 수 있는 서비스를 작성하려고 합니다. 공유 속성만 포함하는 Person 기본 클래스를 만든 후 다음 그림과 같이 해당 기본 클래스에서 InstructorStudent 클래스를 상속할 수 있습니다.

Student and Instructor classes deriving from Person class

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

Table-per-hierarchy example

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

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

Warning

TPT(형식당 하나의 테이블)는 EF Core 3.x에서 지원되지 않지만 EF Core 5.0에서 구현되었습니다.

Table-per-type inheritance

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

또 다른 옵션은 모든 비추상 형식을 개별 테이블에 매핑하는 것입니다. 상속된 속성을 포함한 모든 클래스 속성을 해당 테이블의 열에 매핑합니다. 이 패턴을 ‘TPC(구체적 클래스당 하나의 테이블)’ 상속이라고 합니다. 앞에서 표시된 것처럼 Person, StudentInstructor 클래스에 대한 TPC 상속을 구현한 경우, StudentInstructor 테이블은 상속을 구현한 후에도 이전과 다르지 않습니다.

TPT 패턴이 복잡한 조인 쿼리를 초래할 수 있기 때문에 TPC 및 TPH 상속 패턴은 일반적으로 TPT 상속 패턴보다 우수한 성능을 제공합니다.

이 자습서에서는 TPH 상속을 구현하는 방법을 보여 줍니다. TPH는 Entity Framework Core에서 지원하는 유일한 상속 패턴입니다. 사용자는 Person 클래스를 만들고, Person에서 파생되도록 InstructorStudent 클래스를 변경하며 DbContext에 새 클래스를 추가하고, 마이그레이션을 생성합니다.

다음 내용을 변경하기 전에 프로젝트 사본을 저장하는 것이 좋습니다. 그러면 문제가 발생하여 처음부터 다시 시작해야 하는 경우, 이 자습서의 단계를 역순으로 하거나 전체 시리즈의 처음으로 돌아가는 대신 저장된 프로젝트에서 쉽게 시작할 수 있습니다.

Person 클래스 만들기

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

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

namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

강사 및 학생 업데이트

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

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

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

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

에서 동일한 변경 내용을 만듭니다 Student.cs.

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

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


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

모델에 개인 추가

에 Person 엔터티 형식을 추가합니다 SchoolContext.cs. 새 줄이 강조 표시됩니다.

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

계층당 하나의 테이블 상속을 구성하기 위해 Entity Framework에 필요한 모든 작업입니다. 보다시피, 데이터베이스가 업데이트되면 Student 테이블과 Instructor 테이블 대신 Person 테이블이 생깁니다.

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

변경 내용을 저장하고 프로젝트를 빌드합니다. 그런 다음, 프로젝트 폴더에서 명령 창을 열고 다음 명령을 입력합니다.

dotnet ef migrations add Inheritance

database update 명령을 아직 실행하지 마세요. 이 명령은 Instructor 테이블을 삭제하고 Student 테이블 이름을 Person으로 변경하므로 데이터가 손실됩니다. 기존 데이터를 유지하려면 사용자 지정 코드를 제공해야 합니다.

Migrations/<timestamp>_Inheritance.cs을 열고 Up 메서드를 다음 코드로 바꿉니다.

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

    migrationBuilder.CreateIndex(
         name: "IX_Enrollment_StudentID",
         table: "Enrollment",
         column: "StudentID");

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}

이 코드는 다음과 같은 데이터베이스 업데이트 작업을 처리합니다.

  • Student 테이블을 가리키는 외래 키 제약 조건 및 인덱스를 제거합니다.

  • Instructor 테이블 이름을 Person으로 바꾸고 Student 데이터를 저장하기 위해 필요한 변경을 수행합니다.

  • 학생에 대한 Nullable EnrollmentDate를 추가합니다.

  • 행이 학생용인지, 강사용인지 나타내는 판별자 열을 추가합니다.

  • 학생 행은 고용 날짜를 포함하지 않으므로 HireDate를 Nullable로 설정합니다.

  • 학생을 가리키는 외래 키를 업데이트하는 데 사용할 임시 필드를 추가합니다. 학생을 Person 테이블에 복사하면 새로운 기본 키 값이 생깁니다.

  • Student 테이블에서 Person 테이블로 데이터를 복사합니다. 그러면 학생에게 새 기본 키 값이 할당됩니다.

  • 학생을 가리키는 외래 키 값을 수정합니다.

  • 이제 Person 테이블을 가리키도록 외래 키 제약 조건 및 인덱스를 다시 만듭니다.

(기본 키 형식으로 정수 대신 GUID를 사용한 경우, 학생 기본 키 값을 변경할 필요가 없으며 이 단계 중 일부가 생략되었을 수 있습니다.)

database update 명령을 실행합니다.

dotnet ef database update

(프로덕션 시스템에서는 이전 데이터베이스 버전으로 돌아가기 위해 사용해야 할 경우를 대비해서 Down 메서드에 해당 변경 내용을 적용합니다. 이 자습서에서는 Down 메서드를 사용하지 않습니다.)

참고 항목

기존 데이터가 있는 데이터베이스에서 스키마를 변경할 때 다른 오류가 발생할 수 있습니다. 해결할 수 없는 마이그레이션 오류가 발생하면 연결 문자열에서 데이터베이스 이름을 변경하거나 데이터베이스를 삭제할 수 있습니다. 새 데이터베이스에는 마이그레이션할 데이터가 없으므로 update-database 명령은 오류없이 완료될 가능성이 큽니다. 데이터베이스를 삭제하려면 SSOX를 사용하거나 database drop CLI 명령을 실행합니다.

구현 테스트

앱을 실행하고 다양한 페이지를 시도해 봅니다. 모든 항목이 이전과 같이 작동합니다.

SQL Server 개체 탐색기에서 데이터 연결/SchoolContext, 테이블을 차례로 확장하면 Student 및 Instructor 테이블이 Person 테이블로 바뀐 것을 확인할 수 있습니다. Person 테이블 디자이너를 열면 Student 및 Instructor 테이블에 있던 모든 열이 나타나는 것을 알 수 있습니다.

Person table in SSOX

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

Person table in SSOX - table data

코드 가져오기

완성된 애플리케이션을 다운로드하거나 확인합니다.

추가 리소스

Entity Framework Core의 상속에 대한 자세한 내용은 상속을 참조하세요.

다음 단계

이 자습서에서는 다음을 수행합니다.

  • 데이터베이스에 상속 매핑
  • Person 클래스 만들기
  • 강사 및 학생 업데이트
  • 모델에 개인 추가
  • 마이그레이션 만들기 및 업데이트
  • 구현 테스트

다양한 고급 Entity Framework 시나리오를 처리하는 방법을 알아보려면 다음 자습서로 진행합니다.