[.Net Core] Entity Framework Core : 데이터 쿼리

Yijun Jeon·2022년 10월 13일

Entity Framework Core

목록 보기
1/4
post-thumbnail

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

ORM

ORM (Object-relational mapping) : 객체 관계 매핑은 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법

객체 지향 언어에서 사용할 수 있는 "가상" 객체 데이터베이스를 구축하는 방법임

객체 관계 매핑을 가능하게 하는 상용 또는 무료 소프트웨어 패키지들이 있고, 경우에 따라서는 독자적으로 개발하기도 함

  • DB와 프로그래밍 언어 간의 데이터를 변환하는 기법
  • C# 문법을 이용해 메모리 데이터를 수정하면 DB에 변경 사항이 적용

장점

1) 개발 속도가 매우 빨라진다

  • SQL을 일일이 직접 작성하면 실수 여지도 많고 작업 속도가 느림

2) 버전 관리

  • 라이브를 고려하면 코드 버전에 따라 유동적으로 DB 상태도 변화가 필요
  • ORM을 이용하면 Migration 스크립트를 자동 생성 가능

학습할 땐 다소 지루하지만 한 번 배워놓으면, 그 값어치를 제대로 하는 기술이다!
(Web, Tool, GameServer 공용 기술)

환경 설정

Visual Studio - Entity Framwork Core SqlServer 설치

DB 연결

데이터 모델링

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

  • '클래스이름'Id -> DB에서 Primary Key로 인식
  • 다른 클래스 참조 Column (Foreign Key) -> Navigational Property

DB 연동

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);
    }
}
  • ConnectionString - DB 생성 -> 속성 -> 일반 -> 연결 문자열

EF Core 작동 단계

  1. DbContext 만듬
  2. DbSet<T>를 찾음
  3. 모델링 class 분석하여 column을 찾음
  4. 모델링 class에서 참조하는 다른 class가 있으면, 그 class도 분석
  5. Navigational Property의 대상 class는 DbSet을 따로 추가하지 않아도 간접적으로 만들어짐
  6. OnModelCreating 함수 호출 (추가설정 = overrride)
  7. Database 전체 모델링 구조를 내부 메모리에 들고 있게됨

DB 생성

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

빌드 결과


CRUD

DbCommands.cs로 CRUD 명령어 관리

  • Program.cs에 있던 InitializeDB 또한 이동

Create

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

Read

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 사용

Update

// 특정 플레이어가 소지한 아이템들의 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();
}

Delete

// 특정 이름 플레이어 아이템들 삭제
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();
}

실행 결과


Relationship

데이터 모델링의 클래스 : Entity class

현재 Player Entity class는 Item class의 DbSet을 만들면서 간접적으로 생성이 되기 때문에 직접적인 접근이 안됨
따라서 Player DbSet 테이블도 추가

public DbSet<Player> Players { get; set; }

DB 관계 모델링

  • 1:1 관계
  • 1:다 관계 - 주로 가장 많이 사용됨
  • 다:다 관계

1:다 관계

Player : Item = 1:다 관계

  • Player 당 하나의 Item만 소유 가능 -> 1: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; }
  • 1:1 관계에서 여러번 지정하게 되면 마지막 지정만 할당됨

데이터 로딩

Eager Loading

  • 데이터를 불러올 때 필요한 시점에 즉시 로딩
// 특정 길드에 있는 길드원들이 소지한 모든 아이템들 열람
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
단점 : 필요한 범위를 정확히 모를 경우에도 전부 로딩

Explicit Loading

  • 단계적으로 하나하나 Select 하여 명시적으로 로딩
// 특정 길드에 있는 길드원들이 소지한 모든 아이템들 열람
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에 여러번 접근하기 때문에 접근 비용 많이 듦

Select Loading

  • 필요한 정보를 Select로 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

DTO (Data Transfer Object) : 데이터 운반 객체

Entity Class와 다른 중반 유통과정 클래스 - 파일을 따로 관리하는 것이 편함

Select Loading의 단점을 보완하는 느낌

DataModel.cs

public class GuildDto
{ 
    public string Name { get; set; }
    public int MemberCount { get; set; }
}

Extension 메소드

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
        });
    }
}
  • IEnumerable (LINQ to Object / LINQ to XML 쿼리) :
    한 번에 모든 데이터를 메모리에 들고있는 경향이 있음
  • IQueryable (LINQ to SQL 쿼리)

코드 수정

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

0개의 댓글