코드를 먼저 작성하고 이것을 기반으로 DB를 수정하는 것이 코드 퍼스트(Code First)입니다.
코드 퍼스트가 왜 필요하고 어떻게 사용하는지 알아봅시다.
프로젝트는 'ASP.NET Core 2'로 구성되어 있습니다.
전통적인 프로젝트-DB 관계는 DBA가 DB를 만들고 거기에 맞춰 프로그램을 만드는 형태였습니다.
그런데 장비의 가격은 낮아지고 사양은 높아지면서 점점 DB의 성능 이슈를 돈으로 때워도 큰 부담이 안 되는 시대가오게 됩니다!
그러니 프로그래머들이 임시로 DBA를 겸해서 작업하다가 성능 이슈가 발생하는 부분만(혹은 프로젝트가 시작 할 때나 끝날 때쯤) DBA가 붙어서 최적화시키는 프로세스가 정착되었습니다.
DB에 맞춰 프로그램을 만들게 되면 문제가 DB가 수정되면 해당 사항을 프로젝트에서 찾아서 한땀한땀 수정해야 한다는 것입니다.
그래서 나온 것이 ORM(Object-Relational Mapping)개념 입니다.
DB를 객체화해서 관리하겠다는 개념이죠.
DB가 객체화가 된다면 프로그래머 입장에서는 대상 DB가 뭐인지 신경 쓸 필요가 없습니다.
DB는 모델에 맞춰서 작성하면 되므로 MSSQL이든 MYSQL이든 오라클이든 이 모델에만 맞으면 동작하게 됩니다.
(물론 이걸 해내는 건 누군가가 만든 드라이버입니다 ㅎㅎㅎㅎ)
그러다가.....
'매번 DB에 맞춰 프로그램을 만드는 게 아니라 프로그램에 맞춰 DB를 만들면? 안되나?'
라는 생각이 들게 되죠.
이런 발상에서 나온 개념이 코드 퍼스트(Code First)입니다.
C#에서는 엔트리 프레임웍(EF, Entity Framework)이 'ORM' 역할을 합니다.
이제 프로젝트에서 모델을 만들고 이 모델을 기반으로 DB를 수정합니다.
그렇다는 것은 코드 퍼스트로 만들게 되면 어떤 DB엔진이던 상관없이 DB신경안쓰고 만들 수 있다는 것입니다!
프로젝트 생성 옵션은
닷넷 코어 2.2
웹 응용프로그램
WebAPI
입니다.
누겟에서
Microsoft.EntityFrameworkCore.Tools
를 찾아 설치합니다.
DB엔진에 따라 추가로 설치해야 할 프로바인더(provider) 다릅니다.
MSSQL : Microsoft.EntityFrameworkCore.SqlServer
MySql : MySql.Data.EntityFrameworkCore
Oracle : Oracle.EntityFrameworkCore
SQLite : Microsoft.EntityFrameworkCore.Sqlite
이 프로젝트에서 저는 MSSQL을 사용합니다.
하지만 위에서도 설명했다시피 어떤 DB엔진을 사용해도 상관없습니다.
단지 각 DB엔진의 종류와 버전에 맞는 EF를 찾아 설치해야 합니다.
'appsettings.json'파일을 열어 다음과 같이 커낵션 스트링을 추가해줍니다.
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"CoreCodeFirst": {
"DBType": "mssql",
"ConnectionString": "Server=[주소];DataBase=[데이터 베이스];UId=[아이디];pwd=[비밀번호]"
},
//"CoreCodeFirst": {
// "DBType": "sqlite",
// "ConnectionString": "Data Source=DBFile\\CcfTest.sqlite"
//},
"AllowedHosts": "*"
}
커낵션 스트링을 DB엔진 별로 약간씩 차이가 있습니다.
사용하는 DB엔진에 맞춰 수정하도록 합시다.
DB 종류를 구분하기 위해 'DBType'를 추가해 줍니다.
컨텍스트는 DB역할
모델은 DB의 테이블역할을
하게 됩니다.
불러온 DB정보를 저장해둘 'GlobalStatic' 클래스를 생성하여 다음과 같이 작성합니다.
public static class GlobalStatic
{
/// <summary>
/// db 타입
/// </summary>
public static string DBType = "";
/// <summary>
/// db 커낵트 스트링
/// </summary>
public static string DBString = "";
}
'Startup.cs'를 열어 'Startup()'을 다음과 같이 작성합니다.
public Startup(IConfiguration configuration)
{
Configuration = configuration;
//사용할 DB 정보
string sConnectStringSelect = "CoreCodeFirst";
GlobalStatic.DBType = Configuration[sConnectStringSelect + ":DBType"];
GlobalStatic.DBString = Configuration[sConnectStringSelect + ":ConnectionString"];
}
이렇게 하면 프로그램이 시작할때 'appsettings.json'파일에서 데이터를 읽어 'GlobalStatic'에 저장됩니다.
'ModelDB'폴더를 생성합니다.
'TestUser' 클래스를 추가합니다.
/// <summary>
/// 유저 가입 타입
/// </summary>
public enum UserJoinType
{
None = 0,
Normal,
VIP,
VVIP,
}
/// <summary>
/// 테스트용 유저 정보 모델
/// </summary>
public class TestUser
{
/// <summary>
/// 고유키
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long TestUserID { get; set; }
/// <summary>
/// 사인인에 사용하는 이메일
/// </summary>
public string Email { get; set; }
/// <summary>
/// 비밀번호
/// </summary>
public string Password { get; set; }
/// <summary>
/// 가입 날짜
/// </summary>
public DateTime JoinDate { get; set; }
/// <summary>
/// 가입 형태
/// </summary>
public UserJoinType JoinType { get; set; }
/// <summary>
/// 보유 금액
/// </summary>
public double Money { get; set; }
}
컨텍스트는 EF를 사용할 때 DB를 컨트롤하기 위한 객체입니다.
간단하게 말하자면 DB와 연결하여 동작하는 객체라고 보시면 됩니다.
'ModelDB'폴더에 'EFCoreCodeFirstSampleContext'를 생성하고 아래와 같이 작성합니다.
public class CoreCodeFirstContext : DbContext
{
/// <summary>
/// DB 컨택스트 생성
/// </summary>
/// <param name="options"></param>
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
switch (GlobalStatic.DBType)
{
case "sqlite":
options.UseSqlite(GlobalStatic.DBString);
break;
case "mysql":
//
break;
case "mssql":
default:
options.UseSqlServer(GlobalStatic.DBString);
break;
}
}
public DbSet<TestUser> TestUser { get; set; }
/// <summary>
/// 테이블이 생성될때 동작
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//'TestUser'에 기본 정보 추가
modelBuilder.Entity<TestUser>().HasData(new TestUser
{
idTestUser = 1
, Email = "test01@test.com"
, Password = "1111"
, JoinType = UserJoinType.VVIP
, JoinDate = new DateTime(2019, 10, 10)
, Money = 10.1
}
, new TestUser
{
idTestUser = 2
, Email = "test02@test.com"
, Password = "1111"
, JoinType = UserJoinType.Normal
, JoinDate = new DateTime(2019, 12, 10)
, Money = 1000.22
});
}
}
'OnConfiguring'에 DB컨택스트가 생성될 때 사용되는 코드가 들어갑니다.
'OnModelCreating'에 테이블이 생성될 때 동작하는 코드를 넣습니다.
'Startup.cs'를 열어 'ConfigureServices' 안에 위에서 만든 'CoreCodeFirstContext'를 연결해줍니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//db 컨택스트 연결
services.AddDbContext<CoreCodeFirstContext>();
}
코드 퍼스트의 동작 원리는 버전 관리시스템과 유사합니다.
마이그레이션(Migration) 정보를 생성하여 버전을 관리하고
이것을 DB에 업데이트하여 프로젝트의 모델 정보를 DB에 업데이트합니다.
지금은 DB만 생성이 되어 있고 내용은 없는 상태입니다.
마이그래이션을 생성하는 명령어는 다음과 같습니다.
Add-Migration [마이그레이션 이름]
패키지 관리자를 열고 다음 명령어를 실행합니다.
Add-Migration DB생성
마이그레이션 이름은 클래스 이름 규칙을 따릅니다.
띄어쓰기 같은 것이 안 된다는 것이죠.
'Migrations'폴더가 생성되고 마이그레이션 정보가 들어 있는 파일이 생성된 것을 확인 할 수 있습니다.
업데이트 명령어를 통해 DB에 적용할 마이그레이션 버전을 선택해야 합니다.
update-database [마이그레이션 이름]
다음 명령을 통해 아까 생성한 마이그레이션을 선택해 봅시다.
update-database DB생성
에러가 없다면 아래와 같이 'Done.'가 표시됩니다.
이제 DB를 보면 업데이트 된 것을 확인 할 수 있습니다.
모델을 수정하면 어떻게 동작하게 되는지 확인해 봅시다.
'TestUser'에 'Message'를 추가합니다.
/// <summary>
/// 메시지
/// </summary>
public string Message { get; set; }
아래 명령을 실행합니다.
Add-Migration TestUser_에_Message_추가
마이그레이션 정보가 생성된 것을 확인합니다.
다음 명령을 사용하여 DB에 적용해 봅시다.
update-database TestUser_에_Message_추가
DB 테이블이 변경된 것을 확인할 수 있습니다.
자동으로 생성된 '__EFMigrationsHistory'테이블을 확인하면 마이그레이션 정보도 남아 있습니다.
컬럼의 글자 수 제한이라던가 키와 관련된 내용은 필터를 추가해서 처리할 수 있습니다.
자세한 내용은 아래 링크를 확인해 주세요.
참고 : Microsoft docs - Entity Framework Core/모델 만들기 개요
'TestUser'모델의 'Message'는 지워줍니다.
'TestUserInfo'모델을 만들고 아래와 같이 작성합니다.
/// <summary>
/// 유저 상세 정보
/// </summary>
public class TestUserInfo
{
/// <summary>
/// 고유키
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long idTestUserInfo { get; set; }
/// <summary>
/// TestUser FK
/// </summary>
[ForeignKey("idTestUserForeignKey")]
public TestUser idTestUser { get; set; }
/// <summary>
/// 래벨.
/// 필수값
/// </summary>
[Required]
public int Lv { get; set; }
/// <summary>
/// 닉네임
/// 10자리
/// </summary>
[MaxLength(10)]
public string NickName { get; set; }
}
'EFCoreCodeFirstSampleContext'에 방금 추가한 모델을 선언해 줍니다.
public DbSet<TestUser> TestUser { get; set; }
public DbSet<TestUserInfo> TestUserInfo { get; set; }
마이그레이션을 생성해 봅시다.
이번에는 컬럼이 제거되어서 데이터가 손실될 수 있다는 경고가 표시됩니다.
생성한 마이그레이션을 업데이트해 봅시다.
필터가 컬럼 옵션이 되어 잘 작용 된 것을 볼 수 있습니다.
특정 테이블을 수동으로 관리하려면 제외시켜 놔야 합니다.
'TestIgnore.cs'를 생성하고 아래와 같이 작성합니다.
/// <summary>
/// 제외 테스트용 모델
/// </summary>
public class TestIgnore
{
/// <summary>
/// 고유키
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long idTestIgnore { get; set; }
[Required]
public int Count { get; set; }
}
'EFCoreCodeFirstSampleContext'에 방금 추가한 모델을 선언해 줍니다.
public DbSet<TestUser> TestUser { get; set; }
public DbSet<TestUserInfo> TestUserInfo { get; set; }
public DbSet<TestIgnore> TestIgnore { get; set; }
방금 추가한 모델을 제외하기 위해 'OnModelCreating'에 다음 코드를 추가합니다.
(이것은 테이블이나 속성에 '[NotMapped]'를 넣은 것과 동일합니다.)
//제외
modelBuilder.Ignore<TestIgnore>();
이제 마이그레이션을 생성하고 업데이트를 해보면 'TestIgnore'테이블이 생성되지 않을 걸 알 수 있습니다.
'Ignore'를 사용하면 매핑 자체가 안돼서 오류가 납니다.
테이블은 생성하지 않고(=기존 테이블을 사용하거나 수동으로 생성하는 경우) 매핑하려면
'OnModelCreating'에서 '.HasNoKey().ToView(null)'를 해줘야 합니다.
EF Core 3이전 버전
modelBuilder.Entity<TestIgnore>().HasNoKey().ToView(null);
EF Core 5 이후 버전
modelBuilder.Entity<TestIgnore>().ToTable(nameof(TestIgnore), t => t.ExcludeFromMigrations());
이제 마이그레이션을 해보면 테이블은 생성되지 않지만
데이터에 접근은 되는 것을 알 수 있습니다.
마이그레이션을 초기화하려면 다음과 같은 방법을 사용하면 됩니다.
1) DB 테이블 제거
Update-Database -Migration 0
이 명령어를 사용하면
DB의 마이그레이션 테이블이 제거됩니다.
2) 마이그레이션 폴더 제거
프로젝트의 마이그레이션 폴더를 지워줍니다.
아래 명령어를 사용하여 남아있는 마이그래이션이 있으면 제거해 줍니다.
Remove-Migration
3) 마이그래이션 재시작
Enable-Migrations
이 명령어를 사용하면 마이그레이션이 처음부터 시작됩니다.
4) 새 마이그래이션 시작하기
아래 명령을 사용하여 새 마이그래이션을 시작합니다.
Add-Migration Initialize
( 참고 : Rick Strahl's Web Log - Resetting Entity Framework Migrations to a clean Slate )
4-2) 찌꺼기 제거
만약 4번을 실행했는데 에러가 난다면 아래 명령어를 반복하여 'Migrations'폴더를 비워줍니다.
Remove-Migration
이 폴더가 비워지면 아래 명령을 사용하여 새 마이그래이션을 시작합니다.
Add-Migration Initialize -verbose
SQLite에서 마이그레이션시 다음과 같은 오류가 나오는 경우가 있습니다.
SQLite does not support this migration operation ('DropColumnOperation').
MSDN을 보면 SQLite는 마이그레이션에서 'DropColumn'를 지원하지 않습니다.
(참고 : Microsoft Docs - SQLite EF Core 데이터베이스 공급자 제한 사항 )
해결방법은 마이그레이션을 초기화하고......
다시 생성하는 방법뿐입니다
(위에 6. 마이그레이션 초기화 참고)
완성된 샘플 : Github dang-gun - EntityFrameworkSample/CoreCodeFirst/
보시면 알 수 있는 것이.....
생성된 테이블들이 최적화 돼있다는 보장은 없습니다 ㅎㅎㅎㅎㅎ
그럴 땐 제외시켜놓고 수동으로 관리하면 됩니다.
뭐....가능하면 수동으로 관리해야 할게 없는 게 맞긴 하겠지만 말이죠 ㅎㅎㅎㅎ