
Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part8: Entity Framework Core'를 스터디하며 정리한 글입니다.
Unity의
Dirty Flag와 비슷한 느낌
접근법
db.Entry(yijun).State
No Tracking : 추적되지 않는 상태
SaveChanges()를 해도 존재도 모름
DB에는 있고, 수정사항이 없었음
메모리상의 데이터와 DB상의 데이터 동일
SaveChanges()를 해도 갱신 X
DB에는 아직 존재하지만 삭제되어야 하는 상태
SaveChanges()로 DB 데이터 삭제
DB에는 존재하지만 클라이언트에서 수정된 상태
SaveChanges()로 DB 데이터 갱신
DB에는 아직 존재하지 않음
클라이언트에서 데이터가 추가됨
SaveChanges()로 DB 데이터 추가
SaveChanges()의 역할
추가된 객체들의 상태가 Unchanged로 바뀜
SQL Identity로 PK를 관리
데이터 추가 후 ID 받아와서 객체의 ID property를 채워줌
Relationship을 참고해서 FK 세팅 및 객체 참조 연결
이미 존재하는 사용자를 FK로 연동
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();
}

var guild = db.Guilds.Single(g => g.GuildName == "T1");
Single : 입력한 조건에 일치하는 하나의 element 반환, 조건에 부합하는 데이터가 두 개 이상이면 오류 반환
Update를 할 때 해당 테이블에 대한 전체 데이터를 수정하는 것이 아니라,
수정사항이 있는 데이터만 골라서 수정하게 됨
SaveChanges() -> 내부적으로 DetectChanges 호출DetectChange에서 데이터의 최초 Snapshot VS 현재 SnapShot 비교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; // 변경 데이터 수 반환
Connected Update :
Disconnected Update :
Update 3단계가 한 번에 단계적으로 일어나지 않고 끊기는 경우 - REST API 등
최초 Snapshot의 상태와 변경 Snapshot의 상태 간의 간격이 큼
클라이언트에게 정보를 보내고 한참 뒤에 수정하는 느낌
처리하는 2가지 방법
필요한 정보만 보내서, 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
모든 정보를 다 보내고 받아서, 아예 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
단점 : 모든 정보가 필요, 보안 문제 -> 상대를 신용할 때 사용해야 함
Dependent 데이터가 Principal 데이터 없이 존재할 수 있을까?
정책 상 달라질 수 있음
ex)
NullablePrincial Entitiy - FK가 가리키는 주요 데이터
Player classDependent Entity - 의존적인 데이터
Item class[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();
}
Player가 지워지면 Fk로 해당 Player를 참조하는 Item도 같이 삭제됨

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

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();
}


Guild<->Player
DbCommands.Update_1vM
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
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();
}

기존 데이터들은 다 비워짐
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 예시 코드
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 : 실제 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 오버라이딩으로 해결
EF Core 작동 스텝의 6단계 : OnModelCreating 함수 호출 (추가설정 = overrride)
AppDbContext.cs
protected override void OnModelCreating(ModelBuilder builder)
{
// 앞으로 Item Entity에 접근할 때, 항상 사용되는 모델 레벨의 필터링
// 필터를 무시하고 싶으면 IgnoreQueryFilters 옵션 추가
builder.Entity<Item>().HasQueryFilter(i => i.SoftDeleted == false);
}

