[Entity Framework 6] 여러 종류 DB관리하기
이전 포스팅에서 여러 종류의 DB를 한 프로젝트에서 사용하기 위한 설명을 했습니다.
이 포스팅에서는 여러 DB를 관리하기 위해 제가 정리한 코드를 설명합니다.
0. 테이블로 사용할 모델
테이블로 사용할 모델을 만들어 줍니다.
이 포스팅에서는 테스트 용도로 아래 모델을 선언했습니다.
//Test1Model.cs
/// <summary>
/// 테스트용 모델
/// </summary>
public class Test1Model
{
/// <summary>
/// 고유키
/// </summary>
[Key]
public long idTest1Model { get; set; }
/// <summary>
/// 숫자형
/// </summary>
public int Int { get; set; }
/// <summary>
/// 문자형
/// </summary>
public string Str { get; set; } = string.Empty;
/// <summary>
/// 날짜형
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// 외래키에 연결된 리스트
/// </summary>
[ForeignKey("idTest1Model")]
public ICollection<Test2Model> Test2ModelList { get; set; }
= new List<Test2Model>();
}
//Test2Model.cs
/// <summary>
/// 테스트용 모델
/// </summary>
public class Test2Model
{
/// <summary>
/// 고유키
/// </summary>
[Key]
public long idTest2Model { get; set; }
/// <summary>
/// FK 부모
/// </summary>
[ForeignKey("idTest1Model")]
public long idTest1Model { get; set; }
/// <summary>
/// 연결된 외래키
/// </summary>
/// <summary>
/// 외래키에 연결된 대상
/// </summary>
public Test1Model? Test1Model { get; set; }
}
1. 사용하는 DB 타입
프로젝트에서 사용될 DB 타입을 나열합니다.
//UseDbType.cs
/// <summary>
/// 사용하는 DB 타입
/// </summary>
public enum UseDbType
{
/// <summary>
/// 없음
/// </summary>
None = 0,
/// <summary>
/// In Memory
/// </summary>
InMemory,
/// <summary>
/// SQLite
/// </summary>
SQLite,
/// <summary>
/// MS SQL
/// </summary>
MSSQL,
/// <summary>
/// Postgre SQL
/// </summary>
PostgreSQL,
/// <summary>
/// Maria DB
/// </summary>
MariaDB,
}
2. 사용할 DB정보를 지정
사용할 DB정보는 전역에서 사용될 정보이므로 프로젝트 전역 변수에 저장합니다.
//GlobalDb.cs
public static class GlobalDb
{
/// <summary>
/// DB 타입
/// </summary>
public static UseDbType DBType = UseDbType.InMemory;
/// <summary>
/// DB 컨낵션 스트링 저장
/// </summary>
public static string DBString = "";
}
마이그레이션이나 DB를 조회할 때도 이 정보를 기준으로 동작하게 됩니다.
3. 기본 DB 연결정보 설정하기
'GlobalDb.DBType'이 변경되거나 'GlobalDb.DBString'정보가 없을 때 사용될 기본 정보입니다.
아래 인터페이스를 선언하여 사용합니다.
//DbContextDefaultInfoInterface.cs
/// <summary>
/// DbContext에 입력할 기본 정보 인터페이스
/// </summary>
/// <remarks>
/// 마이그레이션에 사용될 기본DB정보를 생성하거나 전달하는 용도로 사용된다.
/// </remarks>
public interface DbContextDefaultInfoInterface
{
/// <summary>
/// DB 타입
/// </summary>
public UseDbType DBType { get; set; }
/// <summary>
/// DB 연결 문자열
/// </summary>
public string DBString { get; set; }
}
3-1. 외부 파일로 DB 연결정보 관리
이 프로젝트에서는 'SettingInfo_gitignore.json'파일을 불러와 사용하도록 구성되어 있습니다.
필요에 따라 코드를 수정하여 사용해야 합니다.
이 파일은 Git에 올려지지 않는 파일입니다.(소스파일에 들어있지 않음)
Git에 올리면 안 되는 정보를 관리하기 위해 사용하고 있습니다.
'SettingInfo_gitignore.json'파일 구조는 아래와 같습니다.
[
{
"DBTypeString": "[DB 이름]",
"DBString": "[연결 문자열]"
}
]
3-1-1. DB타입을 문자열로 관리하기 위한 모델
외부 파일로 관리할 때 DB타입이 숫자로 되어 있으면 구분하기 힘듭니다.
문자열로 DB타입을 받을 수 있도록 다음과 같은 모델을 선언해 줍니다.
이 모델을 이용하여 'SettingInfo_gitignore.json'파일을 역직열화(Deserialize) 하여 사용합니다.
//DbContextDefaultInfo_Temp.cs
/// <summary>
/// 역직열화에 사용할 모델
/// </summary>
/// <remarks>
/// 인터페이스를 그대로 역직열화 하면 오류가 발생하므로 모델로 사용하기위한 개체이다.
/// </remarks>
public class DbContextDefaultInfo_Temp : DbContextDefaultInfoInterface
{
/// <inheritdoc />
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public UseDbType DBType { get; set; } = UseDbType.None;
/// <inheritdoc />
public string DBString { get; set; } = string.Empty;
/// <summary>
/// 문자열로 DBType을 입력하는 경우
/// </summary>
public string DBTypeString
{
get
{
return this.DBType.ToString();
}
set
{
int nType = 0;
if (true == int.TryParse(value, out nType))
{//숫자형으로 들어왔다.
this.DBType = (UseDbType)nType;
}
else
{
switch (value.ToLower())
{
case "inmemory":
this.DBType = UseDbType.InMemory;
break;
case "sqlite":
this.DBType = UseDbType.SQLite;
break;
case "mssql":
this.DBType = UseDbType.MSSQL;
break;
case "postgresql":
this.DBType = UseDbType.PostgreSQL;
break;
case "mariadb":
this.DBType = UseDbType.MariaDB;
break;
case "none":
default:
this.DBType = UseDbType.None;
break;
}//end switch
}
}
}
}
26번 줄 : 'DBTypeString'에 데이터를 넣으면 자동으로 'UseDbType'으로 변환을 시도하는 코드입니다.
36번 줄 : 대소문자에 의한 오류를 막기 위해 소문자로 변환하여 비교합니다.
3-1-2. 파일 읽기 공통화
'DbContextDefaultInfo_Temp'리스트를 불러올 공통 코드를 'GlobalDb'에 넣습니다.
/// <summary>
/// 지정된 파일(json)에서 지정된 이름의 DbString을 리턴한다.
/// </summary>
/// <param name="sPath"></param>
/// <param name="typeUseDb"></param>
/// <returns></returns>
public static string DbStringLoad(
string sPath
, UseDbType typeUseDb)
{
string sReturn = string.Empty;
//Console.WriteLine($"DbStringLoad : {sPath}");
if (true == File.Exists(sPath))
{
//파일에서 찾을 내용 넣기
string sJson = File.ReadAllText(sPath);
//주석 제거
string jsonWithoutComments = Regex.Replace(sJson, @"(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)", "");
List<DbContextDefaultInfo_Temp>? listDbInfo
= JsonSerializer.Deserialize<List<DbContextDefaultInfo_Temp>>(jsonWithoutComments);
if (null != listDbInfo)
{//지정된 파일을 재대로 읽음
DbContextDefaultInfoInterface? findItem
= listDbInfo
.Where(w => w.DBType == typeUseDb)
.FirstOrDefault();
if (null != findItem)
{//지정된 DB 타입을 찾음
sReturn = findItem.DBString;
}
}
}
return sReturn;
}
23번 줄 : 파일을 읽어 'DbContextDefaultInfo_Temp'리스트를 만듭니다.
28번 줄 : 전달받은 DB타입과 일치하는 타입을 찾습니다.
36번 줄 : 전달받은 DB타입과 일치하는 타입의 문자열을 리턴합니다.
3-1-3. 연결 기본 정보 읽기 공통화
연결 기본 정보 인터페이스(DbContextDefaultInfoInterface)를 선택하여 DB정보를 초기화 하기위한 공통 함수를 만들어 사용합니다.
이 함수를 통해 언제든지 'DB 연결 문자열'을 교체할 수 있습니다.
이 함수는 편하게 연결 정보를 교체하기 위한 용도만 있으므로 다른 프로젝트에 복사할 때 없어도 크게 지장이 없습니다.
/// <summary>
/// 지정된 타입으로 DB GlobalDb.DBString정보를 불러온다.
/// </summary>
/// <param name="typeDb"></param>
/// <param name="bDbStringEmpty">GlobalDb.DBString값을 강제로 비울지 여부</param>
public static void DbStringLoad(
UseDbType typeDb
, bool bDbStringEmpty = false)
{
if(true == bDbStringEmpty)
{
GlobalDb.DBString = string.Empty;
}
switch(typeDb)
{
case UseDbType.InMemory:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_InMemory());
break;
case UseDbType.SQLite:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_Sqlite());
break;
case UseDbType.MSSQL:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_Mssql());
break;
case UseDbType.PostgreSQL:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_Postgresql());
break;
case UseDbType.MariaDB:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_Mariadb());
break;
case UseDbType.None:
GlobalDb.DbStringLoad(new DbContextDefaultInfo_InMemory());
break;
}
}
/// <summary>
/// DbContextDefaultInfoInterface를 전달받아 DB정보를 갱신한다.
/// </summary>
/// <param name="dbContextDefaultInfo"></param>
public static void DbStringLoad(DbContextDefaultInfoInterface dbContextDefaultInfo)
{
GlobalDb.DBType = dbContextDefaultInfo.DBType;
if (string.Empty == GlobalDb.DBString)
{//DB 연결 문자열 정보가 없거나
//DB 연결정보를 다시 불러온다.
GlobalDb.DBString = dbContextDefaultInfo.DBString;
}
}
3-2. 연결 정보 만들기
DB 별로 기본값으로 사용될 정보를 입력합니다.
3-2-1. 'InMemory' 정보 만들기
'InMemory'는 외부에서 접근할 수 없기 때문에 별도의 파일로 관리할 필요가 없습니다.
파일명 : DbContextDefaultInfo_InMemory.cs
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/DbContextInfo/DbContextDefaultInfo_InMemory.cs
3-2-2. 'SQLite' 정보 만들기
'SQLite'는 일반적으로 로컬에서 사용되는 DB입니다.
테스트 프로젝트에서도 많이 사용되므로 별도의 파일로 관리할 필요가 없습니다.
파일명 : DbContextDefaultInfo_Sqlite.cs
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/DbContextInfo/DbContextDefaultInfo_Sqlite.cs
3-2-3. 'MSSQL' 정보 만들기
파일명 : DbContextDefaultInfo_Mssql.cs
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/DbContextInfo/DbContextDefaultInfo_Mssql.cs
3-2-4 . 'PostgreSQL' 정보 만들기
파일명 : DbContextDefaultInfo_Postgresql.cs
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/DbContextInfo/DbContextDefaultInfo_Postgresql.cs
3-2-5. 'MariaDB' 정보 만들기
파일명 : DbContextDefaultInfo_Mariadb.cs
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/DbContextInfo/DbContextDefaultInfo_Mariadb.cs
4. 'DbContext' 만들기
어떤 DB를 사용하더라도 같이 사용할 공통 컨텍스트(Context)를 만듭니다.
테이블 정보도 여기에 선언합니다.
공통 컨텍스트로는 마이그레이션 관리를 할 수 없습니다.
반듯이 DB전용 컨텍스트도 만들어서 관리해야 합니다.
컨텍스트를 사용하기 위해선 누겟에서 참조를 추가해 줍니다.
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Tools
'ModelsDbContext'개체를 생성하면 'OnConfiguring'가 호출되고 'GlobalDb.DBType'과 'GlobalDb.DBString'의 정보를 이용해 사용할 프로 바인더 개체를 지정합니다.
//ModelsDbContext.cs
/// <summary>
///
/// </summary>
public class ModelsDbContext : DbContext
{
#pragma warning disable CS8618 // 생성자를 종료할 때 null을 허용하지 않는 필드에 null이 아닌 값을 포함해야 합니다. null 허용으로 선언해 보세요.
/// <summary>
///
/// </summary>
public ModelsDbContext()
{
}
/// <summary>
///
/// </summary>
/// <param name="options"></param>
public ModelsDbContext(DbContextOptions<ModelsDbContext> options)
: base(options)
{
//Console.WriteLine($"ModelsDbContext : {GlobalDb.DBString}");
}
#pragma warning restore CS8618 // 생성자를 종료할 때 null을 허용하지 않는 필드에 null이 아닌 값을 포함해야 합니다. null 허용으로 선언해 보세요.
/// <summary>
///
/// </summary>
/// <param name="options"></param>
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
//Console.WriteLine($"OnConfiguring DbType : {GlobalDb.DBType}");
//연결 문자열이 없어도 마이그레이션 생성은 가능하다.
//하지만 몇몇 동작이 재대로 되지 않는다.(예> Remove-Migration)
switch (GlobalDb.DBType)
{
case UseDbType.SQLite:
options.UseSqlite(GlobalDb.DBString);
break;
case UseDbType.MSSQL:
options.UseSqlServer(GlobalDb.DBString);
break;
case UseDbType.PostgreSQL:
options.UseNpgsql(GlobalDb.DBString);
break;
case UseDbType.MariaDB:
options.UseMySql(
GlobalDb.DBString
, new MySqlServerVersion(new Version(11, 1, 2)));
break;
case UseDbType.InMemory:
options.UseInMemoryDatabase(GlobalDb.DBString);
break;
default:
break;
}
}
/// <summary>
/// 테스트1 데이터
/// </summary>
public DbSet<Test1Model> Test1Model { get; set; }
/// <summary>
/// 테스트2 데이터
/// </summary>
public DbSet<Test2Model> Test2Model { get; set; }
/// <summary>
/// 데이터 넣기 동작
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//테스트1 데이터 한개 삽입
modelBuilder.Entity<Test1Model>().HasData(
new Test1Model
{
idTest1Model = 1,
Int = 1,
Str = "Test",
Date = DateTime.MinValue,
});
//테스트2 데이터 한개 삽입
modelBuilder.Entity<Test2Model>().HasData(
new Test2Model
{
idTest2Model = 1,
idTest1Model = 1,
});
}
}
39번 줄 : 선택된 DB에 따라 사용할 프로 바인더를 설정합니다.
4-1. 'InMemory' 컨텍스트
'InMemory'는 별도의 파일 없이 메모리에서 동작하는 DB입니다.
메모리에서 동작하므로 서버가 종료되면 데이터도 같이 날아가지만 빠른 속도를 자랑합니다.
(참고 : MS Learn - EF 코어 In-Memory 데이터베이스 공급자)
파일명 : ModelsDbContext_InMemory.cs
참조 : nuget - Microsoft.EntityFrameworkCore.InMemory
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/ModelsDbContext_InMemory.cs
4-2. 'SQLite' 컨텍스트
파일명 : ModelsDbContext_Sqlite.cs
참조 : nuget - Microsoft.EntityFrameworkCore.Sqlite
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/ModelsDbContext_Sqlite.cs
4-3. 'MSSQL' 컨텍스트
파일명 : ModelsDbContext_Mssql.cs
참조 : nuget - Microsoft.EntityFrameworkCore.SqlServer
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/ModelsDbContext_Mssql.cs
4-4. 'PostgreSQL' 컨텍스트
'timestamp with time zone' 에러를 막기 위해 생성자에서 아래 코드를 추가해야 합니다.
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
파일명 : ModelsDbContext_Postgresql.cs
참조 : nuget - Npgsql.EntityFrameworkCore.PostgreSQL, Npgsql.EntityFrameworkCore.PostgreSQL.Design
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/ModelsDbContext_Postgresql.cs
4-5. 'MariaDB' 컨텍스트
파일명 : ModelsDbContext_Mariadb.cs
참조 : nuget - Pomelo.EntityFrameworkCore.MySql
참고 : github - dang-gun/EntityFrameworkSample/MultiMigrations/ModelsDB/DbContexts/ModelsDbContext_Mariadb.cs
5. 마이그래이션 작성
마이그래이션을 할 때 사용할 컨텍스트를 '-Context'을 통해 지정할 수 있습니다.
(참고 : [Entity Framework 6] 여러 종류 DB대응하기 )
'기본 프로젝트'는 위에서 만든 프로젝트를 지정해 줍니다.
'솔루션 탐색기'에서 사용할 시작 프로젝트는 'SettingInfo_gitignore.json'를 가지고 있는 프로젝트로 선택해 줍니다.
(위에서 만든 프로젝트도 가능)
DB별로 'DbContext'파일의 주석을 참고하여 마이그레이션 해줍니다.
6. 테스트하기
테스트 프로젝트 :
WinForm - dang-gun/EntityFrameworkSample/MultiMigrations_Test
ASP.NET Core - dang-gun/EntityFrameworkSample/MultiMigrations_Test_Aspnet
별도의 프로젝트를 생성하고 위에서 만든 프로젝트를 참조해 줍니다.
누겟에서 아래 항목을 찾아 추가해 줍니다.
Microsoft.EntityFrameworkCore.Tools
이제 평상시와 같이 'ModelsDbContext'개체를 만들어서 EF를 활용하면 됩니다.
실시간으로 사용할 DB를 변경해도 잘 동작합니다.
마무리
완성된 프로젝트 : github - dang-gun/EntityFrameworkSample/MultiMigrations
이대로 그냥 쓰면 매번 설정할 필요 없이 어느 DB나 연결해서 사용할 수 있다는 장점이 있긴 하지만.......
프로 바인더의 덩치가 커서 모두 다 참조하면 마이그레이션 프로젝트만 11메가가 넘어갑니다 ㅋㅋㅋㅋㅋ
일반적인 프로젝트는 목표로 하는 DB가 한두 개뿐이 안되므로
이 프로젝트를 참고하여 해당 DB에 대한 참조와 코드만 남기고 정리해서 사용하는 것이 좋습니다.
테이블에 해당하는 'ModelsDbContext'를 제외하면 모든 코드가 항상 동일하므로
'ModelsDbContext'만 교체하는 라이브러리를 만들려고 여러번 시도 했으나 실패하였습니다.
(참고 : stackoverflow - Are there ways to achieve a similar effect to replacing a parent class? )