사용자 지정 Code First 규칙

참고 항목

EF6 이상만 - 이 페이지에서 다루는 기능, API 등은 Entity Framework 6에 도입되었습니다. 이전 버전을 사용하는 경우 이 정보의 일부 또는 전체가 적용되지 않습니다.

Code First를 사용하는 경우 모델은 규칙 집합을 통해 클래스에서 계산됩니다. 기본 Code First 규칙은 엔터티의 기본 키가 되는 속성, 엔터티가 매핑하는 테이블의 이름, 10진수 열의 정밀도 및 크기 조정과 같은 항목을 기본적으로 결정합니다.

경우에 따라 이러한 기본 규칙이 모델에 적합하지 않으며 데이터 주석 또는 흐름 API를 사용하여 많은 개별 엔터티를 구성하여 이러한 규칙을 해결해야 합니다. 사용자 지정 Code First 규칙을 사용하면 모델에 대한 구성 기본값을 제공하는 고유한 규칙을 정의할 수 있습니다. 이 연습에서는 다양한 유형의 사용자 지정 규칙 및 각 규칙을 만드는 방법을 살펴봅니다.

모델 기반 규칙

이 페이지에서는 사용자 지정 규칙에 대한 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; }
    }

 

사용자 지정 규칙 소개

Key라는 속성을 해당 엔터티 형식의 기본 키로 구성하는 규칙을 작성해 보겠습니다.

규칙은 모델 작성기에서 사용하도록 설정되며, 컨텍스트에서 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 Key 열 및 문자열 이름 열로 구성된 복합 키를 갖도록 모델의 형식을 구성합니다. 디자이너에서 모델을 보면 다음과 같습니다.

composite Key

속성 규칙의 또 다른 예는 내 모델의 모든 DateTime 속성을 구성하여 datetime 대신 SQL Server datetime2 형식에 매핑하는 것입니다. 이를 달성하려면 다음 중 하나를 수행합니다.

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

규칙 클래스

규칙을 정의하는 또 다른 방법은 규칙 클래스를 사용하여 규칙을 캡슐화하는 것입니다. 규칙 클래스를 사용하는 경우 System.Data.Entity.ModelConfiguration.Conventions 네임스페이스의 규칙 클래스에서 상속되는 형식을 만듭니다.

다음을 수행하여 앞에서 보여준 datetime2 규칙을 통해 규칙 클래스를 만들 수 있습니다.

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

이 규칙을 사용하도록 EF에 지시하려면 OnModelCreating의 규칙 컬렉션에 추가합니다. 연습과 함께 이를 수행한 경우 다음과 같이 표시됩니다.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

여기서 볼 수 있듯이 규칙의 인스턴스를 규칙 컬렉션에 추가합니다. 규칙에서 상속하면 팀 또는 프로젝트에서 규칙을 그룹화하고 공유하는 편리한 방법을 제공합니다. 예를 들어 모든 조직 프로젝트에서 사용하는 일반적인 규칙 집합이 있는 클래스 라이브러리를 사용할 수 있습니다.

 

사용자 지정 특성

규칙을 사용하는 또 다른 용도는 모델을 구성할 때 새 특성을 사용할 수 있도록 하는 것입니다. 이를 설명하기 위해 String 속성을 유니코드가 아닌 속성으로 표시하는 데 사용할 수 있는 특성을 만들어 보겠습니다.

    [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 메서드에서 반환되는 테이블 이름에 매핑되도록 모델의 모든 형식을 구성합니다. 이 규칙은 흐름 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(계층당 하나의 테이블) 매핑 전략을 TPT(형식당 하나의 테이블)로 변경합니다. 이를 설명하는 가장 좋은 방법은 다음과 같은 구체적인 예제입니다.

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

위의 코드는 다음과 같은 테이블 구조에 매핑됩니다.

tpt Example

이를 방지하고 다음과 같은 몇 가지 방법으로 기본 TPH 매핑을 유지할 수 있습니다.

  1. 계층 구조의 각 형식에 대해 동일한 테이블 이름을 사용하여 ToTable을 호출합니다.
  2. 이 예제에서는 직원인 계층의 기본 클래스에서만 ToTable을 호출합니다.

 

실행 순서

규칙은 흐름 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이 됩니다. 이러한 방식으로 규칙을 사용하면 모델의 형식 또는 속성에 대한 일반적인 규칙을 제공한 다음 다른 하위 집합에 대해 재정의할 수 있습니다.

흐름 API 및 데이터 주석을 사용하여 특정 경우에 규칙을 재정의할 수도 있습니다. 위의 예제에서 흐름 API를 사용하여 속성의 최대 길이를 설정한 경우 보다 구체적인 흐름 API가 보다 일반적인 구성 규칙을 통해 승리하기 때문에 규칙 앞이나 후에 배치할 수 있습니다.

 

기본 제공 규칙

사용자 지정 규칙은 기본 Code First 규칙의 영향을 받을 수 있으므로 다른 규칙 전후에 실행할 규칙을 추가하는 것이 유용할 수 있습니다. 이를 수행하려면 파생 DbContext에서 규칙 컬렉션의 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>();
    }