[Entity Framework 6] 여러 종류 DB대응하기
엔티티 프레임워크 같은 프레임워크를 객체 관계형 매핑(Object–relational mapping), 줄여서 ORM이라고 부릅니다.
이런 프레임워크의 장점 중 하나가 약간의 작업만 하면 다양한 DB를 연결할 수 있다는 것입니다.
이 포스팅에서는 'SQLite'와 'MSSQL'를 따로따로 마이그레이션하고 사용하겠습니다.
다른 DB도 얼마든지 추가할 수 있습니다.
0. 프로젝트 생성
DB 테이블 모델과 컨텍스트(DbContext)를 관리할 프로젝트를 생성합니다.
여기에서 마이그레이션 버전관리가 이루어집니다.
'클래스 라이브러리'로 프로젝트를 생성합니다.
(프로젝트 이름 : EfMultiMigrations)
누겟에서 아래 종속성을 찾아 설치합니다.
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.SqlServer
0-1. 테스트용 모델 생성
테스트에 사용될 모델을 아래와 같이 생성합니다.
'DbData1Model.cs'
namespace ModelsDB;
public class DbData1Model
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int idDbData1Model { get; set; }
public string Name { get; set; } = string.Empty;
public int Age { get; set; } = 0;
}
'DbData2Model.cs'
namespace ModelsDB;
public class DbData2Model
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int idDbData2Model { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
0-2. DB정보 전달용 글로벌(Global) 변수
DB 정보를 전달받을 방법이 없는 건지...제가 못 찾는 건지......
그래서 전역 변수로 DB정보를 받아서 사용하도록 구현하였습니다.
'ModelDllGlobal.cs' 파일을 생성하고 아래와 같이 코드를 넣습니다.
namespace EfMultiMigrations;
public static class ModelDllGlobal
{
/// <summary>
/// 대상 DB 타입
/// </summary>
public static TargetDbType DbType = TargetDbType.None;
/// <summary>
/// 사용할 커낵트 스트링
/// </summary>
public static string DbConnectString = string.Empty;
}
1. 컨텍스트 만들기
DB 종류별로 DB컨텍스트를 만들어야 합니다.
마이그레이션은 메인 컨텍스트를 상속받은 각 DB컨텍스트 별로 해야 합니다.
마이그레이션이 컨텍스트를 기준으로 작성되기 때문입니다.
하지만 접근은 메인 컨텍스트에서 합니다.
1-1. 메인 컨텍스트 만들기
'DbModelContext.cs' 파일을 생성하고 아래와 같이 작성합니다.
namespace ModelsDB;
public class DbModelContext : DbContext
{
#pragma warning disable CS8618 // 생성자를 종료할 때 null을 허용하지 않는 필드에 null이 아닌 값을 포함해야 합니다. null 허용으로 선언해 보세요.
public DbModelContext(DbContextOptions<DbModelContext> options)
: base(options)
{
}
public DbModelContext()
{
}
#pragma warning restore CS8618 // 생성자를 종료할 때 null을 허용하지 않는 필드에 null이 아닌 값을 포함해야 합니다. null 허용으로 선언해 보세요.
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
switch (ModelDllGlobal.DbType)
{
case TargetDbType.Mssql:
options.UseSqlServer(ModelDllGlobal.DbConnectString);
break;
case TargetDbType.Sqlite:
options.UseSqlite(ModelDllGlobal.DbConnectString);
break;
}
}
public DbSet<DbData1Model> DbData1Model { get; set; }
public DbSet<DbData2Model> DbData2Model { get; set; }
/// <summary>
///
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DbData1Model>().HasData(
new DbData1Model
{
idDbData1Model = 1,
Name = "Test",
Age = 1,
});
modelBuilder.Entity<DbData2Model>().HasData(
new DbData2Model
{
idDbData2Model = 1,
FirstName = "T",
LastName = "est",
});
}
}
17번 줄 : 컨택스트가 연결될 때 연결할 DB 정보를 전달할 함수입니다.
40번 줄 : 테이블이 처음 생성될 때 넣어줄 데이터를 설정합니다.
1-2. 'SQLite' 컨텍스트 만들기
위에서 만든 'DbModelContext'를 상속받습니다.
'DbModel_SqliteContext.cs'파일을 생성하고 아래 코드를 넣어줍니다.
namespace ModelsDB;
public class DbModel_SqliteContext : DbModelContext
{
}
1-3. 'MSSQL' 컨텍스트 만들기
'DbModel_MssqlContext.cs'파일을 생성하고 아래 코드를 넣어줍니다.
namespace ModelsDB;
public class DbModel_MssqlContext : DbModelContext
{
}
2. 마이그레이션 하기
EF 명령어에
-Context [사용할 컨텍스트]
를 추가하여 관리할 컨텍스트를 지정할 수 있습니다.
예>
Remove-Migration -Context ModelsDB.DbModel_SqliteContext
2-1. 마이그레이션 준비
마이그레이션을 생성하기 전에 전역 변수인
'ModelDllGlobal.DbType'에 사용할 DB 타입을 지정하고
'ModelDllGlobal.DbConnectString'에 사용할 DB의 커낵션 스트링(Connect string)을 지정해 줍니다.
예를 들면 아래 코드는 'SQLite'를 사용하기 위해 전역변수를 수정한 것입니다.
/// <summary>
/// 대상 DB 타입
/// </summary>
public static TargetDbType DbType = TargetDbType.Sqlite;
/// <summary>
/// 사용할 커낵트 스트링
/// </summary>
public static string DbConnectString = "Data Source=Test.db";
2-2. 'SQLite' 마이그레이션 생성하기
DB정보를 지정하고
'패키지 관리자 콘솔(package manager console, PM)'을 명령어를 넣어
마이그레이션을 생성합니다.
//SQLite 지정
public static TargetDbType DbType = TargetDbType.Sqlite;
public static string DbConnectString = "Data Source=Test.db";
//패키지 관리자 콘솔에 아래 명령어를 넣는다.
//Sqlite용 마이그레이션
Add-Migration InitialCreate -Context ModelsDB.DbModel_SqliteContext -OutputDir Migrations/Sqlite
2-3. 'MSSQL' 마이그레이션
DB정보를 지정하고
'패키지 관리자 콘솔(package manager console, PM)'을 명령어를 넣어
마이그레이션을 생성합니다.
//MSSQL 지정
public static TargetDbType DbType = TargetDbType.Mssql;
public static string DbConnectString = "Server=[주소];DataBase=[데이터 베이스];UId=[아이디];pwd=[비밀번호]";
//패키지 관리자 콘솔에 아래 명령어를 넣는다.
//MSSQL용 마이그레이션
Add-Migration InitialCreate -Context ModelsDB.DbModel_MssqlContext -OutputDir Migrations/Mssql
마이그레이션이 아래와 같이 생성됩니다.
오류
만약
An error occurred while accessing the Microsoft.Extensions.Hosting services when do first migrations
에러가 난다면 'DbContextFactory'를 추가하여 막을 수 있습니다.
(참고 : stackoverflow - An error occurred while accessing the Microsoft.Extensions.Hosting services when do first migrations - Shervin Ivari님 답변)
3. 마이그레이션 적용
PM명령어로 마이그레이션을 적용할 수도 있고,
Update-Database -Context ModelsDB.DbModel_MssqlContext
비하인드 코드로 적용해도 됩니다.
//SQLite를 사용하는 경우
using (DbModel_SqliteContext db1 = new DbModel_SqliteContext())
{
db1.Database.Migrate();
}
------------------------------------
//MSSQL을 사용하는 경우
using (DbModel_MssqlContext db1 = new DbModel_MssqlContext())
{
db1.Database.Migrate();
}
4. 마이그레이션 정보 입력
위와 같은 방법의 불편한 점은 매번 전역변수를 수정해야 한다는 것입니다.
그나마 조금 쉽게 관리하려면 몇 가지 방법이 있습니다.
예전에는 'ConnectionStringName'를 전달하면 됐었는데.....
지금은 안 됩니다.
(EF5, EF6 기준)
4-1. 'IDesignTimeDbContextFactory'를 만드는 방법
'IDesignTimeDbContextFactory'를 만들고 'CreateDbContext'함수에서 전역 변수에 값을 넣어주면 됩니다.
'IDesignTimeDbContextFactory'는 EF명령어를 사용할 때만 동작하므로 런타임에서 동작한다는 걱정은 안 해도 됩니다.
이 방법이 제가 원하는 방법에 가장 가까운데...
EF명령어를 넣으면 자동으로 사용할 DB를 선택할 수 있도록 할 수 있습니다.
단, DB정보를 코드에 픽스하기 싫다면 이 프로젝트(여기서는 'EfMultiMigrations')를 참조하는 별도의 프로젝트를 생성하고
새 프로젝트에서 별도의 출력 파일을 만들어(예> server.json) 이 파일을 읽어서 처리하도록 해야 합니다.
여기서 선택지가 몇 개 있는데
1) 'ModelDllGlobal.DbConnectString'를 외부에서만 관리하기
'DbContext'에서는 'ModelDllGlobal'와 관련된 내용을 전혀 수정하지 않는 방법입니다.
이 방법의 단점은 직접 'ModelDllGlobal'파일을 수정하여 마이그레이션을 관리해야 합니다.
2) 'IDesignTimeDbContextFactory'에서 관리하기
'IDesignTimeDbContextFactory'를 상속받은 클래스는 EF명령어를 직접 호출했을 때만 관리된다는 특성을 이용하여
마이그레이션 할 때만 필요한 정보를 넣는 방식으로 사용합니다.
이렇게 되면 마이그래이션할때 대상 DB와 사용DB가 달라도 관리에 문제가 생기지 않습니다.
대신 사용할 때는 따로 'ModelDllGlobal.DbConnectString'의 내용을 넣어야 합니다.
3) 'DbContext'에서 관리하기
'DbContext'를 상속받은 개체가 초기화될 때 'ModelDllGlobal.DbConnectString'의 내용을 넣는 방식입니다.
직접 EF 명령어를 호출할 때는 옵션이 있는 생성자가, 보통 때는 매개변수가 없는 생성자가 사용됩니다.
이러한 특성을 사용하여 위 2가지 방법의 장단점을 섞어서 사용할 수 있습니다.
이 포스팅에서는 ' 3) 'DbContext'에서 관리하기 '방법을 사용하되
직접 EF명령어를 사용할 때만 자동으로 DB정보를 입력하고,
DB에 접근하기 전에 'ModelDllGlobal.DbConnectString'를 초기화하는 방법을 사용하였습니다.
이렇게 하면 매번 DB정보를 불러오는 오버헤드가 발생하지 않으면서도 필요할 때 큰 작업없이 'DbContext'를 수정할 수 있습니다.
4-1-1. 'DbModel_SqliteContext' 수정
'DbModel_SqliteContext'클래스를 아래와 같이 수정합니다.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using MultiMigrationsSample;
using MultiMigrationsSample.Models;
namespace ModelsDB;
/// <summary>
/// Sqlite전용 컨텍스트
/// </summary>
/// <remarks>
/// Add-Migration InitialCreate -Context DbModel_SqliteContext -OutputDir Migrations/Sqlite
/// Remove-Migration -Context DbModel_SqliteContext
/// Update-Database -Context DbModel_SqliteContext -Migration 0
/// Update-Database -Context DbModel_SqliteContext
/// </remarks>
public class DbModel_SqliteContext : DbModelContext
{
public DbModel_SqliteContext(DbContextOptions<DbModelContext> options)
: base(options)
{
GlobalDb.DbType = TargetDbType.Sqlite;
if (string.Empty == GlobalDb.DbConnectString)
{
GlobalDb.DbConnectString = "Data Source=Test.db";
}
}
public DbModel_SqliteContext()
{
}
}
/// <summary>
/// https://stackoverflow.com/a/60602620/6725889
/// </summary>
public class DbContext_SqliteFactory : IDesignTimeDbContextFactory<DbModel_SqliteContext>
{
public DbModel_SqliteContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<DbModelContext> optionsBuilder
= new DbContextOptionsBuilder<DbModelContext>();
return new DbModel_SqliteContext(optionsBuilder.Options);
}
}
4-1-2. 'DbModel_MssqlContext' 수정
'DbModel_MssqlContext'클래스를 아래와 같이 수정합니다.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using MultiMigrationsSample;
using MultiMigrationsSample.Models;
using Newtonsoft.Json;
namespace ModelsDB;
/// <summary>
/// mssql전용 컨텍스트
/// </summary>
///<remarks>
/// Add-Migration InitialCreate -Context DbModel_MssqlContext -OutputDir Migrations/Mssql
/// Remove-Migration -Context DbModel_MssqlContext
/// Update-Database -Context DbModel_MssqlContext
/// Update-Database -Context DbModel_MssqlContext -Migration 0
///</remarks>
public class DbModel_MssqlContext : DbModelContext
{
public DbModel_MssqlContext(DbContextOptions<DbModelContext> options)
: base(options)
{
if (string.Empty == GlobalDb.DbConnectString)
{
//설정 파일 읽기
string sJson = File.ReadAllText("SettingInfo_gitignore.json");
SettingInfoModel? loadSetting = JsonConvert.DeserializeObject<SettingInfoModel>(sJson);
GlobalDb.DbType = TargetDbType.Mssql;
GlobalDb.DbConnectString = loadSetting!.ConnectionString_Mssql;
}
}
public DbModel_MssqlContext()
{
}
}
public class DbContext_MssqlFactory : IDesignTimeDbContextFactory<DbModel_MssqlContext>
{
public DbModel_MssqlContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<DbModelContext> optionsBuilder
= new DbContextOptionsBuilder<DbModelContext>();
return new DbModel_MssqlContext(optionsBuilder.Options);
}
}
4-1-3. 마이그레이션 진행
이제 EF로 마이그레이션 명령을 사용하면 선택된 컨텍스트에 따라 자동으로 DB를 선택하여 마이그레이션을 생성하는 것을 볼 수 있습니다.
4-2. 전역변수를 변경하는 방법
이 프로젝트를 참조하는 별도의 프로젝트를 만들고
이 프로젝트에서 전역변수를 수동으로 관리하는 것입니다.
이 별도의 프로젝트는 'ASP.NET Core Web API'를 추천합니다.
ASP.NET의 라이프사이클상 DB컨텍스트가 초기화되는 타이밍이 뒤에 있어서 전역변수를 사용하는 경우 미리 전역변수를 수정해둘 수 있습니다.
다른 프로젝트 형식은 각각의 라이프 사이클에 따라 미리 수정이 불가능한 경우가 있어서 전역변수가 기본값을 가지는 경우가 있습니다.
(대표적인 게 콘솔 애플리케이션)
전역 변수를 관리하는 코드는 수작업해야 한다는 단점은 여전합니다.
(이걸 자동화하는 것은 다음 포스팅에서 다루겠습니다.)
단지 관리할 코드양이 줄어들어서 실수할 확률이 줄어들 뿐이죠.
프로젝트를 클래스 라이브러리로 만들었다면 새로운 프로젝트를 생성하고
전에 만들었던 참조한 후 진입점에 아래 코드를 넣어야 합니다.
ModelDllGlobal.DbType = TargetDbType.Sqlite;
//ModelDllGlobal.DbType = TargetDbType.Mssql;
switch (ModelDllGlobal.DbType)
{
case TargetDbType.Sqlite:
ModelDllGlobal.DbConnectString = "Data Source=Test.db";
break;
case TargetDbType.Mssql:
ModelDllGlobal.DbConnectString = "Server=[주소];DataBase=[데이터 베이스];UId=[아이디];pwd=[비밀번호]";
break;
}
시작 프로젝트를 새로 생성한 프로젝트로 해주고
'패키지 관리자 콘솔'은 마이그레이션 정보가 있는 프로젝트로 해줍니다.
('DbContext'가 있는 프로젝트가 다른 프로젝트인 경우. 예> 클래스 라이브러리 참조)
이제 'SQLite'의 마이그레이션을 생성하려면
ModelDllGlobal.DbType = TargetDbType.Sqlite;
로 설정하고 EF 명령어를 사용하고
이제 'MSSQL'의 마이그레이션을 생성하려면
ModelDllGlobal.DbType = TargetDbType.Mssql;
로 설정하고 EF 명령어를 사용하면 됩니다.
단, '4-1. 'IDesignTimeDbContextFactory'를 만드는 방법'에서 '2), 3)' 방법을 사용했다면
명령어만으로도 'ModelDllGlobal.DbType'가 설정되므로 직접 수정할 필요는 없습니다.
4-3. 빌드 구성 추가하는 방법
빌드구성을 추가하고
프로젝트 속성 > 일반 > 조건부 컴파일 기호
를 지정하여 관리하는 방법이 있습니다.
빌드구성은 눈에 확 띄기 때문에 실수할 일이 줄어든다는 장점이 있습니다.
단점은 한 솔루션에 프로젝트가 많으면 이거 하나 때문에 빌드 구성을 매번 바꿔서 빌드해야 한다는 것이죠.
4-4. 프로젝트를 따로 관리하는 방법
각각의 DB별로 프로젝트를 따로 생성하는 방법입니다.
- 'SQLite' 프로젝트
- 'MSSQL' 프로젝트
이런 식으로 말이죠.
마이그레이션을 생성할 때 시작 프로젝트를 설정하고 EF명령어를 사용하는 방법입니다.
이것도 눈에 잘 띄는 방법이라 실수가 적고,
각 DB별로 해야 할 작업이 많다거나 문서로 같은 것을 관리할 생각이면 괜찮은 방법입니다.
5. DB 정보 파일 관리
'ASP.NET Core Web API'프로젝트를 사용하여 전역변수를 수정하는 것과 같은 방법을 사용하지 않으면 전역변수에 DB정보를 전달할 방법은 코드에 픽스하는 방법뿐입니다.
이때 사용하는 방법이 별도 파일을 이용하는 방법입니다.
저는 'json'파일을 이용할 예정이라 누겟에서 'Newtonsoft.Json'를 찾아 설치해줍니다.
EfMultiMigrations > Models
에 'SettingInfoModel.cs'를 생성하고 아래 코드를 넣어줍시다.
namespace EfMultiMigrations.Models;
/// <summary>
/// 이 프로젝트에서 사용할 설정 모델
/// </summary>
public class SettingInfoModel
{
/// <summary>
/// mssql에 사용할 연결 문자열
/// </summary>
public string ConnectionString_Mssql { get; set; } = string.Empty;
}
프로젝트 루트에 'SettingInfo_gitignore.json'파일을 생성하고 아래와 같이 넣어줍니다.
//깃에는 올리면 안되는 정보
{
"ConnectionString_Mssql": "Server=[주소];DataBase=[데이터 베이스];UId=[아이디];pwd=[비밀번호]",
}
'SQLite'는 로컬 정보라 그냥 두고, 'MSSQL'의 정보는 자신의 서버에 맞게 수정합니다.
생성한 'SettingInfo_gitignore.json'파일의 '출력 디렉터리로 복사' 옵션을 '새 버전이면 복사'로 바꿔줍니다.
'SQLite'의 'IDesignTimeDbContextFactory'는 수정할 필요가 없으므로
'MSSQL'의 'DbContext_MssqlFactory'의 'CreateDbContext'를 아래와 같이 수정합니다.
public DbModel_MssqlContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<DbModelContext> optionsBuilder
= new DbContextOptionsBuilder<DbModelContext>();
//설정 파일 읽기
string sJson = File.ReadAllText("SettingInfo_gitignore.json");
SettingInfoModel? loadSetting = JsonConvert.DeserializeObject<SettingInfoModel>(sJson);
//Add-Migration InitialCreate -Context ModelsDB.DbModel_MssqlContext -OutputDir Migrations/Mssql
//Update-Database -Context ModelsDB.DbModel_MssqlContext -Migration 0
//"Server=[주소];DataBase=[데이터 베이스];UId=[아이디];pwd=[비밀번호]"
ModelDllGlobal.DbType = TargetDbType.Mssql;
ModelDllGlobal.DbConnectString = loadSetting!.ConnectionString_Mssql;
optionsBuilder.UseSqlServer(ModelDllGlobal.DbConnectString);
return new DbModel_MssqlContext(optionsBuilder.Options);
}
이제 다른 프로젝트가 없어도 'EfMultiMigrations'에서 마이그레이션이 가능합니다.
'SettingInfo_gitignore.json'파일 제외
'SettingInfo_gitignore.json'이 파일은 깃에 올리면 안 되니 샘플에도 제외되어 있습니다.
샘플 프로젝트를 사용할 때 직접 생성하여 사용해야 합니다.
//깃에는 올리면 안되는 정보
{
"ConnectionString_Mssql": "Server=[주소];DataBase=[DB이름];UId=[계정];pwd=[비밀번호];Encrypt=True;TrustServerCertificate=true;"
}
6. 테스트 프로그램
github - dang-gun/EntityFrameworkSample/MultiMigrationsSample
사용할 DB를 변경해 가며 사용하는 샘플 프로그램입니다.
마무리
샘플 및 테스트 프로젝트 :
github - dang-gun/EntityFrameworkSample/MultiMigrationsSample
케이스 바이 케이스로 만든 샘플이 아니라서 복잡합니다.
그래서 샘플 프로젝트가 의미 있을지 모르겠지만 일단 올려두겠습니다. ㅎㅎㅎ