메모
EF6 이후 버전만 - 이 페이지에서 설명하는 기능, API 등이 Entity Framework 6에 도입되었습니다. 이전 버전을 사용하는 경우 일부 또는 모든 정보가 적용되지 않습니다.
Code First를 사용하는 경우 모델은 규칙 집합을 사용하여 클래스에서 계산됩니다. 기본 코드 첫 번째 규칙은 엔터티의 기본 키가 되는 속성, 엔터티가 매핑하는 테이블의 이름, 10진수 열의 정밀도 및 크기 조정과 같은 항목을 결정합니다.
경우에 따라 이러한 기본 설정이 모델에 적합하지 않으며, 데이터 주석 또는 Fluent API를 사용하여 많은 개별 엔터티를 구성하여 우회해야 합니다. 사용자 지정 코드 첫 번째 규칙을 사용하면 모델에 대한 구성 기본값을 제공하는 고유한 규칙을 정의할 수 있습니다. 이 연습에서는 다양한 유형의 사용자 지정 규칙과 각 규칙을 만드는 방법을 살펴봅니다.
Model-Based 관습
이 페이지에서는 사용자 지정 규칙에 대한 DbModelBuilder API를 다룹니다. 이 API는 대부분의 사용자 지정 규칙을 작성하는 데 충분해야 합니다. 그러나 최종 모델을 만든 후 조작하는 규칙인 모델 기반 규칙을 작성하여 고급 시나리오를 처리하는 기능도 있습니다. 자세한 내용은 모델 기반 규약을 참조하세요.
우리 모델
먼저 규칙과 함께 사용할 수 있는 간단한 모델을 정의해 보겠습니다. 프로젝트에 다음 클래스를 추가합니다.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
}
public class Product
{
public int Key { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public DateTime? ReleaseDate { get; set; }
public ProductCategory Category { get; set; }
}
public class ProductCategory
{
public int Key { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
사용자 지정 규칙 소개
키라는 속성을 해당 엔터티 형식의 기본 키로 구성하는 규칙을 작성해 보겠습니다.
모델 작성기에서 관례가 활성화되며, 이는 컨텍스트에서 OnModelCreating을 재정의하여 액세스할 수 있습니다. 다음과 같이 ProductContext 클래스를 업데이트합니다.
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
}
이제 모델에서 Key라는 모든 속성이 해당 엔터티의 기본 키로 구성됩니다.
구성하려는 속성 형식을 필터링하여 규칙을 보다 구체적으로 만들 수도 있습니다.
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
이렇게 하면 Key라는 모든 속성이 해당 엔터티의 기본 키로 구성되지만 정수인 경우에만 구성됩니다.
IsKey 메서드의 흥미로운 기능은 가산적이라는 것입니다. 즉, 여러 속성에서 IsKey를 호출하면 모두 복합 키의 일부가 됩니다. 한 가지 주의할 점은 키에 대해 여러 속성을 지정할 때 해당 속성에 대한 순서도 지정해야 한다는 것입니다. 아래와 같이 HasColumnOrder 메서드를 호출하여 이 작업을 수행할 수 있습니다.
modelBuilder.Properties<int>()
.Where(x => x.Name == "Key")
.Configure(x => x.IsKey().HasColumnOrder(1));
modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder(2));
이 코드는 int 키 열과 문자열 이름 열로 구성된 복합 키를 갖도록 모델의 형식을 구성합니다. 디자이너에서 모델을 보면 다음과 같습니다.
속성 규칙의 또 다른 예는 내 모델의 모든 DateTime 속성을 구성하여 datetime 대신 SQL Server의 datetime2 형식에 매핑하는 것입니다. 다음을 사용하여 이 작업을 수행할 수 있습니다.
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
컨벤션 클래스
규칙을 정의하는 또 다른 방법은 규칙 클래스를 사용하여 규칙을 캡슐화하는 것입니다. Convention 클래스를 사용하는 경우 System.Data.Entity.ModelConfiguration.Conventions 네임스페이스의 Convention 클래스에서 상속되는 형식을 만듭니다.
다음을 수행하여 앞에서 보여 준 datetime2 규칙을 사용하여 규칙 클래스를 만들 수 있습니다.
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
이 규칙을 사용하도록 EF에 지시하려면 OnModelCreating의 Conventions 컬렉션에 추가합니다. 단계별 설명을 따라왔다면 이 절차는 다음과 같이 설정해 볼 수 있습니다.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
여기서 볼 수 있듯이 규칙 컬렉션에 규칙의 인스턴스를 추가합니다. 컨벤션에서 상속받으면 팀 또는 프로젝트 간 컨벤션을 그룹화하고 공유할 수 있는 편리한 방법을 제공합니다. 예를 들어 모든 조직 프로젝트에서 사용하는 일반적인 규칙 집합이 있는 클래스 라이브러리를 사용할 수 있습니다.
사용자 지정 특성
규칙을 사용하는 또 다른 용도는 모델을 구성할 때 새 특성을 사용할 수 있도록 하는 것입니다. 이를 설명하기 위해 문자열 속성을 유니코드가 아닌 속성으로 표시하는 데 사용할 수 있는 특성을 만들어 보겠습니다.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{
}
이제 모델에 이 특성을 적용하는 규칙을 만들어 보겠습니다.
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
이 규칙을 사용하면 문자열 속성에 NonUnicode 특성을 추가할 수 있습니다. 즉, 데이터베이스의 열이 nvarchar 대신 varchar로 저장됩니다.
이 규칙에 대해 유의해야 할 한 가지는 문자열 속성 이외의 항목에 NonUnicode 특성을 배치하면 예외가 throw된다는 것입니다. 문자열 이외의 형식에서는 IsUnicode를 구성할 수 없으므로 이 작업을 수행합니다. 이 경우 문자열이 아닌 항목을 필터링할 수 있도록 규칙을 보다 구체적으로 만들 수 있습니다.
위의 규칙은 사용자 지정 특성을 정의하는 데 작동하지만, 특히 특성 클래스의 속성을 사용하려는 경우 훨씬 쉽게 사용할 수 있는 다른 API가 있습니다.
이 예제에서는 특성을 업데이트하고 IsUnicode 특성으로 변경하므로 다음과 같습니다.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
일단 이 속성을 가지고 나면, 속성이 유니코드여야 하는지 여부를 규약에 알리기 위해 특성에 bool을 설정할 수 있습니다. 다음과 같이 구성 클래스의 ClrProperty에 액세스하여 이미 가지고 있는 규칙에서 이 작업을 수행할 수 있습니다.
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
이는 충분히 쉽지만 규칙 API의 Having 메서드를 사용하여 이 작업을 수행하는 보다 간결한 방법이 있습니다. Having 메서드에는 Func<PropertyInfo 형식의 매개 변수가 있습니다. T> 는 PropertyInfo를 Where 메서드와 동일하게 허용하지만 개체를 반환해야 합니다. 반환된 개체가 null이면 속성이 구성되지 않습니다. 즉, Where와 마찬가지로 속성을 필터링할 수 있지만 반환된 개체도 캡처하여 Configure 메서드에 전달한다는 측면에서 다릅니다. 다음과 같이 작동합니다.
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
사용자 지정 특성은 Having 메서드를 사용하는 유일한 이유가 아니며, 형식 또는 속성을 구성할 때 필터링하는 항목에 대해 추론해야 하는 모든 곳에서 유용합니다.
형식 구성
지금까지 모든 규칙은 속성에 대한 것이었지만 모델에서 형식을 구성하기 위한 규칙 API의 또 다른 영역이 있습니다. 환경은 지금까지 본 규칙과 유사하지만 구성 내의 옵션은 속성 수준 대신 엔터티에 있습니다.
형식 수준 규칙이 유용할 수 있는 것 중 하나는 테이블 명명 규칙을 변경하여 EF 기본값과 다른 기존 스키마에 매핑하거나 다른 명명 규칙을 사용하여 새 데이터베이스를 만드는 것입니다. 이렇게 하려면 먼저 모델의 형식에 대해 TypeInfo를 수락하고 해당 형식의 테이블 이름을 반환할 수 있는 메서드가 필요합니다.
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
이 메서드는 타입을 입력받아 CamelCase 대신 밑줄을 사용한 소문자 문자열로 변환하여 반환합니다. 이 모델에서는 ProductCategory 클래스가 ProductCategories 대신 product_category 테이블에 매핑됩니다.
해당 메서드가 있으면 다음과 같은 규칙에서 호출할 수 있습니다.
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
이 규칙은 GetTableName 메서드에서 반환되는 테이블 이름에 매핑되도록 모델의 모든 형식을 구성합니다. 이 규칙은 Fluent API를 사용하여 모델의 각 엔터티에 대해 ToTable 메서드를 호출하는 것과 같습니다.
이에 대해 유의해야 할 한 가지는 ToTable EF를 호출할 때 테이블 이름을 결정할 때 일반적으로 수행하는 복수화 없이 정확한 테이블 이름으로 제공하는 문자열을 사용한다는 것입니다. 우리의 규칙에 따라 테이블 이름이 product_categories 대신 product_category인 이유입니다. 우리는 다원화 서비스를 직접 호출하여 우리의 규칙에서 그것을 해결할 수 있습니다.
다음 코드에서는 EF6에 추가된 종속성 확인 기능을 사용하여 EF가 사용했을 복수화 서비스를 검색하고 테이블 이름을 복수화합니다.
private string GetTableName(Type type)
{
var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();
var result = pluralizationService.Pluralize(type.Name);
result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
메모
GetService의 제네릭 버전은 System.Data.Entity.Infrastructure.DependencyResolution 네임스페이스의 확장 메서드입니다. 이를 사용하려면 using 문을 컨텍스트에 추가해야 합니다.
ToTable 및 상속
ToTable의 또 다른 중요한 측면은 형식을 지정된 테이블에 명시적으로 매핑하는 경우 EF에서 사용할 매핑 전략을 변경할 수 있다는 것입니다. 상속 계층의 모든 형식에 대해 ToTable을 호출하여 위에서와 같이 형식 이름을 테이블 이름으로 전달하는 경우 기본 TPH(Table-Per-Hierarchy) 매핑 전략을 TPT(Table-Per-Type)로 변경합니다. 이를 설명하는 가장 좋은 방법은 구체적인 예제입니다.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Manager : Employee
{
public string SectionManaged { get; set; }
}
기본적으로 직원과 관리자는 모두 데이터베이스의 동일한 테이블(직원)에 매핑됩니다. 테이블에는 각 행에 저장되는 인스턴스 유형을 알려주는 판별자 열이 있는 직원과 관리자가 모두 포함됩니다. 계층 구조에 대한 단일 테이블이 있으므로 TPH 매핑입니다. 그러나 두 클래스에서 ToTable을 호출하는 경우 각 형식에는 자체 테이블이 있으므로 TPT라고도 하는 자체 테이블에 각 형식이 매핑됩니다.
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
위의 코드는 다음과 같은 테이블 구조에 매핑됩니다.
이를 방지하고 다음과 같은 몇 가지 방법으로 기본 TPH 매핑을 유지할 수 있습니다.
- 계층 구조의 각 형식에 대해 동일한 테이블 이름을 사용하여 ToTable을 호출합니다.
- 계층 구조의 기본 클래스에만 ToTable을 호출하십시오. 이 예에서는 기본 클래스가 "Employee"입니다.
실행 순서
컨벤션은 Fluent API와 동일하게 마지막이 승리하는 방식으로 작동합니다. 즉, 동일한 속성의 동일한 옵션을 구성하는 두 개의 규칙을 작성하는 경우 마지막으로 실행할 규칙이 우선합니다. 예를 들어 아래 코드에서 모든 문자열의 최대 길이는 500으로 설정되지만 모델의 Name이라는 모든 속성을 최대 길이가 250으로 구성합니다.
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
최대 길이를 250으로 설정하는 규칙은 모든 문자열을 500으로 설정하는 규칙 이후이므로 모델의 Name이라는 모든 속성에는 MaxLength가 250이고 설명과 같은 다른 문자열은 500이 됩니다. 이러한 방식으로 규칙을 사용하면 모델의 형식 또는 속성에 대한 일반 규칙을 제공한 다음 다른 하위 집합에 대해 재정의할 수 있습니다.
Fluent API 및 데이터 주석을 사용하여 특정 경우에 규칙을 재정의할 수도 있습니다. 위의 예제에서 Fluent API를 사용하여 속성의 최대 길이를 설정한 경우 보다 구체적인 Fluent API가 보다 일반적인 구성 규칙보다 우선하기 때문에 규칙 앞이나 후에 배치할 수 있습니다.
내장된 표준
사용자 지정 규칙은 기본 Code First 규칙의 영향을 받을 수 있으므로 다른 규칙 전후에 실행할 규칙을 추가하는 것이 유용할 수 있습니다. 이렇게 하려면 파생된 DbContext에서 Conventions 컬렉션의 AddBefore 및 AddAfter 메서드를 사용할 수 있습니다. 다음 코드는 기본 제공 키 검색 규칙 전에 실행되도록 앞에서 만든 규칙 클래스를 추가합니다.
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
이는 기본 제공 규칙 전후에 실행해야 하는 규칙을 추가할 때 가장 많이 사용됩니다. 기본 제공 규칙 목록은 System.Data.Entity.ModelConfiguration.Conventions 네임스페이스에서 찾을 수 있습니다.
모델에 적용하지 않으려는 규칙을 제거할 수도 있습니다. 규칙을 제거하려면 Remove 메서드를 사용합니다. PluralizingTableNameConvention을 제거하는 예제는 다음과 같습니다.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
.NET