[.Net Core] Entity Framework Core : Migration & DbContext

Yijun Jeon·2022년 10월 19일

Entity Framework Core

목록 보기
4/4
post-thumbnail

Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part8: Entity Framework Core'를 스터디하며 정리한 글입니다.

Migration

DB의 버전 관리는 어떻게 해야 할까? - 매우 중요!!

ex)
v1.1(라이브) - SQL
v2.2(현재) - 테이블에 새로운 칼럼을 추가

일단 EF Core DbContext <-> DB 상태에 대해 동의가 있어야 함

  • 닭이 먼저냐 달걀이 먼저냐와 같은 문제
  • 어떤 쪽이 원본인가
  • 무엇을 기준으로 할 것인가?
  1. Code-First : 가장 많이 활용
  2. Database-First
  3. SQL-First

Code-First

  • 원본이 Entity Class & DbContext 기준
    • 지금까지 우리가 사용하던 방식

항상 최신 상태로 DB를 업데이트 하고 싶다는 말은 아님

Migration 생성

솔루션에 Microsoft.EntityFrameworkCore.Tools 설치

도구 - NuGet 패키지 관리자 - 패키지 관리자 콘솔

Add-Migraion [Name]

Add-Migraion 단계

  1. DbContext를 찾아서 분석 -> DB 모델링 (최신)

  2. ~ModelSnapshot.cs를 이용해서 가장 마지막 Migration 상태의 DB 모델링 (가장 마지막 상태)

  3. 1-2 비교 결과 도출
    -> ModelSnapshot : 최신 DB 모델링 - 하나로만 관리됨
    -> Migrate.Designer.csMigrate.cs : Migration 관련된 세부 정보

Migrate.cs

수동으로 Up/Down을 입력할 수도 있음 - But 비추천

Migration 적용

SQL change script : 가장 많이 사용

  • Script-Migration [From] [To] [Options]


-> 만들어진 script를 DB에 적용하기만 하면 됨

DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Item]') AND [c].[name] = N'ItemGrade');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Item] DROP CONSTRAINT [' + @var0 + '];');
ALTER TABLE [Item] DROP COLUMN [ItemGrade];

GO

DELETE FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20221018143317_ItemGrade';

GO

Database.Migrate 호출

  • 코드 상에 직접 입력하는 방식이라서 위험할 수 있음 -> 비추천

DbCommands.InitializeDB()

db.DataBase.Migrate()

Command Line 방식

  • 패키지 관리자 콘솔 이용

중간에 Migration을 사용할 경우 DB를 한 번 날려주고 다시 reset해서 사용해주는 것이 좋음

Update-Database [option] - 옵션 없을 시 최신 상태로 적용


Update-Database [Name] - 특정 Migration으로 Sync


Remove-Migration - 마지막 Migration 삭제

Database-First

  • 원본이 Database 기준
    • DB에 맞춰서 AppDbContext를 작성해야 함

DB만 있는 상태에서 자동으로 AppDbContext 만들어주는 방식

EF Core Power Tools 설치
https://marketplace.visualstudio.com/items?itemName=ErikEJ.EFCorePowerTools

프로젝트 - EF Core Power Tools - Reverse Engineer(역공학)



SQL-First

DbContext, DB 둘 다 없는 상황에서 SQL을 직접 작성하여 기반으로 하는 방식
C++의 경우 꽤 많이 사용됨

1) 손수 수작업
2) Script-Migration [From] [To] [Options]
3) DB끼리의 비교를 이용해서 SQL 추출


Entity State & Relationship

DbContext 심화 - 최적화 관련

1) ChangeTracker

  • Tracking State 관련

2) Database

  • Transaction
  • DB Creation / Migration
  • Raw SQL

3) Model

  • DB 모델링

State 복습

Detached

No Tracking : 추적되지 않는 상태

SaveChanges()를 해도 존재도 모름

Unchanged

DB에는 있고, 수정사항이 없었음

메모리상의 데이터와 DB상의 데이터 동일

SaveChanges()를 해도 갱신 X

Deleted

DB에는 아직 존재하지만 삭제되어야 하는 상태

SaveChanges()로 DB 데이터 삭제

Modified

DB에는 존재하지만 클라이언트에서 수정된 상태

SaveChanges()로 DB 데이터 갱신

Added

DB에는 아직 존재하지 않음

클라이언트에서 데이터가 추가됨

SaveChanges()로 DB 데이터 추가

State 체크

메소드
Entry().State
Entry().Property().IsModified
Entry().Navigation().IsModified

Stage가 대부분 직관적이지만 RelationShip이 개입하면 살짝 더 복잡함

Add / AddRange

상태 변화

  1. NotTracking -> Added
  2. Tracking -> FK 설정이 필요한지에 따라 Modified or 기존 상태 유지
// Added
Console.WriteLine("1번)" + db.Entry(yijun).State);

db.SaveChanges();
// Add Test
{
    Item item = new Item()
    {
        TemplateId = 500,
        Owner = yijun,
    };
    db.Items.Add(item);
    // 아이템 추가 -> 간접적으로 Player로 영향
    // Player는 Tracking 상태이고, FK 설정은 필요 없음 - 아이템 쪽에 있기 때문

    // Unchanged
    // FK가 Player 쪽에 있었다면, State는 Modified
    Console.WriteLine("2번)" + db.Entry(yijun).State);
}

Remove / RemoveRange

상태 변화

  1. (DB에 의해 생성된 Key) && (C#의 기본값 아님) -> 필요에 따라 Unchanged or Modified or Deleted
  2. (DB에 의해 생성된 Key 없음) || (C#의 기본값) -> Added
// Delete Test
{
    Player p = db.Players.First();

    // DB는 이 새로운 길드의 존재도 모름 (DB 키 없음)
    p.Guild = new Guild() { GuildName = "곧 삭제될 길드" };

    // 위에서 아이템이 이미 DB에 들어간 상태 (DB가 발급했던 키 있음)
    p.OwnedItem = items[0];

    db.Players.Remove(p);

    Console.WriteLine("3번)" + db.Entry(p).State); // Deleted
    Console.WriteLine("4번)" + db.Entry(p.Guild).State); // Added
    Console.WriteLine("5번)" + db.Entry(p.OwnedItem).State); // Deleted <- 1:1
}

곧 삭제하는데 왜 굳이 Added일까? -> 동작의 일관성 때문
DB에서도 일단 존재는 알아야함 - Cascade Delete 처리 등을 위해


Update / UpdateRange

EF에서 Entity를 Update하는 기본적인 방법은 Update가 아님

  • Tracked Entity 얻어오고 -> property 수정 -> SaveChanges

Update는 Untracked Entity를 통으로 업데이트 할 때 (Disconnected State)

EF Core Update Step
1) Update 호출
2) Entity State -> Modified로 변경
3) 모든 Non-Relational Property의 IsModified = true 로 변경

Relationship 상태 변화

  1. (DB에 의해 생성된 Key) && (C#의 기본값 아님) -> 필요에 따라 Unchanged or Modified or Deleted
  2. (DB에 의해 생성된 Key 없음) || (C#의 기본값) -> Added
// Update Test
{
    // Disconnected
    Player p = new Player();

    p.PlayerId = 2; // PK
    p.Name = "FakerSenpai";
    // 아직 DB는 이 새로운 길드의 존재도 모름 -> DB 키 없음 -> 0
    p.Guild = new Guild() { GuildName = "Update Guild" };

    Console.WriteLine("6번)" + db.Entry(p.Guild).State); // Detached
    db.Players.Update(p);
    Console.WriteLine("7번)" + db.Entry(p.Guild).State); // Added
}


Attach

Untracked Entity를 Tracked Entity로 변경

DB 접근 전에 강제로 세팅해주는 느낌

Relationship 상태 변화

  1. (DB에 의해 생성된 Key) && (C#의 기본값 아님) -> 필요에 따라 Unchanged
  2. (DB에 의해 생성된 Key 없음) || (C#의 기본값) -> Added
// Attach Test
{
    Player p = new Player();

    // TEMP
    p.PlayerId = 3;
    p.Name = "Deft-_-"; // DB에 반영 X

    p.Guild = new Guild() { GuildName = "Attach Guild" };

    Console.WriteLine("8번)" + db.Entry(p.Guild).State); // Detached
    db.Players.Attach(p);

    // p.Name = "Deft-_-"; // DB에 반영 O
    Console.WriteLine("9번)" + db.Entry(p.Guild).State); // Added
}




State 조작

직접 State를 조작할 수 있다 - 최적화 등의 이유

ex)
Entry().State = EntityState.Added
Entry().Property("").IsModified = true

// State 조작
{
    Player p = new Player() { Name = "StateTest" };
    db.Entry(p).State = EntityState.Added; // Tracked로 변환
    //db.Players.Add(p) // 동일한 효과

    db.SaveChanges();
}

TrackGraph

Relationship이 있는 Untracked Enitity의 State 조작

  • 전체 데이터 중에서 일부만 갱신 가능

Graph 의 DFS처럼 하나하나 검사하면서 적용

// TrackGraph
{
    // Disconnect 상태에서,
    // 모두 갱신하는게 아니라 플레이어 이름'만' 갱신
    Player p = new Player()
    {
        PlayerId = 2,
        Name = "Faker_New"
    };

    p.OwnedItem = new Item() { TemplateId = 777 }; // 무시될 아이템 정보 가정
    p.Guild = new Guild() { GuildName = "TrackGraphGuild" }; // 무시될 길드 정보 가정

    // Graph 의 DFS처럼 하나하나 검사하면서 적용
    db.ChangeTracker.TrackGraph(p, e =>
    {
        if(e.Entry.Entity is Player)
        {
            e.Entry.State = EntityState.Unchanged;
            e.Entry.Property("Name").IsModified = true;
        }
        else if(e.Entry.Entity is Guild)
        {
            e.Entry.State = EntityState.Unchanged;
        }
        else if(e.Entry.Entity is Item)
        {
            e.Entry.State = EntityState.Unchanged;
        }
    });

    db.SaveChanges();
}


ChangeTracker

상태 정보의 변화를 감지하고 싶을 때 유용
ex)

  • Player의 Name이 바뀔 때 로그를 찍고 싶은 경우
  • Validation 코드를 넣고 싶은 경우
  • Player가 생성된 시점을 CreateTime으로 정보를 추가하고 싶은 경우

Step
1. SaveChanges를 override
2.ChangeTracker.Entries를 이용해서 바뀐 정보 추출 / 사용

DataModel.cs

// 만들어진 시간 추적 인터페이스
public interface ILogEntity
{ 
    DateTime CreateTime { get; }
    void SetCreateTime();
}

[Table("Player")]
public class Player : ILogEntity
{
    public DateTime CreateTime { get; private set; }

    public void SetCreateTime()
    {
        CreateTime = DateTime.Now;
    }
}

AppDbContext.cs

// SaveChanges override
public override int SaveChanges()
{
    var entities = ChangeTracker.Entries().Where(e => e.State == EntityState.Added);

    foreach(var entity in entities)
    {
        // Player만 추출됨
        ILogEntity tracked =  entity.Entity as ILogEntity;
        if (tracked != null)
            tracked.SetCreateTime();
    }

    return base.SaveChanges();
}


SQL 호출

경우에 따라 직접 만든 SQL을 호출할 수 있음

ex)

  1. LINQ로 처리할 수 없는 것 -> Stored Procedure 호출 등
    • SQL문을 하나의 함수 호출로 DB 에처리하는 방식
  2. 성능 최적화 등

FromSql

FromSqlRaw / FromSqlInterpolated 두 버전으로 나뉘어짐

EF Core 쿼리에 Raw SQL 추가 - SELECT ~~ 로 추출만 가능

// FromSql
{
    string name = "Yijun";
    // SQL Injection ( Web Hacking)
    // string name2 = "'Anything' OR 1=1";

    var list = db.Players
        .FromSqlRaw("SELECT * FROM dbo.Player Where Name = {0}",name)
        .Include(p => p.OwnedItem) // Include 로딩 필요
        .ToList();

    foreach(var p in list)
    {
        Console.WriteLine($"{ p.Name} {p.PlayerId}"); // Yijun 1
    }

    // String Interpolation C# 6.0
    var list2 = db.Players
        .FromSqlInterpolated($"SELECT * FROM dbo.Player Where Name = {name}")
        .ToList();

    foreach (var p in list)
    {
        Console.WriteLine($"{p.Name} {p.PlayerId}"); // Yijun 1
    }
}

ExecuteSqlCommand

ExecuteSqlRaw / ExecuteSqlInterpolated 두 버전으로 나뉘어짐

Non-Query (SELECT가 아닌) SQL - UPDATE도 가능

Reload

Tracked Entity가 있을 때 ExecuteSqlCommand로 DB 정보가 변경 되었다면 자동 갱신 X
-> 다시 한 번 DB와 Entity 사이의 정보를 최신 상태로 맞춰줘야 함

// ExecuteSqlCommand (Non-Query SQL) + Reload
{
    Player p = db.Players.Single(p => p.Name == "Faker");
	// p.Name == "Faker"
    string prevName = "Faker";
    string afterName = "Faker_New";
    db.Database.ExecuteSqlInterpolated($"UPDATE dbo.Player SET Name={afterName} Where Name={prevName}");

    db.Entry(p).Reload();
    // p.Name == "Faker_New"
}


Logging

참고 사이트 : https://learn.microsoft.com/ko-kr/ef/core/

NuGet 패키지 관리 -> Microsoft.Extensions.Logging.Console 설치

// ExecuteSqlCommand (Non-Query SQL) + Reload
{
    Player p = db.Players.Single(p => p.Name == "Faker");
	// p.Name == "Faker"
    string prevName = "Faker";
    string afterName = "Faker_New";
    db.Database.ExecuteSqlInterpolated($"UPDATE dbo.Player SET Name={afterName} Where Name={prevName}");

    db.Entry(p).Reload();
    // p.Name == "Faker_New"
}

->

  • Single -> SELECT TOP(2) : 중복 or 데이터 없는지 감지하기 위해

0개의 댓글