2024. 2. 6. 15:30

이전 포스팅에서 여러 종류의 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? )