[.Net Core] Entity Framework Core : 데이터 수정

Yijun Jeon·2022년 10월 14일

Entity Framework Core

목록 보기
2/4
post-thumbnail

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

Entity 상태 관리

  • 데이터 로딩 함수들 삭제

State

Unity의 Dirty Flag와 비슷한 느낌

접근법

db.Entry(yijun).State

Detached

No Tracking : 추적되지 않는 상태

SaveChanges()를 해도 존재도 모름

Unchanged

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

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

SaveChanges()를 해도 갱신 X

Deleted

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

SaveChanges()로 DB 데이터 삭제

Modified

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

SaveChanges()로 DB 데이터 갱신

Added

DB에는 아직 존재하지 않음

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

SaveChanges()로 DB 데이터 추가

SaveChanges

SaveChanges()의 역할

  1. 추가된 객체들의 상태가 Unchanged로 바뀜

  2. SQL Identity로 PK를 관리

  3. 데이터 추가 후 ID 받아와서 객체의 ID property를 채워줌

  4. Relationship을 참고해서 FK 세팅 및 객체 참조 연결

이미 존재하는 사용자를 FK로 연동

  1. Tracked Instance (추적되고 있는 객체)를 얻어옴
  2. 데이터 연결
db.SaveChanges();
{
    var owner = db.Players.Where(p => p.Name == "yijun").First();
    Item item = new Item()
    { 
        TemplateId = 300,
        CreatedDate=DateTime.Now,
        Owner = owner
    };
    db.Items.Add(item);
    db.SaveChanges();
}


Update 기초

Update 단계

  1. Tracked Entity를 얻어옴
  2. Entity 클래스의 property 변경 (set)
  3. SaveChanges 호출
var guild = db.Guilds.Single(g => g.GuildName == "T1");

Single : 입력한 조건에 일치하는 하나의 element 반환, 조건에 부합하는 데이터가 두 개 이상이면 오류 반환

Update 내부 로직

Update를 할 때 해당 테이블에 대한 전체 데이터를 수정하는 것이 아니라,
수정사항이 있는 데이터만 골라서 수정하게 됨

  • SaveChanges() -> 내부적으로 DetectChanges 호출
  • DetectChange에서 데이터의 최초 Snapshot VS 현재 SnapShot 비교
  • 현재 SnapShot이 변경된 데이터만 Update

SQL문

public static void UpdateTest()
{
    using (AppDbContext db = new AppDbContext())
    {
        // 최초 상태
        var guild = db.Guilds.Single(g => g.GuildName == "T1");

        // 현재 상태
        guild.GuildName = "GEN";

        db.SaveChanges();
    }
}

->

SELECT TOP(2) Guildid, GuildName
FROM [Guilds]
WHERE GuildName = N'T1';

SET NOCOUNT ON; // 최적화
UPDATE [Guilds]
SET GuildName = @p0 // 파라미터
WHERE GuildId = @p1
SELECT @@ROWCOUNT; // 변경 데이터 수 반환

Disconnected Update

Connected Update :

  • Update 3단계가 한 번에 단계적으로 일어나는 경우

Disconnected Update :

  • Update 3단계가 한 번에 단계적으로 일어나지 않고 끊기는 경우 - REST API 등

  • 최초 Snapshot의 상태와 변경 Snapshot의 상태 간의 간격이 큼

  • 클라이언트에게 정보를 보내고 한참 뒤에 수정하는 느낌

처리하는 2가지 방법

  1. Reload Update 방식
  2. Full Update 방식

Reload Update

필요한 정보만 보내서, 1-2-3 Step을 다시 밟음

PK(Id)로 다시 데이터를 긁어오기 때문에 'Reload 방식'

DbCommands.cs

public static void ShowGuilds()
{
    using(AppDbContext db = new AppDbContext())
    {
        foreach(var guild in  db.Guilds.MapGuildToDto().ToList()) // DTO로 Guild 정보 추출
        {
            Console.WriteLine($"GuildId({guild.GuildId}) GuildName({guild.Name}) MemberCount({guild.MemberCount})");
        }

    }
}
public static void UpdateByReload()
{
    ShowGuilds();

    // 외부에서 수정을 원하는 데이터의 ID / 정보를 넘겨줬다고 가정
    Console.WriteLine("Input GuildId");
    Console.Write(" > ");
    int id = int.Parse(Console.ReadLine());

    Console.WriteLine("Input GuildName");
    Console.Write(" > ");
    string name = Console.ReadLine();

    // 1-2-3 Step 다시 실행
    using (AppDbContext db = new AppDbContext())
    {
        Guild guild = db.Find<Guild>(id); // PK로 가장 빠르게 찾는 방법
        
        guild.GuildName = name;
        db.SaveChanges();
    }
    Console.WriteLine("--- Update Complected ---");
    ShowGuilds();
}

장점 : 최소한의 정보로 Update 가능

단점 : DB Read를 두 번 실행 - 클라에게 전달 + 변경용 Read

Full Update

모든 정보를 다 보내고 받아서, 아예 Entity를 다시 만들고 통채로 Update

데이터를 Full로 채워서 새로 만들어서 Update하기 때문에 'Full' 방식

DbCommands.cs

public static string MakeUpdateStr()
{
    var jsonStr = "{\"GuildId\":1, \"GuildName\":\"Hello\", \"Members\":null}";
    return jsonStr;
}

public static void UpdateByFull()
{
    ShowGuilds();

    // json 사용 - 반드시 json일 필요는 없음
    string jsonStr = MakeUpdateStr();
    Guild guild = JsonConvert.DeserializeObject<Guild>(jsonStr);

    // json 미사용
	Guild guild2 = new Guild()
    { 
        GuildId = 1,
        GuildName = "TestGuild"
    };


    using (AppDbContext db = new AppDbContext())
    {
        db.Guilds.Update(guild); // PK가 겹치는 데이터를 찾아서 업데이트
        db.SaveChanges();
    }

    Console.WriteLine("--- Update Complected ---");
    ShowGuilds();
}

장점 : DB에 다시 Read할 필요 없이 바로 Update

단점 : 모든 정보가 필요, 보안 문제 -> 상대를 신용할 때 사용해야 함


Foreign Key & Nullable

  • 업데이트 관련 함수 모두 삭제

Dependent 데이터가 Principal 데이터 없이 존재할 수 있을까?

정책 상 달라질 수 있음
ex)

  1. 주인이 없는 아이템은 불가능!
  2. 주인이 없는 아이템도 가능! - (로그 차원에서)
    -> Nullable

Principal Entity

Princial Entitiy - FK가 가리키는 주요 데이터

  • Player class

Dependent Entity

Dependent Entity - 의존적인 데이터

  • FK를 포함하는 쪽
  • Item class

Nullable

[ForeignKey("OwnerId")] // 기본세팅 - Nullable
OR
public int? OwnerId { get; set; }

DbCommands.cs - Test 코드

public static void Test()
{
    ShowItems();

    Console.WriteLine("Input Delete PlayerId");
    Console.Write(" > ");
    int id = int.Parse(Console.ReadLine());

    using (AppDbContext db = new AppDbContext())
    {
        // 연관된 데이터에 대한 수정사항이 있을 경우 Include 해주는 것이 좋음
        Player player = db.Players
            .Include(p => p.Item) // Include 해주지 않으면 오류 발생 !!
            .Single(p => p.PlayerId == id);

        db.Players.Remove(player);
        db.SaveChanges();
    }

    Console.WriteLine("--- Test Complected ---");
    ShowItems();
}
  • FK가 Nullable이 아니라면 - (정책1)

Player가 지워지면 Fk로 해당 Player를 참조하는 Item도 같이 삭제됨

  • FK가 Nullable이라면 - (정책2)

Player가 지워지더라도 FK로 해당 Player를 참조하는 Item은 그대로


Relationship Update

1:1 관계

Item <-> Player

DbCommands.Update_1v1

using (AppDbContext db = new AppDbContext())
{
    // 연관된 데이터에 대한 수정사항이 있을 경우 Include 해주는 것이 좋음 
    Player player = db.Players
        .Include(p => p.Item) // Include 해주지 않으면 오류 발생 !!
        .Single(p => p.PlayerId == id);

    // 기존 Item을 수정하는 방식
    if (player.Item != null)
    {
        player.Item.TemplateId = 888;
        player.Item.CreatedDate = DateTime.Now;
    }

    // 기존 Item을 대체하는 방식
    // 기존의 Item은 자동으로 Onwer가 사라짐
    player.Item = new Item()
    {
        TemplateId = 777,
        CreatedDate = DateTime.Now
    };

    db.SaveChanges();
}
  • Item 대체

  • Item 수정

1:다 관계

Guild <-> Player

DbCommands.Update_1vM

  • Eager Loading 없이 새로 List 할당한 경우
using (AppDbContext db = new AppDbContext())
{
    Guild guild = db.Guilds
        //.Include(g => g.Members)
        .Single(g => g.GuildId == id);

    guild.Members = new List<Player>()
    {
        new Player(){Name = "Dopa"}
    };

    db.SaveChanges();
}

정상적으로 추가됨 But, 정확한 논리에 따른 결과는 아님!

DbCommands.Update_1vM

  • Eager Loading 하여 새로 List 할당한 경우
using (AppDbContext db = new AppDbContext())
{
    Guild guild = db.Guilds
        .Include(g => g.Members)
        .Single(g => g.GuildId == id);

    guild.Members = new List<Player>()
    {
        new Player(){Name = "Dopa"}
    };

    db.SaveChanges();
}

기존 데이터들은 다 비워짐

Eager Loading + 추가 로직

using (AppDbContext db = new AppDbContext())
{
    Guild guild = db.Guilds
        .Include(g => g.Members)
        .Single(g => g.GuildId == id);

    guild.Members.Add(new Player() {
        Name = "Dopa" 
    });

    db.SaveChanges();
}

Delete

Delete 예시 코드

public static void TestDelete()
{
    ShowItems();

    Console.WriteLine("Select Delete ItemId");
    Console.Write(" > ");
    int id = int.Parse(Console.ReadLine());

    using (AppDbContext db  = new AppDbContext())
    {
        Item item = db.Items.Find(id);
        db.Items.Remove(item);
        db.SaveChanges();
    }

    Console.WriteLine("--- TestDelete Completed --- ");

    ShowItems();
}

그러나, 실제 게임에서 데이터란 굉장히 중요하기 때문에 DB에서 실제로 삭제하는 경우는 드묾

SoftDeleted

SoftDeleted : 실제 DB에서 데이터를 삭제하는 방법이 아닌 삭제되었다는 사실만 기입하는 방식

using (AppDbContext db  = new AppDbContext())
{
	Item item = db.Items.Find(id);
	//db.Items.Remove(item);
    item.SoftDeleted = true;
	db.SaveChanges();
}

그럼 매번 Item에 접근할 때마다 Item.SoftDeleted를 검사해주어야 할까?

-> OnModelCreating 오버라이딩으로 해결

OnModelCreating

EF Core 작동 스텝의 6단계 : OnModelCreating 함수 호출 (추가설정 = overrride)

AppDbContext.cs

protected override void OnModelCreating(ModelBuilder builder)
{
    // 앞으로 Item Entity에 접근할 때, 항상 사용되는 모델 레벨의 필터링
    // 필터를 무시하고 싶으면 IgnoreQueryFilters 옵션 추가
    builder.Entity<Item>().HasQueryFilter(i => i.SoftDeleted == false);
}
  • ItemId 1,2 삭제 결과 :


0개의 댓글