
Inflearn - '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part8: Entity Framework Core'를 스터디하며 정리한 글입니다.
ORM (Object-relational mapping) : 객체 관계 매핑은 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법
객체 지향 언어에서 사용할 수 있는 "가상" 객체 데이터베이스를 구축하는 방법임
객체 관계 매핑을 가능하게 하는 상용 또는 무료 소프트웨어 패키지들이 있고, 경우에 따라서는 독자적으로 개발하기도 함
1) 개발 속도가 매우 빨라진다
2) 버전 관리
학습할 땐 다소 지루하지만 한 번 배워놓으면, 그 값어치를 제대로 하는 기술이다!
(Web, Tool, GameServer 공용 기술)
Visual Studio - Entity Framwork Core SqlServer 설치
Datamodels.cs
// 클래스 이름 = DB 테이블 이름 = Item
[Table("Item")]
public class Item
{
// DB Column
// '클래스이름'Id -> Primary Key
public int ItemId { get; set; } // DB에서 관리하는 고유 Id
public int TemplateId { get; set; } // Data Sheet에서 관리하는 Id - 어떤 종류의 아이템인지 ex) 101 -> 크라켄
public DateTime CreatedDate { get; set; } // 아이템 생성 시간
// 다른 클래스 참조 -> Foreign Key(외부 키 참조) : Navigational Property <- Entity Framework
public int OwnerId { get; set; }
public Player Owner { get; set; } // 아이템 소유자
}
Entity Framework
AppDbContext.cs
public class AppDbContext : DbContext
{
// Dbset<Item> -> EF Core에게 알려줌
// "Items이라는 DB 테이블이 있는데, 세부적인 칼럼/키 정보는 Item class를 참고하라!"
public DbSet<Item> Items { get; set; }
// 어떤 DB를 어떻게 연결해라 - (각종 설정, Authorization 등)
public const string ConnectionString = "DB 속성 참조";
// 처음 DB와 연동하는 부분
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(ConnectionString);
}
}
DbSet<T>를 찾음Program.cs
public static void InitializeDB(bool forceReset = false)
{
using AppDbContext db = new AppDbContext();
{
// 강제 Reset이 아니고
// 만약 DB가 이미 만들어져 있다면
if (!forceReset && (db.GetService<IDatabaseCreator>() as RelationalDatabaseCreator).Exists())
return;
db.Database.EnsureDeleted(); // 처음 초기화
db.Database.EnsureCreated(); // 모델링과 일치하게 생성
Console.WriteLine("DB Initialized");
}
//db.Dispose(); // 사용 후 처리
}
static void Main(string[] args)
{
InitializeDB(forceReset: true);
}

DbCommands.cs로 CRUD 명령어 관리
Program.cs에 있던 InitializeDB 또한 이동// DB 지정이 필요
public static void CreateTestData(AppDbContext db)
{
var player = new Player()
{ Name = "Yijun" };
List<Item> items = new List<Item>()
{
// Primary Key는 명시적으로 설정해 주지 않아도 자동 설정됨
new Item()
{
TemplateId = 101,
CreatedDate = DateTime.Now,
Owner = player,
},
...
};
// 여러개를 동시에 추가
db.Items.AddRange(items);
db.SaveChanges();
}
public static void ReadAll()
{
using (var db = new AppDbContext()) // 현재 DB 정보 불러옴
{
// Include : Eager Loading (즉시 로딩)
foreach(Item item in db.Items.AsNoTracking().Include(i => i.Owner))
{
Console.WriteLine($"TemplateId({item.TemplateId}) Owner({item.Owner.Name}) Created({item.CreatedDate})");
}
}
}
Tracking Snapshot : 데이터 변경 탐지 기능
AsNoTracking() : ReadOnly로 접근 - Tracking Snapshot에서 감지할 필요가 없다고 알림
item.Owner.Name : foreign key를 직접 접근하면 오류 발생
Include(i => i.Owner) 방식의 Eager Loading 사용
// 특정 플레이어가 소지한 아이템들의 CreateDate 수정
public static void UpdateDate()
{
Console.WriteLine("Input Player Name");
Console.Write("> ");
string name = Console.ReadLine();
using (var db = new AppDbContext())
{
// LINQ 문법
var items = db.Items.Include(i => i.Owner)
.Where(i => i.Owner.Name == name);
foreach(Item item in items)
{
item.CreatedDate = DateTime.Now;
}
db.SaveChanges();
}
ReadAll();
}
// 특정 이름 플레이어 아이템들 삭제
public static void DeleteItem()
{
Console.WriteLine("Input Player Name");
Console.WriteLine("> ");
string name = Console.ReadLine();
using (var db = new AppDbContext())
{
// LINQ 문법
var items = db.Items.Include(i => i.Owner)
.Where(i => i.Owner.Name == name);
// 여러개 삭제
db.Items.RemoveRange(items);
db.SaveChanges();
}
ReadAll();
}

데이터 모델링의 클래스 : Entity class
현재 Player Entity class는 Item class의 DbSet을 만들면서 간접적으로 생성이 되기 때문에 직접적인 접근이 안됨
따라서 Player DbSet 테이블도 추가
public DbSet<Player> Players { get; set; }
Player : Item = 1:다 관계
1:다 관계 - '다'의 테이블에서 FK로 '1' 참조
특정 Player가 가진 모든 Item search 기능
-> Player class에도 Item ICollection 추가
public ICollection<Item> Items { get; set; }
ICollection은 List의 부모격, 가장 빠름
만약 강제로 1:1 관계로 만들었을 때, 서로 어떤 테이블을 FK로 정할지 몰라서 에러가 나게 됨
해결 방법
//public int OwnerId { get; set; }
public Player Owner { get; set; }
-> 에러
public int OwnerId { get; set; }
public Player Owner { get; set; }
[ForeignKey("OwnerId")]
public Player Owner { get; set; }
// 특정 길드에 있는 길드원들이 소지한 모든 아이템들 열람
public static void EagerLoading()
{
Console.WriteLine("길드 이름을 입력하세요");
Console.Write(" > ");
string name = Console.ReadLine();
using (var db = new AppDbContext())
{
Guild guild = db.Guilds.AsNoTracking()
.Where(g => g.GuildName == name)
.Include(g => g.Members) // 1차원적 연동 - Members의 Player
.ThenInclude(p => p.Item) // 2차원적 연동 - Members의 Player의 Item
.First(); // 하나만 추출
foreach(Player player in guild.Members)
{
Console.WriteLine($"ItemId({player.Item.TemplateId}) Owner({player.Name})");
}
}
}
장점 : DB 접근 한 번으로 전부 로딩 가능 - SQL : JOIN
단점 : 필요한 범위를 정확히 모를 경우에도 전부 로딩
// 특정 길드에 있는 길드원들이 소지한 모든 아이템들 열람
public static void ExplicitLoading()
{
Console.WriteLine("길드 이름을 입력하세요");
Console.Write(" > ");
string name = Console.ReadLine();
using (var db = new AppDbContext()) // 현재 DB 정보 불러옴
{
// 단계적으로 하나하나 Select
Guild guild = db.Guilds
.Where(g => g.GuildName == name)
.First();
// 명시적
db.Entry(guild).Collection(g => g.Members).Load(); // List 등 -> Collection
foreach(Player player in guild.Members)
{
db.Entry(player).Reference(p => p.Item).Load(); // 객체 -> Reference
}
foreach (Player player in guild.Members)
{
Console.WriteLine($"ItemId({player.Item.TemplateId}) Owner({player.Name})");
}
}
}
Explicit Loading의 경우 AsNoTracking() 적용 시 오류
장점 : 필요한 시점에 필요한 데이터만 로딩 가능
단점 : DB에 여러번 접근하기 때문에 접근 비용 많이 듦
Anonymous type으로 직접 추출 - SQL의 SELECT Count(*) 느낌// 특정 길드에 있는 길드원 수
public static void SelectLoading()
{
Console.WriteLine("길드 이름을 입력하세요");
Console.Write(" > ");
string name = Console.ReadLine();
using (var db = new AppDbContext())
{
var info = db.Guilds // var로 추출하는 것이 국룰
.Where(g => g.GuildName == name)
.Select(g => new // Anonymous type
{
Name = g.GuildName,
MemberCount = g.Members.Count
})
.First();
Console.WriteLine($"GuildName({info.Name}), MemberCount({info.MemberCount})");
}
}
장점 : 필요한 정보만 직접적으로 추출 가능
단점 : Select 문을 일일이 작성해줘야 함
DTO (Data Transfer Object) : 데이터 운반 객체
Entity Class와 다른 중반 유통과정 클래스 - 파일을 따로 관리하는 것이 편함
Select Loading의 단점을 보완하는 느낌
DataModel.cs
public class GuildDto
{
public string Name { get; set; }
public int MemberCount { get; set; }
}
Extensions.cs
public static class Extensions // Extensions 메소드는 static이 국룰
{
public static IQueryable<GuildDto> MapGuildToDto(this IQueryable<Guild> guild) // 정해진 메소드 형식
{
return guild.Select(g => new GuildDto()
{
Name = g.GuildName,
MemberCount = g.Members.Count
});
}
}
SelectLoading()
using (var db = new AppDbContext())
{
var info = db.Guilds // var로 추출하는 것이 국룰
.Where(g => g.GuildName == name)
.MapGuildToDto()
.First();
Console.WriteLine($"GuildName({info.Name}), MemberCount({info.MemberCount})");
}