
Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part8: Entity Framework Core'를 스터디하며 정리한 글입니다.
DB의 버전 관리는 어떻게 해야 할까? - 매우 중요!!
ex)
v1.1(라이브) - SQL
v2.2(현재) - 테이블에 새로운 칼럼을 추가
일단 EF Core DbContext <-> DB 상태에 대해 동의가 있어야 함
항상 최신 상태로 DB를 업데이트 하고 싶다는 말은 아님
솔루션에 Microsoft.EntityFrameworkCore.Tools 설치
도구 - NuGet 패키지 관리자 - 패키지 관리자 콘솔
Add-Migraion [Name]

Add-Migraion 단계
DbContext를 찾아서 분석 -> DB 모델링 (최신)
~ModelSnapshot.cs를 이용해서 가장 마지막 Migration 상태의 DB 모델링 (가장 마지막 상태)
1-2 비교 결과 도출
-> ModelSnapshot : 최신 DB 모델링 - 하나로만 관리됨
-> Migrate.Designer.cs와 Migrate.cs : Migration 관련된 세부 정보

Migrate.cs

수동으로 Up/Down을 입력할 수도 있음 - But 비추천
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
DbCommands.InitializeDB()
db.DataBase.Migrate()
중간에 Migration을 사용할 경우 DB를 한 번 날려주고 다시 reset해서 사용해주는 것이 좋음
Update-Database [option] - 옵션 없을 시 최신 상태로 적용


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


Remove-Migration - 마지막 Migration 삭제


DB만 있는 상태에서 자동으로
AppDbContext만들어주는 방식
EF Core Power Tools 설치
https://marketplace.visualstudio.com/items?itemName=ErikEJ.EFCorePowerTools
프로젝트 - EF Core Power Tools - Reverse Engineer(역공학)



DbContext, DB 둘 다 없는 상황에서 SQL을 직접 작성하여 기반으로 하는 방식
C++의 경우 꽤 많이 사용됨
1) 손수 수작업
2) Script-Migration [From] [To] [Options]
3) DB끼리의 비교를 이용해서 SQL 추출
DbContext 심화 - 최적화 관련
1) ChangeTracker
2) Database
3) Model
No Tracking : 추적되지 않는 상태
SaveChanges()를 해도 존재도 모름
DB에는 있고, 수정사항이 없었음
메모리상의 데이터와 DB상의 데이터 동일
SaveChanges()를 해도 갱신 X
DB에는 아직 존재하지만 삭제되어야 하는 상태
SaveChanges()로 DB 데이터 삭제
DB에는 존재하지만 클라이언트에서 수정된 상태
SaveChanges()로 DB 데이터 갱신
DB에는 아직 존재하지 않음
클라이언트에서 데이터가 추가됨
SaveChanges()로 DB 데이터 추가
메소드
Entry().State
Entry().Property().IsModified
Entry().Navigation().IsModified
Stage가 대부분 직관적이지만 RelationShip이 개입하면 살짝 더 복잡함
상태 변화
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);
}
상태 변화
Unchanged or Modified or DeletedAdded// 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 처리 등을 위해


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 상태 변화
Unchanged or Modified or DeletedAdded// 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
}


Untracked Entity를 Tracked Entity로 변경
DB 접근 전에 강제로 세팅해주는 느낌
Relationship 상태 변화
UnchangedAdded// 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를 조작할 수 있다 - 최적화 등의 이유
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();
}

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


상태 정보의 변화를 감지하고 싶을 때 유용
ex)
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을 호출할 수 있음
ex)
Stored Procedure 호출 등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
}
}
ExecuteSqlRaw / ExecuteSqlInterpolated 두 버전으로 나뉘어짐
Non-Query (SELECT가 아닌) SQL - UPDATE도 가능
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"
}

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"
}
->
