[Entity Framework] SQLite에서 EFCore.BulkExtensions 사용하기

김남규·2022년 1월 7일
1

Entity Framework는 데이터베이스를 간편하게 사용할 수 있도록 해주는 유용한 도구이지만, 많은 데이터를 한 번에 입력하거나 삭제하는 기능이 제공되지 않는다. Entity Framework의 기능을 이용해 대량의 데이터를 입력하거나 삭제하려면 많은 시간이 소요된다.

Nuget에 공개된 EFCore.BulkExtensions라는 라이브러리를 사용하면 이런 문제를 해결할 수 있다. 하지만 Entity Framework를 통해 대량의 데이터를 처리하는 것보다는 SqlBulkCopy 등을 이용한 SQL문을 작성하여 처리하는 것이 처리 속도가 더 빠르다. EFCore.BulkExtensions는 빠른 반응 속도를 요하지 않을 때 편리하게 사용할 수 있다.

EFCore.BulkExtensions의 GitHub 주소는 다음과 같다. EFCore.BulkExtensions에서 제공되는 기능의 목록과 사용법에 관한 내용이 있다.
https://github.com/borisdj/EFCore.BulkExtensions

EFCore.BulkExtensions 사용하기

  1. .net core 프로젝트를 생성한다.
    참고로 EFCore.BulkExtensions는 .net core 용이다. .net framework 프로젝트에서는 사용할 수 없다.
  • .net core Windows Forms App 프로젝트 타입을 선택한다.
  • 프로젝트명은 아무거나 주어도 되지만, 여기서는 Sqlite_BulkExtensions_Test라는 이름을 사용한다.
  • .NET 6.0 프레임워크를 사용한다.
  1. 솔루션 탐색기의 Dependencies를 선택한 후, “Manage Nuget Packages ...” 메뉴를 열어서 다음의 패키지들을 설치한다.
    Entity Framework과 EFCore.BulkExtensions을 사용하기 위한 패키지들이다. SQLitePCLRaw.bundle_e_sqlite3는 Sql Server용을 작성할 때는 필요없는 패키지이다. EFCore.BulkExtensions를 SQLite에서 사용하기 위해서는 SQLitePCL.Batteries.Init()를 한 번 호출해 주기 위해서 필요하다.
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Sqlite
  • SQLitePCLRaw.bundle_e_sqlite3
  • EFCore.BulkExtensions

  1. Table 클래스를 생성한다. Table_Test 클래스를 생성하고, 아래와 같이 작성한다.
  • Table 명 : Test
  • Column
    ID : NVARCHAR(36)
    Name : NVACHAR(20)
using System.ComponentModel.DataAnnotations.Schema;

namespace Sqlite_BulkExtensions_Test {
    [Table("Test")]
    public class Table_Test {
        [Column("ID", TypeName = "NVARCHAR(36)")]
        public string Id { get; set; }
        [Column("Name", TypeName = "NVARCHAR(20)")]
        public string Name { get; set; }
    }
}
  1. DbContext 클래스를 생성한다. DbContext_Test.cs 클래스를 생성하고, 아래와 같이 작성한다.
using Microsoft.EntityFrameworkCore;

namespace Sqlite_BulkExtensions_Test {
    public class DbContext_Test : DbContext {
				public DbSet<Table_Test> Test { get; set; }

        private string _dbPath;

        public DbContext_Test(string dbPath) {
            SQLitePCL.Batteries.Init();

            this._dbPath = dbPath;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite($"Data Source={this._dbPath}");    }
}
  1. Form1에 다음과 같이 UI를 추가한다.
  • 입력한 데이터의 크기를 입력받을 TextBox를 추가하고 이름을 _tbSize라고 입력한다.
  • Bulk Insert를 실행할 Button을 추가하고 이름을 _btBulkInsert라고 입력한다.
  • Batch Delete를 실행할 Button을 추가하고 이름을 _btBatchDelete라고 입력한다.
  • 로그를 표시하기 위해 TextBox를 추가하고 이름을 _tbLog라고 입력한다.

  1. Form1.cs 소스에 다음의 using문을 추가한다.
using EFCore.BulkExtensions;
  1. Form1.cs 소스에 변수를 추가한다.
// SQLite DB 파일의 경로
private string DbPath = "Test.db";

// DbContext
private DbContext_Test _db;
  1. Form1에 Load Event를 연결한 후, 다음처럼 코드를 작성한다.
private void Form1_Load(object sender, EventArgs e) {
    Init();
}

private void Init() {
    // DbContext Object 생성
    this._db = new DbContext_Test(DbPath);

    // 테이블 스키마가 생성되었는 지 확인
    this._db.Database.EnsureDeleted();
    this._db.Database.EnsureCreated();
}
  1. BulkInsert 버튼에 Click Event를 연결한 후, 다음처럼 코드를 작성한다.
    EFCore.BulkExtensions에서 제공하는 메서드들은 sync와 async 방식이 있다. 둘 중 하나를 선택해서 사용한다.
private void _btBulkInsert_Click(object sender, EventArgs e) {
    BulkInsert();
}

private async void BulkInsert() {
    Stopwatch sw = new Stopwatch();

	// 입력 테스트를 위한 샘플 데이터의 크기 설정
    int size = Convert.ToInt32(this._tbSize.Text);
		
	// 입력 테스트를 위한 샘플 데이터 생성
    List<Table_Test> list = GenerateSampleData(size);

    sw.Start();

    // BulkInsert는 sync 방식과 async 방식이 있다. 둘 중 하나를 선택해서 사용한다.
    #region ===== sync =====
    this._db.BulkInsert(list);
    #endregion ===== sync =====

    #region ===== async =====
    //using (var trans = await this._db.Database.BeginTransactionAsync()) {

    //    await this._db.BulkInsertAsync(list);
    //    await trans.CommitAsync();
    //}
    #endregion ===== async =====

    sw.Stop();

    this._tbLog.AppendText(String.Format("BulkInsert({0}) : {1}\r\n",
		    size, sw.ElapsedMilliseconds / 1000));
}

// 주어진 크기만큼 샘플 데이터를 생성한다.
private List<Table_Test> GenerateSampleData(int size) {
    List<Table_Test> list = new List<Table_Test>();

    for (int i = 0; i < size; i++) {
        Table_Test tb = new Table_Test();
        tb.Id = Guid.NewGuid().ToString();
        tb.Name = i.ToString();

        list.Add(tb);
    }

    return list;
}
  1. BatchDelete 버튼에 Click Event를 연결한 후, 다음과 같이 작성한다.
private async void _btBatchDelete_Click(object sender, EventArgs e) {
    BatchDelete();
}

private void BatchDelete() {
    Stopwatch sw = new Stopwatch();

    sw.Start();

    #region ===== sync =====
    this._db.Test.BatchDelete();
    #endregion ===== sync =====

    #region ===== async =====
    //this._db.Test.BatchDeleteAsync();
    #endregion ===== async =====

    sw.Stop();

    this._tbLog.AppendText(String.Format("BatchDelete : {0}\r\n",
	      sw.ElapsedMilliseconds / 1000));
}
  1. 프로젝트에 Text 파일을 하나 추가하고, 이름을 Test.db로 준다.
    파일을 생성할 때, 꼭 Text 파일일 필요는 없고, 아무 형태의 파일을 생성하고 파일명을 Test.db로만 주면 된다.
    그런 후에, 솔루션 탐색기에서 Test.db를 선택한 후, 속성창에서 "Copy To Output Directory" 항목에서 "Copy if newer"를 선택한다.

  1. 프로젝트를 빌드 한 후 실행한다.
    1,000,000개의 데이터를 생성한 후, BulkInsert를 하면 20초 정도가 소요된다.
    BatchDelete는 0초 정도 소요된다.

  1. 실행할 때, DB 관련 오류가 난다면 Output 폴더에 Test.db 파일이 있는지 확인한다.

  2. Form1.cs 전체 코드

using System.Diagnostics;

using EFCore.BulkExtensions;

namespace Sqlite_BulkExtensions_Test {
    public partial class Form1 : Form {
        // SQLite DB 파일의 경로
        private string DbPath = "Test.db";

        // DbContext
        private DbContext_Test _db;

        public Form1() {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e) {
            Init();
        }

        private void Init() {
            // DbContext Object 생성
            this._db = new DbContext_Test(DbPath);

            // 테이블 스키마가 생성되었는 지 확인
            this._db.Database.EnsureDeleted();
            this._db.Database.EnsureCreated();
        }

        private void _btBulkInsert_Click(object sender, EventArgs e) {
            BulkInsert();
        }

        private async void BulkInsert() {
            Stopwatch sw = new Stopwatch();

            // 입력 테스트를 위한 샘플 데이터의 크기 설정
            int size = Convert.ToInt32(this._tbSize.Text);

            // 입력 테스트를 위한 샘플 데이터 생성
            List<Table_Test> list = GenerateSampleData(size);

            sw.Start();

            // BulkInsert는 sync 방식과 async 방식이 있다. 둘 중 하나를 선택해서 사용한다.
            #region ===== sync =====
            this._db.BulkInsert(list);
            #endregion ===== sync =====

            #region ===== async =====
            //using (var trans = await this._db.Database.BeginTransactionAsync()) {

            //    await this._db.BulkInsertAsync(list);
            //    await trans.CommitAsync();
            //}
            #endregion ===== async =====

            sw.Stop();

            this._tbLog.AppendText(String.Format("BulkInsert({0}) : {1}\r\n", size, sw.ElapsedMilliseconds / 1000));
        }

        // 주어진 크기만큼 샘플 데이터를 생성한다.
        private List<Table_Test> GenerateSampleData(int size) {
            List<Table_Test> list = new List<Table_Test>();

            for (int i = 0; i < size; i++) {
                Table_Test tb = new Table_Test();
                tb.Id = Guid.NewGuid().ToString();
                tb.Name = i.ToString();

                list.Add(tb);
            }

            return list;
        }

        private async void _btBatchDelete_Click(object sender, EventArgs e) {
            BatchDelete();
        }

        private void BatchDelete() {
            Stopwatch sw = new Stopwatch();

            sw.Start();

            #region ===== sync =====
            this._db.Test.BatchDelete();
            #endregion ===== sync =====

            #region ===== async =====
            //this._db.Test.BatchDeleteAsync();
            #endregion ===== async =====

            sw.Stop();

            this._tbLog.AppendText(String.Format("BatchDelete : {0}\r\n", sw.ElapsedMilliseconds / 1000));
        }
    }
}

0개의 댓글