본문 바로가기
Entity Framework Core

.NET Core MVC] EF Core - 8. Entity 상속(table-per-hierachy inheritance)

by Fastlane 2023. 8. 31.
728x90
반응형

OOP에서 코드 재사용성을 높이기 위해 상속을 사용할 수 있다. 공통 properties를 갖는 Person base class를 상속받도록 Instructor, Student class를 수정해보자. 

 

웹페에지는 수정되지 않고, 코드만 수정하면 자동으로 database에 반영된다. 

 

Map inheritance to database

이러한 상속을 database에 구현하는 여러 방법이 있다.

  • TPH

students, instructors 모두의 정보를 포함하는 Person table을 갖을 수 있다. 

row가 어떤 type인지 식별하기 위한 discriminator column을 갖는다. 

하나의 table에서 상속 구조를 만드는 패턴을 table-per-hierachy inheritance라고 한다. 

  • TPT

다른 방법으로는, 상속 구조와 더 비슷한 모습을 갖는다. Person, Instructor, Student 테이블을 따로 갖는다. 

각 entity class를 위한 table을 갖는 패턴은 table-per-type inheritance라고 한다. 

  • TPC

다른 방법으로는 non-abstract type을 개별 테이블로 만드는 것이다. 상속된 properties를 포함한 클래스의 모든 properties를 테이블의 컬럼과 매칭하는 것이다. 이 패턴을 Table-per-Concrete Class 상속이라 한다. Person, Student, Instructor classes의 TPC 상속 구현을 하면, Student, Instructor 테이블은 상속 구현 이후에도 달라보이는 것이 없다. 

 

TPC, TPH 상속 패턴은 TPT 상속 패턴보다 성능이 좋다. TPT 패턴은 복잡한 조인 쿼리가 생기기 때문이다. 

 

이번 POST 에서는 TPH 패턴을 구현해보자. TPH가 EF Core에서 지원하는 유일한 상속 패턴이다. 

Person class를 만들고, Person을 상속받도록 Instructor, Student classes를 수정한 다음 DbContext에 새 class를 추가하고 migration해보자. 

 

Person class 생성

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

namespace WEB.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, Student 수정

Instructor.cs

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

namespace WEB.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 WEB.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을 model에 추가 

using WEB.Models;
using Microsoft.EntityFrameworkCore;


namespace WEB.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)
    {
        //db 생성 시, DbSet property name으로 테이블을 생성한다.
        //아래 코드를 추가하면, 테이블명을 지정할 수 있다.

        modelBuilder.Entity<Course>().ToTable("Course");
        modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
        modelBuilder.Entity<Student>().ToTable("Person");
        modelBuilder.Entity<Department>().ToTable("Department");
        modelBuilder.Entity<Instructor>().ToTable("Person");
        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 });

    }
}

migration

dotnet ef migrations add Inheritance

<timestamp>_Inheritance.cs 파일을 살펴보자. 

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropColumn(
        name: "FirstName",
        table: "Student");


    migrationBuilder.DropColumn(
        name: "LastName",
        table: "Student");


    migrationBuilder.DropColumn(
        name: "FirstName",
        table: "Instructor");


    migrationBuilder.DropColumn(
        name: "LastName",
        table: "Instructor");


    migrationBuilder.AlterColumn<int>(
        name: "ID",
        table: "Student",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .OldAnnotation("SqlServer:Identity", "1, 1");


    migrationBuilder.AlterColumn<int>(
        name: "ID",
        table: "Instructor",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .OldAnnotation("SqlServer:Identity", "1, 1");


    migrationBuilder.CreateTable(
        name: "Person",
        columns: table => new
        {
            ID = table.Column<int>(type: "int", nullable: false)
                .Annotation("SqlServer:Identity", "1, 1"),
            LastName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
            FirstName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Person", x => x.ID);
        });


    migrationBuilder.AddForeignKey(
        name: "FK_Instructor_Person_ID",
        table: "Instructor",
        column: "ID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);


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

up method 코드를 아래의 코드로 변경하자. 

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);
}

이 코드는 다음 처리르 한다. 

  • Enrollment 테이블의 외래키 제약조건을 삭제한다. 

  • Instructor 테이블 이름을 Person으로 바꾸고, 컬럼을 수정한다. 
  • student를 가리키는 FK 저장용으로 사용할 임시컬럼(oldId)를 추가한다. 
  • Student data를 Person 테이블에 insert 한다. 새로운 PK가 생성된다. Enrollment의 StudentId를 update한다. 
  • 임시컬럼(oldId)를 삭제한다. 
  • Student 테이블을 삭제한다. 
  • Enrollment 테이블의 Person table을 가리키는 외래키 제약조건과 인덱스를 생성한다. 

만일 PK type이 integer 대신 GUID를 사용했다면 student의 PK가 변경되지 않아도 되며, 몇가지 단계를 생략해도 된다. 

migration을 database에 적용하자. 

dotnet ef database update

 

728x90
반응형

댓글