3. EF Core Model
EF Core는 여러 규칙을 나타내는 Convention과 테이블의 칼럼을 정의하는 annotation attribute, Fluent API의 조합을 통해 런타임에서 Entity Model을 생성하는 데 사용합니다. 때문에 클래스상에서 동작하는 모든 Action은 후에 실제 Database에 적용되는 Action으로 자동적으로 변환됩니다. 여기서 Entity Class는 테이블의 구조를, 클래스의 인스턴스는 테이블의 행을 나타냅니다.
(1) EF Core Convention
모델을 정의하기 위해서 사용할 수 있는 방법으로는 대게 3가지가 있는데 그 중 첫 번째 방법은 EF Core convention을 사용하는 것입니다. 번역하자면 '규칙'에 해당하는데 아래 규칙을 통해 Model을 정의하는 것입니다.
- 테이블의 이름은 DbContext에 있는 DbSet<T> 속성과 일치하는 것으로 추정합니다.
- 칼럼의 이름은 Entity Model 클래스에 있는 속성과 일치하는 것으로 추정합니다.
- 클래스에서 사용된 string Type은 실제 데이터베이스에서 nvarchar로 추정합니다.
- 클래스에서 사용된 int Type은 실제 데이터베이스에서 int로 추정합니다.
- 기본키는 Id 혹은 ID와 같은 이름의 속성이 됩니다. 예를 들어 Product와 같은 클래스에서 ProductId나 ProductID이름이 기본키의 속성이 됩니다. 만약 이 속성이 GUID나 int Type이라면 해당 열은 IDENTITY특성을 가진다고 추정합니다.
(2) EF Core Annotation Attribute
Convention은 실제 데이터베이스의 객체와 클래스를 완벽히 일치시키는 데에는 충분하지 않을 수 있습니다. 따라서 모델을 정의하는 2번째 방법인 Annotation Attribute를 알아둘 필요가 있습니다. Attribute는 우리말로 '속성'에 해당합니다.
아래는 보통 테이블에 적용할 수 있는 일반적인 Annotation Attribute를 나열한 것입니다.
속성 | 설명 |
[Required] | 값은 Null일 수 없습니다. |
[StringLength(50)] | 문자열의 길이는 최대50자입니다. |
[RegularExpression(정규식)] | 값은 지정한 정규식과 일치해야 합니다. |
[Column(TypeName = "money", Name = "UnitPrice")] | 컬럼의 Type과 이름을 지정합니다. |
예를 들어 위에서 스크립트를 통해 생성한 Northwind 데이터베이스의 Products테이블을 보면
CREATE TABLE [dbo].[Products](
[ProductId] [int] IDENTITY(1,1) NOT NULL,
[ProductName] [nvarchar](40) NOT NULL,
[SupplierId] [int] NULL,
[CategoryId] [int] NULL,
[QuantityPerUnit] [nvarchar](20) NULL,
[UnitPrice] [money] NULL,
[UnitsInStock] [smallint] NULL,
[UnitsOnOrder] [smallint] NULL,
[ReorderLevel] [smallint] NULL,
[Discontinued] [bit] NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
(
[ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
--이하 생략
클래스명을 동일하게 Products이름으로 생성할 수 있고
public class Products
{
}
최대 40자이면서 값이 Null이 될 수 없는 ProductName은 아래와 같은 방식으로 Annotation Attribute를 설정할 수 있습니다.
public class Products
{
[Required]
[StringLength(40)]
public string ProductName { get; set; }
}
.NET Type과 데이터베이스 Type 간에 명백히 일치하는 Type이 존재하지 않는 경우에도 Annotation Attribute를 사용할 수 있습니다. 예를 들어 Products테이블에서 UnitPrice의 Type이 money로 되어 있는 경우 이에 대응할 .NET Type이 존재하지 않으므로 이를 Annotation Attribute로 대체하고 대체 가능한 .NET Type을 명시하는 것입니다.
[Column(TypeName = "money")]
public decimal? UnitPrice { get; set; }
또 다른 예로 Categories테이블의 DDL을 보면
Description [NTEXT]
로 되어 있는걸 볼 수 있는데 이러한 경우에도 다음과 같이 Annotation Attribute를 적용해 줄 수 있습니다.
[Column(TypeName = "ntext")]
public string Description { get; set; }
(3) EF Core Fluent API
Fluent API는 모델을 정의하는 마지막 방법으로서 Attribute를 대체하여 사용할 수 있습니다. 예를 들어 ProductName을 정의하는 경우 2개의 Attribute를 사용했는데 이를 Fluent API로 대체하여 사용하려면 database context 클래스의 OnModelCreating 메서드 안에서 아래와 같은 방식으로 Fluent API를 구현할 수 있습니다.
modelBuilder.Entity<Product>()
.Property(product => product.ProductName)
.IsRequired()
.HasMaxLength(40);
(4) Fluent API를 통한 데이터 시드의 이해
데이터 시드는 초기 데이터 집합으로 데이터베이스를 채우는 프로세스를 말하며 Fluent API를 사용해 이를 구현하는 것입니다.
예를 들어 새로운 데이터베이스를 생성하는 과정에서 Product 테이블에 하나의 행을 포함시키고자 한다면 HasData()메서드를 사용해 데이터를 추가합니다.
modelBuilder.Entity<Product>()
.HasData(new Product
{
ProductId = 1,
ProductName = "Chai",
UnitPrice = 18.00
}
);
(5) EF Core model 만들기
위에서는 어떻게 Model을 정의하는지에 대해서 알아보았으므로 이제 Northwind 데이터베이스에 2개의 테이블을 추가하기 위한 모델을 실제로 만들어 보도록 하겠습니다.
지금 프로젝트에 Product.cs파일과 Category.cs라는 2개의 파일을 생성하고 각각의 파일에 아래와 같이 클래스를 정의합니다.
public class Product
{
}
public class Category
{
}
● Product와 Category 엔티티 클래스 정의하기
각각의 클래스는 entity model이 될 수 있으며 테이블에서 각 컬럼을 표현하는 데 사용될 수 있습니다. 실제 Northwind데이터베이스에서 Categories테이블은 4개의 컬럼으로 되어 있는데
CREATE TABLE [dbo].[Categories](
[CategoryId] [int] IDENTITY(1,1) NOT NULL,
[CategoryName] [nvarchar](15) NOT NULL,
[Description] [ntext] NULL,
[Picture] [image] NULL
)
위의 테이블에서 Pricute를 제외하고 Category테이블은 다음과 같이 정의할 수 있습니다.
using System.ComponentModel.DataAnnotations.Schema;
public class Category
{
public int CategoryId { get; set; }
public string? CategoryName { get; set; }
[Column(TypeName = "ntext")]
public string? Description { get; set; }
public virtual ICollection<Product> Products { get; set; }
public Category()
{
Products = new HashSet<Product>();
}
}
Products는 관련된 행에 대한 탐색속성을 제공하기 위한 것이며 Category() 생성자 메서드는 Products를 초기화하여 Product가 Category에서 추가될 수 있도록 합니다.
Product 클래스는 Products테이블을 나타내도록 하기 위한 것인데 본래 Products테이블에는 많은 수의 컬럼들이 존재하지만 지금은 대략 ProductId, ProductName, UnitPrice, UnitsInStock, Discontinued, CategoryId로 하여 6개의 컬럼만 정의할 것입니다.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace myapp;
public class Product
{
public int ProductId { get; set; }
[Required]
[StringLength(40)]
public string ProductName { get; set; } = null!;
[Column("UnitPrice", TypeName = "money")]
public decimal? Cost { get; set; }
[Column("UnitsInStock")]
public short? Stock { get; set; }
public bool Discontinued { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}
예제에서 Cost와 Stock는 Column속성을 통해 실제로 테이블에 적용될 Column의 다른 이름을 지정하고 있고 CategoryId는 Category와 관련되는데 product의 부모 Category와 연결시키는데 사용되며 Category 테이블과 외래 키를 형성하게 됩니다.
Category의 Products 와 Product의 Category는 virtual로 정의되어있습니다. 이는 EF Core가 속성을 override 하는 것을 허용하는 것이고 몇몇 필요한 확장 기능을 제공할 수 있도록 합니다.
(6) Northwind database context 클래스로 테이블 추가하기
DbContext로 상속받은 Northwind 클래스안에서는 최소 하나의 DbSet<T> Type을 정의해야 하는데 이들 속성이 곧 테이블을 나타냅니다. EF Core에게 테이블이 가진 각 컬럼을 알리기 위해 DbSet<T>속성은 Generic을 사용하여 각 테이블의 행을 나타냅니다.
Northwind 클래스는 선택적으로 OnModelCreating()이름의 오버라이드 된 메서드를 가질 수 있는데 이곳에서 Fluent API를 통해 Entity Class에서 속성(Attribute)이 사용되는 것을 대체할 수 있습니다.
using Microsoft.EntityFrameworkCore;
namespace myapp;
public class Northwind : DbContext
{
public DbSet<Category>? Categories { get; set; }
public DbSet<Product>? Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connection = "Data Source=.;Initial Catalog=Northwind;Integrated Security=true;MultipleActiveResultSets=true;";
optionsBuilder.UseSqlServer(connection);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>()
.Property(category => category.CategoryName)
.IsRequired()
.HasMaxLength(15);
}
}
(7) dotnet-ef
테스트를 위해 Console App생성과 실행하는단계에서 몇 번 dotnet 명령어를 사용했는데 이 명령어는 EF Core를 위해서도 사용될 수 있습니다. 즉, 디자인 타임에서 이전 모델에서 새로운 모델로의 마이그레이션을 생성하고 적용하거나 기존에 존재하는 데이터베이스로부터 모델을 위한 코드를 생성할 수도 있습니다.
dotnet-ef 도구는 자동으로 설치되지 않기 때문에 Global 혹은 Local 단위로 패키지를 설치할 수 있으며 이미 도구의 이전 버전이 설치된 경우라면 다음과 같은 방법으로 설치된 버전을 삭제할 수 있습니다.
우선 아래 명령을 통해 설치여부를 확인합니다.
dotnet tool list --global |
예제에서는 .NET Core 5.0 때의 버전이 설치되어 있다고 하므로 이를 삭제하고
dotnet tool uninstall --global dotnet-ef |
최신버전을 설치합니다.
dotnet tool install --global dotnet-ef --version 6.0.6 |
(8) Scaffolding Model
Scaffolding Model은 이미 존재하는 데이터베이스로부터 Model을 표현하는 클래스를 만드는 것이며 이를 Reverse Engineering이라고 합니다.
Reverse Engineering을 실행해 보기 위해 우선 프로젝트에서 Microsoft.EntityFrameworkCore.Design을 추가하고
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6" />
</ItemGroup>
Command-Line에서 아래와 같이 dotnet ef명령을 내려줍니다.
dotnet ef dbcontext scaffold "Server=.;user id=sa;password=1234;Database=Northwind;" Microsoft.EntityFrameworkCore.SqlServer --table Categories --table Products --output-dir Models --namespace myapp --data-annotations --context Northwind |
--table은 Reverse Engineering으로 가져올 테이블을 명시한 것이며 --output-dir은 클래스가 생성될 폴더를 지정한 것입니다. --namespace로는 클래스에서 붙일 namespace를 지정하고 --data-annotations로는 Fluent API와 함께 data annotation을 사용하도록 지정하였습니다.
프로젝트에서 Models폴더를 보면 명령으로 내린것처럼 Category.cs, Northwind.cs, Product.cs 이렇게 3개의 파일이 있는데 이들 파일을 열어보면 아무래도 이전에 임의로 만든 클래스와는 다르게 좀 더 세부적인 Attrubute가 더 추가되어 있는 걸 확인할 수 있습니다.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace myapp
{
[Index("CategoryName", Name = "CategoryName")]
public partial class Category
{
public Category()
{
Products = new HashSet<Product>();
}
[Key]
public int CategoryId { get; set; }
[StringLength(15)]
public string CategoryName { get; set; } = null!;
[Column(TypeName = "ntext")]
public string? Description { get; set; }
[Column(TypeName = "image")]
public byte[]? Picture { get; set; }
[InverseProperty("Category")]
public virtual ICollection<Product> Products { get; set; }
}
}
클래스의 Index는 EF Core 5.0에서 추가된 것으로 테이블에 index가 걸려있는걸 나타냅니다. 이전에는 단지 Fluent API로만 Index를 표현할 수 있었습니다.
만들어진 클래스의 이름은 Category인데 실제 테이블의 이름은 Categories입니다. 이러한 차이는 dotnet-ef가 Humanizer라는 제3자 라이브러리를 사용함으로써 Category라는 이름으로 클래스의 이름을 자동적으로 표현했기 때문입니다.
생성된 Entity Class를 보면 partial 키워드를 사용하여 만들어 졌음을 알 수 있는데 이는 추가적인 코드가 필요할 때 별도로 동일한 이름의 클래스를 생성할 수 있도록 하기 위함이고 dotnet-ef를 사용해 Entity Class를 재생성할 때 추가된 코드가 손실되는 것을 방지할 수 있도록 해줍니다.
Products속성에서는 [InverseProperty]가 사용되었습니다. 이것은 Product Entity Class에서 Category 속성과의 외래키 관계(foreign key relationship)를 정의하기 위한 것입니다.
Entity Class외에도 Northwind.cs파일과 같은 Class에서도 이전에 만들었던 Northwind.cs와는 다소 차이가 남을 볼 수 있습니다.
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace myapp
{
public partial class Northwind : DbContext
{
public Northwind()
{
}
public Northwind(DbContextOptions<Northwind> options)
: base(options)
{
}
public virtual DbSet<Category> Categories { get; set; } = null!;
public virtual DbSet<Product> Products { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
optionsBuilder.UseSqlServer("Server=.;user id=sa;password=1234;Database=Northwind;");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.ReorderLevel).HasDefaultValueSql("((0))");
entity.Property(e => e.UnitPrice).HasDefaultValueSql("((0))");
entity.Property(e => e.UnitsInStock).HasDefaultValueSql("((0))");
entity.Property(e => e.UnitsOnOrder).HasDefaultValueSql("((0))");
entity.HasOne(d => d.Category)
.WithMany(p => p.Products)
.HasForeignKey(d => d.CategoryId)
.HasConstraintName("FK_Products_Categories");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
우선 Northwind context 클래스도 parial로 되어 있습니다. 이 또한 임의의 추가적인 코드를 동일한 이름의 클래스로 만들 수 있도록 하기 위한 것입니다.
Northwind context 클래스는 또한 2개의 생성자 메서드를 가지고 있습니다. 이 중에서 매개변수를 갖는 생성자 함수는 runtime에서 다른 연결 문자열을 전달하는데 유용하게 사용될 수 있습니다.
Categories와 Products테이블을 표현하기 위한 DbSet<T> 2개의 속성은 null값을 설정하고 있습니다. 이는 Compiler가 코드를 분석하는 과정에서 위험(Warning)을 표시하지 않기 위한 것이며 해당 설정은 Runtime에서는 영향을 끼치지 않습니다.
OnConfiguring() 메서드에서는 생성자에서 지정된 options가 존재하지 않으면 데이터베이스 연결에 주어진 문자열을 사용하게 되어 있습니다. 또한 컴파일러가 연결 문자열에서 민감한 정보를 포함해서는 안된다는 것을 상기시키기 위한 warning도 같이 포함되어 있음을 볼 수 있습니다.
OnModelCreating() 메서드에서는 2개의 Entity Class를 설정하기 위해 Fluent API를 사용하고 OnModelCreatingPartial이라는 메서드를 호출하고 있습니다. 이것은 또 다른 Northwind 클래스를 생성하여 필요한 Fluent API 설정을 추가할 수 있도록 하기 위한 것입니다. 대부분의 Partial Class는 dotnet-ef도구를 사용해 코드를 재생성할 때 별도로 추가한 코드가 손실되는 것을 막고자 하는 목적이 큽니다. 왜냐하면 dotnet-ef는 코드를 재생성할 때 기존에 생성된 코드 파일을 삭제하고 다시 생성하는 방식을 사용하기 때문입니다.
(9) 사전규칙 모델 설정
SQLite를 위한 DateOnly와 TimeOnly Type의 지원과 함께 EF6에서 소개된 새로운 기능 중 하나는 사전규칙 모델(preconvention model)이라는 것입니다.
Model이 복잡해질 수록 Entity Type과 속성을 파악하는데 규칙(Convention)에 의존하게 되고 테이블과 컬럼(Colum)에 일치하여 모델 빌드되는 것이 더욱 어려워질 수 있습니다. 사전규칙 모델은 규칙이 모델을 분석하고 빌드하는 데 사용하는 대신 직접 규칙을 구성한다는 개념으로 작용합니다.
예를 들어 모든 string속성이 기본적으로 최대50자까지의 문자열만 가질 수 있다고 하거나 사용자 인터페이스를 구현하는 모든 속성은 매핑되지 않아야 한다는 규칙이 있다면 아래와 같이 해당 규칙을 지정할 수 있습니다.
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<string>().HaveMaxLength(50);
configurationBuilder.IgnoreAny<Interface>();
}
'.NET > C#' 카테고리의 다른 글
[C#] Entity Framework Core - 4. 데이터 조작과 트랜잭션 (0) | 2022.06.24 |
---|---|
[C#] Entity Framework Core - 3. 질의하기및 Pattern 로드 (0) | 2022.06.24 |
[C#] Entity Framework Core - 1. 시작/설정하기 (0) | 2022.06.24 |
[C#] 함수(메서드)의 실행과 디버깅및 테스팅 (0) | 2022.05.06 |
[C#] TCP/IP 통신 (0) | 2021.10.28 |