자습서: 복잡한 데이터 모델 만들기 - ASP.NET MVC 및 EF Core 사용

이전 자습서에서는 세 가지 엔터티로 구성된 간단한 데이터 모델을 사용했습니다. 이 자습서에서는 더 많은 엔터티 및 관계를 추가하고, 서식 지정, 유효성 검사 및 데이터베이스 매핑 규칙을 지정하여 데이터 모델을 사용자 지정합니다.

완료되면 엔터티 클래스는 다음 그림에 표시된 완성된 데이터 모델을 구성하게 됩니다.

Entity diagram

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

  • 데이터 모델 사용자 지정
  • Student 엔터티 변경
  • Instructor 엔터티 만들기
  • OfficeAssignment 엔터티 만들기
  • Course 엔터티 수정
  • Department 엔터티 만들기
  • Enrollment 엔터티 수정
  • 데이터베이스 컨텍스트 업데이트
  • 테스트 데이터로 데이터베이스 시드
  • 마이그레이션 추가
  • 연결 문자열 변경
  • 데이터베이스 업데이트

필수 조건

데이터 모델 사용자 지정

이 섹션에서는 서식 지정, 유효성 검사 및 데이터베이스 매핑 규칙을 지정하는 특성을 사용하여 데이터 모델을 사용자 지정하는 방법을 배웁니다. 그런 다음, 이어지는 몇 개 섹션에서는 특성을 이미 만든 클래스에 추가하고, 모델에 나머지 엔터티 형식에 대한 새 클래스를 만들어 완벽한 학교 데이터 모델을 만듭니다.

DataType 특성

학생 등록 날짜의 경우, 이 필드에서 필요한 것은 날짜이지만 현재 모든 웹 페이지는 날짜와 함께 시간을 표시합니다. 데이터 주석 특성을 사용하면 데이터를 표시하는 모든 보기에서 표시 형식을 해결하는 하나의 코드 변경을 만들 수 있습니다. 수행 방법의 예제를 보려면 특성을 Student 클래스의 EnrollmentDate 속성에 추가합니다.

다음 예제와 같이 Models/Student.cs에서 System.ComponentModel.DataAnnotations 네임스페이스에 대한 using 문을 추가하고 EnrollmentDate 속성에 DataTypeDisplayFormat 특성을 추가합니다.

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

DataType 특성은 데이터베이스 내장 형식보다 구체적인 데이터 형식을 지정하는 데 사용됩니다. 이 경우에는 날짜 및 시간이 아닌 날짜만 추적하고자 합니다. DataType 열거형은 날짜, 시간, 전화 번호, 통화, 전자 메일 주소 등과 같은 많은 데이터 형식을 제공합니다. DataType 특성을 통해 응용 프로그램에서 자동으로 유형별 기능을 제공하도록 설정할 수도 있습니다. 예를 들어, DataType.EmailAddress에 대해 mailto: 링크를 만들고 HTML5를 지원하는 브라우저에서 DataType.Date에 대해 날짜 선택기를 제공할 수 있습니다. DataType 특성은 HTML 5 브라우저가 인식할 수 있는 HTML 5 data-(데이터 대시로 발음) 특성을 내보냅니다. DataType 특성은 유효성 검사를 제공하지 않습니다.

DataType.Date는 표시되는 날짜의 서식을 지정하지 않습니다. 기본적으로 데이터 필드는 서버의 CultureInfo의 기본 형식에 따라 표시됩니다.

DisplayFormat 특성은 날짜 형식을 명시적으로 지정하는 데 사용됩니다.

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

ApplyFormatInEditMode 설정은 값이 편집을 위해 텍스트 상자에 표시될 때 서식 지정도 적용되어야 함을 지정합니다. (통화 값의 경우 편집을 위한 텍스트 상자에 통화 기호를 표시하지 않는 등, 특정 필드에 대해서 이것이 필요하지 않을 수 있습니다.)

DisplayFormat 특성은 단독으로 사용될 수 있지만 일반적으로 DataType 특성도 사용하는 것이 좋습니다. DataType 특성은 데이터를 화면에 렌더링하는 방법과 반대로 데이터의 의미 체계를 전달하고 DisplayFormat으로 가져올 수 없는 다음과 같은 이점을 제공합니다.

  • 브라우저는 HTML5 기능을 활성화할 수 있습니다(예: 달력 컨트롤, 로캘에 적합한 통화 기호, 이메일 링크, 일부 클라이언트 쪽 입력 유효성 검사 등을 표시하기 위해).

  • 기본적으로 브라우저는 사용자의 로캘에 따른 올바른 서식을 사용하여 데이터를 렌더링합니다.

자세한 내용은 <입력> 태그 도우미 설명서를 참조하세요.

앱을 실행하고 학생 인덱스 페이지로 이동하여 등록 날짜에 대해 시간이 더 이상 표시되지 않음을 확인합니다. 학생 모델을 사용하는 다른 보기에도 동일하게 적용됩니다.

Students index page showing dates without times

StringLength 특성

특성을 사용하여 데이터 유효성 검사 규칙 및 유효성 검사 오류 메시지를 지정할 수 있습니다. StringLength 특성은 데이터베이스의 최대 길이를 설정하고, ASP.NET Core MVC에 대한 클라이언트 쪽 및 서버 쪽 유효성을 검사를 제공합니다. 이 특성의 최소 문자열 길이를 지정할 수도 있지만, 최소값은 데이터베이스 스키마에 영향을 주지 않습니다.

사용자가 이름을 50자 이하로 입력하였는지를 확인한다고 가정합니다. 이 제한 사항을 추가하려면 다음 예제와 같이 StringLength 특성을 LastNameFirstMidName 속성에 추가합니다.

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

StringLength 특성은 이름에 공백을 입력할 수 있습니다. RegularExpression 특성을 사용하여 입력에 제한을 적용할 수 있습니다. 예를 들어 다음 코드는 첫 번째 문자가 대문자여야 하고, 나머지 문자는 사전순이어야 합니다.

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

MaxLength 특성은 StringLength 특성과 비슷한 기능을 제공하지만, 클라이언트 쪽 유효성 검사는 제공하지 않습니다.

이제 데이터베이스 스키마에서 변경이 필요한 방식으로 데이터베이스 모델이 변경되었습니다. 애플리케이션 UI를 사용하여 데이터베이스에 추가했을 수도 있는 데이터 손실 없이 마이그레이션을 사용하여 스키마를 업데이트합니다.

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

dotnet ef migrations add MaxLengthOnNames
dotnet ef database update

migrations add 명령은 변경으로 인해 두 개의 열에 대한 최대 길이가 짧아질 수 있으므로 데이터 손실이 발생할 수 있음에 대해 경고합니다. 마이그레이션은 <timeStamp>_MaxLengthOnNames.cs라는 파일을 만듭니다. 이 파일에는 현재 데이터 모델과 일치하도록 데이터베이스를 업데이트하는 Up 메서드의 코드가 포함됩니다. database update 명령은 해당 코드를 실행합니다.

timestamp가 접두사로 사용된 마이그레이션 파일 이름이 마이그레이션을 요청하는 Entity Framework에서 사용됩니다. update-database 명령을 실행하기 전에 여러 마이그레이션을 만들 수 있습니다. 그런 다음, 모든 마이그레이션이 생성된 순서 대로 적용됩니다.

앱을 실행하고, 학생 탭을 선택하고, 새로 만들기를 클릭한 다음, 50자보다 긴 이름을 입력해 봅니다. 애플리케이션에서 이 작업을 수행하지 못하게 합니다.

열 특성

또한 특성을 사용하여 클래스 및 속성을 데이터베이스에 매핑하는 방법을 제어할 수 있습니다. 필드에 중간 이름이 포함될 수 있으므로 첫 번째 이름 필드에 이름 FirstMidName을 사용한 경우를 가정합니다. 하지만 데이터베이스에 임시 쿼리를 작성하는 사용자는 해당 이름이 익숙하기 때문에 데이터베이스 열 이름을 FirstName으로 하려고 합니다. 이 매핑을 수행하기 위해 Column 특성을 사용할 수 있습니다.

Column 특성은 데이터베이스를 만드는 시기를 지정하고 FirstMidName 속성에 매핑하는 Student 테이블의 열 이름은 FirstName이 됩니다. 즉, 코드가 Student.FirstMidName을 참조하는 경우 데이터는 Student 테이블의 FirstName 열에서 가져오거나 업데이트됩니다. 열 이름을 지정하지 않으면 속성 이름과 동일한 이름이 지정됩니다.

Student.cs 다음 강조 표시된 코드와 같이 파일에 문을 System.ComponentModel.DataAnnotations.Schema 추가하고 using 열 이름 특성을 FirstMidName 속성에 추가합니다.

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50)]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

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

Column 특성을 추가하면 SchoolContext를 지원하는 모델이 변경되므로 데이터베이스가 일치하지 않습니다.

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

dotnet ef migrations add ColumnFirstName
dotnet ef database update

SQL Server 개체 탐색기에서 학생 테이블을 두 번 클릭하여 학생 테이블 디자이너를 엽니다.

Students table in SSOX after migrations

처음 두 마이그레이션을 적용하기 전에 이름 열은 nvarchar(MAX) 형식이었습니다. 이제는 nvarchar(50)이며 열 이름이 FirstMidName에서 FirstName으로 변경되었습니다.

참고 항목

다음 섹션의 모든 엔터티 클래스 만들기를 완료하기 전에 컴파일을 시도하는 경우 컴파일러 오류가 발생할 수 있습니다.

Student 엔터티 변경

Student entity

에서 Models/Student.cs이전에 추가한 코드를 다음 코드로 바꿉다. 변경 내용은 강조 표시되어 있습니다.

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50)]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

필수 특성

Required 특성에서 이름 속성은 필수 필드입니다. Required 특성은 값 형식(DateTime, int, double, float 등)과 같은 null을 허용하지 않는 형식에 필요하지 않습니다. Null일 수 없는 형식은 자동으로 필수 필드로 처리됩니다.

MinimumLength가 적용되려면 Required 특성이 MinimumLength와 함께 사용되어야 합니다.

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

표시 특성

Display 특성은 텍스트 상자에 대한 캡션이 각 인스턴스의 속성 이름 대신 “First Name”, “Last Name”, “Full Name” 및 “Enrollment Date”여야 함을 지정합니다(단어를 분할하는 공백 없음).

FullName 계산된 속성

FullName은 다른 두 개의 속성을 연결하여 생성되는 값을 반환하는 계산된 속성입니다. 따라서 get 접근자만 있으며 FullName 열은 데이터베이스에 생성되지 않습니다.

Instructor 엔터티 만들기

Instructor entity

템플릿 코드를 다음 코드로 바꿔서 만듭니다 Models/Instructor.cs.

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

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { 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; }

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

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

학생과 강사 엔터티의 여러 속성이 동일하다는 것을 확인하세요. 이 시리즈의 뒷부분에 나오는 상속 구현 자습서에서는 이 코드를 리팩터링하여 중복을 제거합니다.

다음과 같이 HireDate 특성을 쓸 수 있도록 여러 특성을 한 줄에 배치합니다.

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

CourseAssignments 및 OfficeAssignment 탐색 속성

CourseAssignmentsOfficeAssignment 속성은 탐색 속성입니다.

강사는 여러 강좌를 가르칠 수 있으므로 CourseAssignments는 컬렉션으로 정의됩니다.

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

탐색 속성이 여러 엔터티를 포함할 수 있는 경우, 해당 형식은 항목이 추가, 삭제 및 업데이트될 수 있는 목록이어야 합니다. List<T> 또는 HashSet<T>와 같은 형식 또는 ICollection<T>를 지정할 수 있습니다. ICollection<T>를 지정하는 경우 EF는 기본적으로 HashSet<T> 컬렉션을 만듭니다.

이러한 항목은 다대다 관계에 대한 섹션의 아래쪽에 설명된 CourseAssignment 엔터티이기 때문입니다.

Contoso University 비즈니스 규칙에 따르면 강사는 최대 하나의 사무실만 가질 수 있으므로 OfficeAssignment 속성은 단일 OfficeAssignment 엔터티를 포함합니다(사무실이 할당되지 않은 경우 null일 수 있음).

public OfficeAssignment OfficeAssignment { get; set; }

OfficeAssignment 엔터티 만들기

OfficeAssignment entity

다음 코드를 사용하여 만듭니 Models/OfficeAssignment.cs 다.

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

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

        public Instructor Instructor { get; set; }
    }
}

키 특성

InstructorOfficeAssignment 엔터티 간에는 일 대 영 또는 일 대 일 관계가 있습니다. 사무실 할당은 할당된 강사에 관하여 존재하며, 따라서 해당 기본 키도 Instructor 엔터티에 대한 해당 외래 키입니다. 하지만, 해당 이름이 ID 또는 classnameID 명명 규칙을 따르지 않으므로 Entity Framework는 자동으로 InstructorID를 이 엔터티의 기본 키로 인식할 수 없습니다. 따라서 Key 특성은 키로 식별하는 데 사용됩니다.

[Key]
public int InstructorID { get; set; }

엔터티에 자체 기본 키가 있지만 classnameID 또는 ID가 아닌 다른 항목 속성의 이름을 지정하려는 경우 Key 특성을 사용할 수도 있습니다.

기본적으로 EF는 열이 관계 확인을 위한 것이기 때문에 키를 데이터베이스에서 생성되지 않은 것으로 처리합니다.

강사 탐색 속성

강사 엔터티에 null 허용 OfficeAssignment 탐색 속성이 있으므로(강사에 사무실이 할당되지 않을 수 있으므로), OfficeAssignment 엔터티에는 null 비허용 Instructor 탐색 속성이 있습니다(사무실 할당은 강사 없이는 존재할 수 없으므로, InstructorID는 null을 허용하지 않음). 강사 엔터티에 관련된 OfficeAssignment 엔터티가 있는 경우 각 엔터티는 해당 탐색 속성의 다른 엔터티에 대한 참조를 갖게 됩니다.

강사 탐색 속성에 [Required] 특성을 배치하여 관련된 강사가 반드시 있도록 지정할 수도 있지만, InstructorID 외래 키(이 테이블에 대한 키이기도 함)가 null을 허용하지 않으므로 이를 수행하지 않아도 됩니다.

Course 엔터티 수정

Course entity

에서 Models/Course.cs이전에 추가한 코드를 다음 코드로 바꿉다. 변경 내용은 강조 표시되어 있습니다.

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

namespace ContosoUniversity.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; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

강좌 엔터티에는 관련된 부서 엔터티를 가리키는 외래 키 속성 DepartmentID가 있으며, 여기에는 Department 탐색 속성이 있습니다.

Entity Framework는 관련된 엔터티에 대한 탐색 속성이 있는 경우 사용자가 외래 키 속성을 데이터 모델에 추가하지 않아도 됩니다. EF에서 자동으로 필요할 때마다 데이터베이스에 외래 키를 생성하고, 이에 대한 그림자 속성을 만듭니다. 하지만 데이터 모델에 외래 키가 있으면 더 간단하고 더 효율적으로 업데이트를 수행할 수 있습니다. 예를 들어, 편집할 Course 엔터티를 페치하는 경우 이를 로드하지 않으면 Department 엔터티가 null이므로, Course 엔터티를 업데이트할 때 먼저 Department 엔터티를 가져와야 합니다. 외래 키 속성 DepartmentID가 데이터 모델에 포함된 경우 업데이트하기 전에 Department 엔터티를 페치할 필요가 없습니다.

DatabaseGenerated 특성

CourseID 속성의 None 매개 변수가 있는 DatabaseGenerated 특성은 기본 키 값이 데이터베이스에서 생성되지 않고 사용자에 의해 제공되도록 지정합니다.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

기본적으로 Entity Framework은 기본 키 값이 데이터베이스에서 생성된다고 가정합니다. 이는 대부분의 시나리오에서 사용자가 원하는 것입니다. 그러나 Course 엔터티의 경우 한 부서에 지정한 1,000개 시리즈, 다른 부서에 지정한 2,000개 시리즈와 같은 사용자 지정 강좌 수를 사용하게 됩니다.

또한 행이 생성되거나 업데이트된 날짜를 기록하는 데 데이터베이스 열이 사용되는 경우에 DatabaseGenerated 특성을 사용하여 기본 값을 생성할 수 있습니다. 자세한 내용은 생성된 속성을 참조하세요.

외래 키 및 탐색 속성

Course 엔터티의 외래 키 속성 및 탐색 속성은 다음 관계를 반영합니다.

강좌는 한 부서에 할당되므로, 위에서 언급한 이유로 DepartmentID 외래 키와 Department 탐색 속성이 있습니다.

public int DepartmentID { get; set; }
public Department Department { get; set; }

강좌에는 등록된 학생이 여러 명 있을 수 있으므로 Enrollments 탐색 속성은 컬렉션입니다.

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

여러 강사가 한 강좌를 수업할 수 있으므로 CourseAssignments 탐색 속성은 컬렉션입니다(형식 CourseAssignment나중에 설명).

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

Department 엔터티 만들기

Department entity

다음 코드를 사용하여 만듭니 Models/Department.cs 다.

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

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

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

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

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

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

열 특성

앞서 Column 특성을 사용하여 열 이름 매핑을 변경했습니다. Department 엔터티에 대한 코드에서 Column 특성은 SQL 데이터 형식 매핑을 변경하는 데 사용되었으므로 열은 데이터베이스의 SQL Server money 형식을 사용하여 정의됩니다.

[Column(TypeName="money")]
public decimal Budget { get; set; }

Entity Framework이 속성에 대해 사용자가 정의한 CLR 형식에 따라 적절한 SQL Server 데이터 형식을 선택하기 때문에 열 매핑은 일반적으로 필요하지 않습니다. CLR decimal 형식은 SQL Server decimal 유형에 매핑됩니다. 하지만 이 경우 열에 통화 금액이 포함되므로 금액 데이터 형식이 더 적합합니다.

외래 키 및 탐색 속성

외래 키 및 탐색 속성은 다음과 같은 관계를 반영합니다.

부서는 관리자가 있을 수도 있고 없을 수도 있으며, 관리자는 항상 강사입니다. 따라서 InstructorID 속성은 강사 엔터티에 외래 키로 포함되며, 속성이 null 허용임을 표시하도록 int 형식이 지정된 후 물음표가 추가됩니다. 탐색 속성이 Administrator로 명명되나, 강사 엔터티를 포함합니다.

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

부서에는 강좌가 많이 있을 수 있으므로 강좌 탐색 속성이 있습니다.

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

참고 항목

규칙에 따라 Entity Framework는 null 비허용 외래 키 및 다대다 관계에 대한 하위 삭제를 사용하도록 허용합니다. 이는 마이그레이션을 추가하려고 하면 예외가 발생하는 순환 하위 삭제 규칙으로 이어질 수 있습니다. 예를 들어, Department.InstructorID 속성을 nullable로 정의하지 않은 경우 EF는 강사를 삭제할 때 부서를 삭제하도록 계단식 삭제 규칙을 구성합니다. 이는 원하는 결과가 아닙니다. 비즈니스 규칙에 null 비허용인 InstructorID 속성이 필요한 경우, 다음 흐름 API 문을 사용하여 관계에서 하위 삭제를 사용하지 않도록 설정해야 합니다.

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Enrollment 엔터티 수정

Enrollment entity

에서 Models/Enrollment.cs이전에 추가한 코드를 다음 코드로 바꿉다.

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

namespace ContosoUniversity.Models
{
    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; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

외래 키 및 탐색 속성

외래 키 속성 및 탐색 속성은 다음 관계를 반영합니다.

등록 레코드는 단일 강좌에 해당하므로 CourseID 외래 키 속성 및 Course 탐색 속성이 있습니다.

public int CourseID { get; set; }
public Course Course { get; set; }

등록 레코드는 단일 학생에 해당하므로 StudentID 외래 키 속성 및 Student 탐색 속성이 있습니다.

public int StudentID { get; set; }
public Student Student { get; set; }

다 대 다 관계

StudentCourse 엔터티 간에는 다 대 다 관계가 있으며, Enrollment 엔터티는 데이터베이스에서 ‘페이로드가 있는’ 다 대 다 조인 테이블로 작동합니다. ‘페이로드가 있다’는 것은 Enrollment 테이블에 조인된 테이블에 대한 외래 키 외에 추가 데이터가 포함되어 있다는 것을 의미합니다(이 경우 기본 키 및 Grade 속성).

다음 그림은 이러한 관계 모양을 엔터티 다이어그램으로 보여 줍니다. (이 다이어그램은 EF 6.x에 대한 Entity Framework 파워 도구를 사용하여 생성되었습니다. 다이어그램 만들기는 자습서 내용에 해당하지 않으며 그림으로만 사용됩니다.)

Student-Course many to many relationship

각 관계 줄에는 한쪽 끝에 1, 다른 한쪽 끝에는 별표(*)가 있으며, 이는 일대다 관계를 나타냅니다.

Enrollment 테이블에 등급 정보가 포함되지 않은 경우 두 개의 외래 키(CourseIDStudentID)를 포함해야 합니다. 그런 경우에는 데이터베이스에서 페이로드 없는 다대다 조인 테이블(또는 순수 조인 테이블)일 수 있습니다. InstructorCourse 엔터티에 그러한 종류의 다 대 다 관계가 있으며, 다음 단계는 페이로드가 없는 조인 테이블로 작동하는 엔터티 클래스를 만드는 것입니다.

EF Core는 다대다 관계에 암시적 조인 테이블을 지원하지만 이 자습서는 암시적 조인 테이블을 사용하도록 업데이트되지 않았습니다. 이 자습서의 업데이트된 Razor 페이지 버전인 다대다 관계를 참조하세요.

CourseAssignment 엔터티

CourseAssignment entity

다음 코드를 사용하여 만듭니 Models/CourseAssignment.cs 다.

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

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

조인 엔터티 이름

조인 테이블은 강사-강좌 간 다대다 관계에 대한 데이터베이스에 필요하며, 엔터티 집합으로 표시되어야 합니다. 이름은 조인 엔터티 EntityName1EntityName2로 지정하는 것이 일반적입니다. 이 경우 CourseInstructor가 됩니다. 그러나 관계를 설명하는 이름을 선택하는 것이 좋습니다. 데이터 모델은 단순하게 시작되며 나중에 자주 페이로드를 가져오는 페이로드 없는 조인을 사용하여 증가됩니다. 설명이 포함된 엔터티 이름으로 시작하는 경우 나중에 이름을 변경할 필요가 없습니다. 이상적으로 조인 엔터티는 비즈니스 도메인에서 고유의 자연스러운(가능한 한 단어) 이름을 갖습니다. 예를 들어 Books 및 Customers는 Ratings를 통해 연결될 수 있습니다. 이 관계의 경우 CourseAssignmentCourseInstructor보다 더 낫습니다.

복합 키

외래 키가 null을 허용하지 않고 고유하게 테이블의 각 행을 식별하기 때문에 별도 기본 키가 필요하지 않습니다. 및 CourseID 속성은 InstructorID 복합 기본 키로 작동해야 합니다. EF에서 복합 기본 키를 식별하는 유일한 방법은 흐름 API를 사용하는 것입니다(특성을 사용해서는 수행할 수 없음). 다음 섹션에 복합 기본 키를 구성하는 방법이 나와 있습니다.

복합 키는 한 강좌에 여러 행 및 한 강사에 여러 행을 가질 수 있는 반면, 동일한 강사 및 강좌에는 여러 행을 가질 수 없음을 확인합니다. Enrollment 조인 엔터티는 고유한 기본 키를 정의하므로 이러한 종류의 중복이 가능합니다. 그러한 중복을 방지하려면 외래 키 필드에서 고유한 인덱스를 추가하거나 CourseAssignment와 유사한 기본 복합 키로 Enrollment를 구성할 수 있습니다. 자세한 내용은 인덱스를 참조하세요.

데이터베이스 컨텍스트 업데이트

다음 강조 표시된 코드를 Data/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; }

        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<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

이 코드는 새 엔터티를 추가하고 CourseAssignment 엔터티의 복합 기본 키를 구성합니다.

흐름 API 대안 정보

DbContext 클래스에서 OnModelCreating 메서드의 코드는 흐름 API를 사용하여 EF 동작을 구성합니다. API는 종종 EF Core 설명서의 이 예제와 같이 일련의 메서드 호출을 단일 명령문으로 연결하여 사용되기 때문에 “흐름”이라고 부릅니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

이 자습서에서는 특성으로 작업할 수 없는 데이터베이스 매핑에 대해서만 흐름 API를 사용합니다. 그러나 대부분의 서식 지정, 유효성 검사 및 매핑 규칙을 지정하는 데 흐름 API를 사용할 수 있습니다. MinimumLength와 같은 일부 특성은 흐름 API를 통해 적용할 수 없습니다. 앞서 설명한 대로 MinimumLength는 스키마를 변경하지 않고, 클라이언트와 서버 쪽 유효성 검사만 적용합니다.

일부 개발자는 흐름 API를 단독으로 사용하는 것을 선호하므로 자신의 엔터티 클래스를 “정리”할 수 있습니다. 원하는 경우 특성과 흐름 API를 섞을 수 있으며, 흐름 API를 사용해야만 수행할 수 있는 몇 가지 사용자 지정이 있습니다. 하지만 일반적으로 이러한 두 가지 방법 중 하나를 선택하여 가능한 한 일관되게 사용하는 것이 좋습니다. 둘 다 사용하는 경우 충돌이 있을 때마다 흐름 API가 특성을 재정의합니다.

특성 대 흐름 API의 비교에 관한 자세한 내용은 구성 메서드를 참조하세요.

관계를 보여 주는 엔터티 다이어그램

다음 그림에는 Entity Framework 파워 도구가 완벽한 School 모델을 만드는 다이어그램이 나와 있습니다.

Entity diagram

여기에서 일 대 다 관계 줄(1~*) 외에도, InstructorOfficeAssignment 엔터티 간 일 대 영 또는 일 대 일 관계 줄(1~0..1)과 Instructor 및 Department 엔터티 간 영 대 다 또는 일 대 다 관계 줄(0..1~*)을 확인할 수 있습니다.

테스트 데이터로 데이터베이스 시드

만든 새 엔터티에 Data/DbInitializer.cs 대한 시드 데이터를 제공하기 위해 파일의 코드를 다음 코드로 바꿉 있습니다.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

첫 번째 자습서에서 보았듯이, 이 코드의 대부분은 단순히 새 엔터티 개체를 만들고 테스트를 위해 필요에 따라 속성에 샘플 데이터를 로드합니다. 어떻게 다대다 관계가 처리되는지 확인하세요. 코드는 EnrollmentsCourseAssignment 조인 엔터티 집합에서 엔터티를 만들어서 관계를 생성합니다.

마이그레이션 추가

변경 내용을 저장하고 프로젝트를 빌드합니다. 그런 다음, 프로젝트 폴더에서 명령 창을 열고 migrations add 명령을 입력합니다. (update-database 명령은 아직 실행하지 마십시오.)

dotnet ef migrations add ComplexDataModel

가능한 데이터 손실에 대한 경고가 표시됩니다.

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

이 시점에서 database update 명령 실행을 시도하면(아직 수행하지 말 것) 다음 오류가 발생합니다.

ALTER TABLE 문이 FOREIGN KEY 제약 조건 “FK_dbo.Course_dbo.Department_DepartmentID”와 충돌했습니다. 데이터베이스 “ContosoUniversity”, 테이블 “dbo.Department”, 열 ‘DepartmentID’에서 충돌이 발생합니다.

종종 기존 데이터와 마이그레이션을 실행하는 경우 외래 키 제약 조건을 만족하도록 데이터베이스에 스텁 데이터를 삽입해야 합니다. Up 메서드의 생성된 코드는 null 비허용 DepartmentID 외래 키를 Course 테이블에 추가합니다. 코드를 실행할 때 강좌 테이블에 이미 행이 있는 경우 SQL Server에서 null일 수 없는 열에 배치하는 값을 알지 못하므로 AddColumn 작업이 실패합니다. 이 자습서의 경우 새 데이터베이스에서 마이그레이션을 실행하지만, 프로덕션 애플리케이션에서는 마이그레이션이 기존 데이터를 처리하도록 해야 합니다. 따라서 다음 지침에는 수행 방법의 예가 나와 있습니다.

기존 데이터로 이 마이그레이션을 수행하려면 새 열에 기본 값을 제공하도록 코드를 변경하고 “Temp”라는 이름의 스텁 부서를 만들어 기본 부서로 작동하도록 합니다. 결과적으로, Up 메서드를 실행한 후에 기존 강좌 행은 모두 “Temp” 부서에 연결됩니다.

  • {timestamp}_ComplexDataModel.cs 파일을 엽니다.

  • DepartmentID 열을 강좌 테이블에 추가하는 코드 줄을 주석으로 처리합니다.

    migrationBuilder.AlterColumn<string>(
        name: "Title",
        table: "Course",
        maxLength: 50,
        nullable: true,
        oldClrType: typeof(string),
        oldNullable: true);
                
    //migrationBuilder.AddColumn<int>(
    //    name: "DepartmentID",
    //    table: "Course",
    //    nullable: false,
    //    defaultValue: 0);
    
  • 부서 테이블을 만드는 코드 뒤에 다음 강조 표시된 코드를 추가합니다.

    migrationBuilder.CreateTable(
        name: "Department",
        columns: table => new
        {
            DepartmentID = table.Column<int>(nullable: false)
                .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
            Budget = table.Column<decimal>(type: "money", nullable: false),
            InstructorID = table.Column<int>(nullable: true),
            Name = table.Column<string>(maxLength: 50, nullable: true),
            StartDate = table.Column<DateTime>(nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Department", x => x.DepartmentID);
            table.ForeignKey(
                name: "FK_Department_Instructor_InstructorID",
                column: x => x.InstructorID,
                principalTable: "Instructor",
                principalColumn: "ID",
                onDelete: ReferentialAction.Restrict);
        });
    
    migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    // Default value for FK points to department created above, with
    // defaultValue changed to 1 in following AddColumn statement.
    
    migrationBuilder.AddColumn<int>(
        name: "DepartmentID",
        table: "Course",
        nullable: false,
        defaultValue: 1);
    

프로덕션 애플리케이션에서 코드 또는 스크립트를 작성하여 부서 행을 추가하고 강좌 행을 새 부서 행에 연결합니다. 그런 다음에는 “Temp” 부서 또는 기본값이 Course.DepartmentID 열에 필요하지 않습니다.

변경 내용을 저장하고 프로젝트를 빌드합니다.

연결 문자열 변경

이제 새 엔터티에 대한 시드 데이터를 빈 데이터베이스에 추가하는 새 코드가 DbInitializer 클래스에 있습니다. EF가 비어 있는 새 데이터베이스를 만들도록 하려면 appsettings.json의 연결 문자열에서 데이터베이스의 이름을 ContosoUniversity3 또는 사용 중인 컴퓨터에서 사용한 적 없는 다른 이름으로 변경합니다.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

변경 내용을 appsettings.json에 저장합니다.

참고 항목

데이터베이스 이름을 변경하는 대신, 데이터베이스를 삭제할 수 있습니다. SSOX(SQL Server 개체 탐색기) 또는 database drop CLI 명령을 사용합니다.

dotnet ef database drop

데이터베이스 업데이트

데이터베이스 이름을 변경했거나 데이터베이스를 삭제한 후 명령 창에서 database update 명령을 실행하여 마이그레이션을 수행합니다.

dotnet ef database update

DbInitializer.Initialize 메서드로 이어지는 앱을 실행하여 새 데이터베이스를 실행하고 채웁니다.

이전에 수행한 SSOX에서 데이터베이스를 열고, 만들었던 테이블을 모두 볼 수 있도록 Tables 노드를 확장합니다. (아직 이전 시점의 SSOX가 열려 있는 경우 새로 고침 단추를 클릭합니다.)

Tables in SSOX

앱을 실행하여 데이터베이스를 시드하는 이니셜라이저 코드를 트리거합니다.

CourseAssignment 테이블을 마우스 오른쪽 단추로 클릭하고 데이터 보기를 선택하여 데이터가 있는지 확인합니다.

CourseAssignment data in SSOX

코드 가져오기

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

다음 단계

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

  • 데이터 모델 사용자 지정
  • Student 엔터티 변경
  • Instructor 엔터티 만들기
  • OfficeAssignment 엔터티 만들기
  • Course 엔터티 수정
  • Department 엔터티 만들기
  • Enrollment 엔터티 수정
  • 데이터베이스 컨텍스트 업데이트
  • 테스트 데이터로 데이터베이스 시드
  • 마이그레이션 추가
  • 연결 문자열 변경
  • 데이터베이스 업데이트

관련 데이터에 액세스하는 방법을 자세히 알아보려면 다음 자습서로 진행합니다.