[.Net Core] Entity Framework Core : 세부 설정

Yijun Jeon·2022년 10월 17일

Entity Framework Core

목록 보기
3/4
post-thumbnail

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

Data Modeling Configuration

Configuration : 세부설정

Convention -> Data Annotation -> Fluent API 순서로 우선순위

Convention : 관례

  • 가장 많이 사용

각종 형식과 이름 등을 정해진 규칙에 맞게 만들면 EF Core에서 알아서 처리

쉽고 빠르지만, 모든 경우를 처리할 수는 없음
ex) 클래스이름Id -> PK로 자동인식

주요사항

  1. Entity Class 관련

    • public 접근 한정자로 선언 + non-static
    • property 중에서 public getter를 찾으면서 분석
    • property 이름 -> table column 이름
  2. 이름, 형식, 크기 관련

    • .NET 형식 <-> SQL 형식 (int, bool) - 자동 변환
    • .NET 형식의 기본 Nullable 여부를 따라감
      ex) string -> nullable, int -> non-null, int? -> nullable
  3. PK 관련

    • Id 혹은 <클래스이름>Id 정의된 property -> PK로 인정
      - 후자 권장
    • 복합키 (Composite Key) : Convnetion으로 처리 불가
      - 두 가지 property로 하나의 PK 구성

Data Annotation : 데이터 주석

class / property 등에 Attribute를 붙여 추가 정보
ex) [Table("Item)"] -> Table 이름 "Item"으로 설정

Fluent API : 직접 정의

OnModelCreating에서 직접 설정을 정의해서 만드는 '귀찮은' 방식
활용범위는 가장 넒음
ex)

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<Item>().HasQueryFilter(i => i.SoftDeleted == false);
}

DB 활용

Q1. DB column type, size, nullable

ConventionAnnotationFluent Api
Nullable[Required].isRequired()
문자열 길이[MaxLength(20)].HasMaxLength(2)
문자 형식.IsUnicode(true)

Q2. PK

ConventionAnnotationFluent Api
PK<클래스이름>Id[Key].HasKey()
복합키[Key][Column(Order=0)],[Key][Column(Order=1)].HasKey(x => new{x.Prop1, x.Prop2})

Q3. Index

ConventionAnnotationFluent Api
인덱스 추가.HasIndex(p => p.Prop1)
복합 인덱스 추가.HasIndex(p => p.Prop1, p.Prop2)
인덱스 이름 정해서 추가.HasIndex(p => p.Prop1).HasName("Index_Name")
유니크 인덱스 추가.HasIndex(p => p.Prop1).IsUnique()

Q4. 테이블 이름

ConventionAnnotationFluent Api
Table NameDBSet<T> property이름 or class 이름[Table("Name")].ToTable("Name")

Q5. 칼럼 이름

ConventionData AnnotationFluent Api
Column Name변수 이름[Column("Name")].HasColumnName("Name")

Q6. 코드 모델링에서는 사용, DB 모델링에서는 제외 - property/class

ConventionData AnnotationFluent Api
[NotMapped].Ignore()

Q7. Soft Delete

ConventionData AnnotationFluent Api
Soft Delete.HasQueryFilter()

언제 무엇을 우선으로 사용해야 하나?

1) Convention이 가장 무난
2) Validation과 관련된 부분들은 Data Annotaion - 직관적, SaveChanges 용이
3) 그 외에는 Fluent Api

Relationship Configuration

Convention -> Data Annotaion -> Fluent API

기본 용어

  • Principal Entity
  • Dependent Entity
  • Navigational Property
  • Primary Key (PK)
  • Foreign Key (FK)
  • Principal Key = PK or Unique Alternate Key
    - PK 대신 주 키로 대체 가능
  • Required Relationship - Not Null
  • Optional Relationship - Nullable

FK 설정

FK & Nullable
1) Required Relationship (Not Null)

  • 삭제할 때 OnDelete 인자를 Cascade 모드로 호출
  • Principal 삭제시 Dependent도 삭제

2) Optional Relationship (Nullable)

  • 삭제할 때 OnDelete 인자를 ClientSetNull 모드로 호출
  • Princiap 삭제시 Dependent Tracking -> FK Null로 세팅
  • Princiap 삭제시 Dependent Non-Tracking -> Exception 방생

Convention

Convention을 이용한 FK 설정
1. <PrincipalKeyName> -> PlayerId - 가장 많이 사용
2. <Class><PrincipalKeyName> -> PlayerPlayerId
3. <NavigarionalPropertyName><PrincipalKeyName> -> OwnerPlayerId | OnwerId

Convention 방식으로 못하는 것들

  1. 복합 FK
  2. 다수의 Navigational Property가 같은 클래스를 참조할 때
    ex)
public int OwnerId { get; set; }
public Player Owner { get; set; } // 아이템 소유자
public int CreatorId { get; set; }
public Player Creator { get; set; } // 아이템 창시자

-> 불가능
  1. DB나 삭제 관련 커스터마이징

Data Anotation

[ForeignKey("Prop_Name")]

[ForeignKey("Owner")]
public int OwnerId { get; set; }
public Player Owner { get; set; } // 아이템 소유자
둘 다 가능
public int OwnerId { get; set; }
[ForeignKey("OwnerId")]
public Player Owner { get; set; } // 아이템 소유자

[InverseProperty] -> 다수의 Navigational Property가 같은 클래스를 참조할 때

[InverseProperty("OwnedItem")]
public int OwnerId { get; set; }
public Player Owner { get; set; }
둘 다 가능 
[InverseProperty("Owner")]
public Item OwnedItem { get; set;}

Fluent Api

  • 자신 : .HasOne(), .HasMany()
  • 상대방 : .WithOne(), .WithMany()
  • .HasForeignKey()
  • .IsRequired() - Non-Nullable
  • .OnDelete() - 삭제 커스터마이징
  • .HasConstraintName()
  • .HasPrincipalKey()

Convention으로만 선언해놓은 경우
AppDbContext.OnModelCreating()

builder.Entity<Player>()
    .HasMany(p => p.CreatedItems) // Player의 입장
    .WithOne(i => i.Creator) // Item의 입장
    .HasForeignKey(i => i.CreatorId); // 1:M 경우 반드시 M 쪽에 FK

builder.Entity<Player>()
    .HasOne(p => p.OwnedItem)
    .WithOne(i => i.Owner)
    // 1:1 경우 FK 위치가 정해져 있지 않기 때문에
	// 캐스팅으로 지정 필요
    .HasForeignKey<Item>(i => i.OwnerId);

Shadow Property & Backing Field

Shadow Property

  • class에는 있지만 DB에는 없음 -> [NotMapped], .Ignore()
  • DB에는 있지만 class에는 없음 -> ShadowProperty
    ex) RecoveredDate - DB에서 복구한 시각
    • class에 있을 필요가 없음

생성 -> .Property<T>("Name")
AppDbContext.OnModelCreating

builder.Entity<Item>().Property<DateTime>("RecoveredDate");

Read/Write -> .Property("Name").CurrentValue

db.Entry(items[0]).Property("RecoveredDate").CurrentValue = DateTime.Now;

Bakcing Field (EF Core)

이전 버전에는 없었음

  • private field를 DB에 매핑하고, public getter로 가공해서 사용할 때 유용
    ex) DB에는 json 형태로 string을 저장하고, getter는 json을 parsing해서 가공하여 사용

private property에 하나의 layer를 더 추가하여 public property로 접근

public struct ItemOption
{
    public int str;
    public int dex;
    public int hp;
}

[Table("Item")]
public class Item
{
    private string _jsonData;
    public string JsonData
    {
        get { return _jsonData; }
        //set { _jsonData = value; }
    }

    public void SetOption(ItemOption option)
    {
        _jsonData = JsonConvert.SerializeObject(option);
    }
    public ItemOption GetOption()
    {
        return JsonConvert.DeserializeObject<ItemOption>(_jsonData);
    }
}

DbCommands.cs
items[0].SetOption(new ItemOption() { dex = 1, hp = 2, str = 3 });

public property의 경우 setter가 반드시 있어야 DB에 들어갈 수 있음

  • But, setter가 있으면 외부에서 직접 접근할 수 있어지기 때문에 추가 조치 필요

-> 일반적으로 Fluent Api 방식만 사용 : .HasField("Prop_Name")

AppDbContext.OnModelCreating()

// Backing Field
builder.Entity<Item>()
    .Property(i => i.JsonData)
    .HasField("_jsonData");


Entity Class & Table Mapping

Entity Class 하나를 통으로 Read/Write -> 부담

Owned Type

일반 class를 Entity Class에 추가하는 개념

public class ItemOption
{ 
    public int Str { get; set; }
    public int Dex { get; set; }   
    public int Hp { get; set; }
}
-> PK 없이 Item Entity에서 ItemOPtion을 소유하고 싶을 때
public class Item
{
	...
    public ItemOption Option { get; set; }
    ...
}

동일한 Table에 추가

  • .OwnsOne()

AppDbContext.OnModelCreating()

// Owned Type
builder.Entity<Item>()
    .OwnsOne(i => i.Option);

Relationship이 아닌 Ownership의 개념이기 때문에 .Include() 없이 자동 로딩

장점 : 관리 측면, 효율성 측면 모두 유용함

다른 Table에 추가

  • .OwnsOne().ToTable()

AppDbContext.OnModelCreating()

// Owned Type
builder.Entity<Item>()
    .OwnsOne(i => i.Option)
    .ToTable("ItemOption");

ItemOption 테이블은 ItemId가 PK, FK 겸용

Table Per Hierarchy (TPH)

상속 관계의 여러 class를 하나의 테이블에 매핑

  • ex) Item -> EventItem (기간제 아이템)

Convention

  • 일단 부모 class를 상속받아 만들고, DbSet에 추가
    -> Disciriminator column에서 어떤 Item class인지 분류

AppDbContext.cs

public DbSet<Item> Items { get; set; }
// TPH
public DbSet<EventItem> EventItems { get; set; }

단점 : Item을 접근하고 싶을 때 Items,EventItems 따로 접근해야 함

Fluent Api

  • EventItem의 DbSet 추가 필요 X
    -> .HasDiscriminator().HasValue()
public enum ItemType
{ 
    NormalItem,
    EventItem
}
-> Item.Type

DbCommands.CreateTestData()

new EventItem()
{
    TemplateId = 102,
    CreatedDate = DateTime.Now,
    Owner = faker,
    DestoryDate = DateTime.Now,
},

AppDbContext.OnModelCreating()

// TPH
builder.Entity<Item>()
    .HasDiscriminator(i => i.Type) // 구분자
    .HasValue<Item>(ItemType.NormalItem) // 구분 값
    .HasValue<EventItem>(ItemType.EventItem);

Table Splitting

  • 다수의 Entity Class를 하나의 테이블에 매핑
  • Relationship을 한 테이블에 몰빵

Fluent Api

  • ItemDetail의 DbSet 추가 필요 X
    -> .ToTable("Table_Name")
public class ItemDetail
{ 
    public int ItemDetailId { get; set; }
    public string Description { get; set; }
}
-> Item.Detail
// Convention 만으로 빌드하면 Navigational Property로 다른 Table이 생성됨

DbCommands.CreateTestData()

// Test Table Splitting
items[2].Detail = new ItemDetail(){ Description = "this is detail item" };

AppDbContext.OnModelCreating()

// Table Splitting
builder.Entity<Item>()
    .HasOne(i => i.Detail)
    .WithOne() // ItemDetail에는 X
    .HasForeignKey<ItemDetail>(i => i.ItemDetailId);

// 같은 "Item" 테이블로 몰빵
builder.Entity<Item>().ToTable("Items");
builder.Entity<ItemDetail>().ToTable("Items");

Ownership이 아닌 Relationship의 개념이기 때문에 로딩 시 .Include() 필요


Backing Field & Relationship

Backing Field : private field를 DB에 매핑

Navigational Property에도 사용 가능!

-> private Field로 사용하면서도 DB에 테이블도 생성

  • 아이템 리뷰 - 별점 시스템

DataModel.cs

public class ItemReview
{ 
    public int ItemReviewId { get; set; }
    public int Score { get; set; } // 0~5점
}

public class Item
{	
	public double? AverageScore { get; set; } // 평균 별점
    public readonly List<ItemReview> _reviews = new List<ItemReview>();
    // public ICollection<ItemReview> Reviews { get; set; }
    public IEnumerable<ItemReview> Reviews { get { return _reviews.ToList(); } }

    public void AddReview(ItemReview review)
    {
        _reviews.Add(review);
        AverageScore = _reviews.Average(r => r.Score);
    }
    public void RemoveReview(ItemReview review)
    {
        _reviews.Remove(review);
        // 리뷰가 하나라도 있다면 평균
        AverageScore = _reviews.Any() ? _reviews.Average(r => r.Score) : (double?)null; 
    }

}

readonly : get은 가능 set은 불가능
IEnumerable : 직접적으로 Add 불가능

AppDbContext.OnModelCreaing()

// Backing Field + Relationship
builder.Entity<Item>()
    .Metadata
    .FindNavigation("Reviews")
    .SetPropertyAccessMode(PropertyAccessMode.Field);

DbCommands.CreateTestData()

// Backing Field + Relationship
items[0].AddReview(new ItemReview() { Score = 5 });
items[0].AddReview(new ItemReview() { Score = 4 });
items[0].AddReview(new ItemReview() { Score = 1 });
items[0].AddReview(new ItemReview() { Score = 5 });


User Defined Function

User Defined Function (UDF) : 사용자가 직접 만든 SQL을 호출하게 하는 기능

  • 연산을 Contents 쪽이 아니라 DB 쪽에서 하도록 떠넘기고 싶을 때
  • EF Core 쿼리가 약간 비효율적임

UDF Step

  1. Configuration
    • static 함수를 만들고 EF Core에 등록
  2. Database Setup
  3. 사용

Step 1 : Configuration

직접 계산하던 ItemReview의 AverageScore -> DB 쪽에서 계산하도록 변경

public clas Item
{
	public ICollection<ItemReview> Reviews { get; set; }
}

함수 EF Core 등록 방법

  1. Annotation : [DbFunction()]
  2. Fluent Api : HasDbFunction()

Program.cs

// Annotation(Attribute)
[DbFunction()]
public static double? GetAverageReviewScore(int itemId)
{
    // C#에서 사용하는 용도가 아닌 UDF 용도
    throw new NotImplementedException("사용 금지!");
}

OR

// AppDbContext.OnModelCreating()
builder.HasDbFunction(() => Program.GetAverageReviewScore(0));

Step 2 : Database Setup

Database에 사용자 정의 함수 SQL로 전달

DbCommands.InitializeDB()

string command =
    @" CREATE FUNCTION GetAverageReviewScore (@itemId INT) RETURN FLOAT
        AS
        BEGIN

        DECLARE @result AS FLOAT

        SELECT @result =  AVG(CAST([Score] AS FLOAT))
        FROM ItemReview As r
        WHERE @itemId = r.ItemId

        RETURN @result

        END
    ";
db.Database.ExecuteSqlRaw(command);

step 3 : 사용

DbCommands.CreateTestData()

// UDF Test
items[0].Reviews = new List<ItemReview>()
{
    new ItemReview(){Score = 5},
    new ItemReview(){Score = 3},
    new ItemReview(){Score = 2},
};

items[1].Reviews = new List<ItemReview>()
{
    new ItemReview(){Score = 1},
    new ItemReview(){Score = 1},
    new ItemReview(){Score = 0},
};

DbCommands.cs

public static void ClacAverage()
{
    using(AppDbContext db = new AppDbContext())
    {
        // Program.GetAverageReviewScore(itemId) // 오류 발생
        foreach(double? average in  db.Items.Select(i => Program.GetAverageReviewScore(i.ItemId)))
        {
            if(average == null)
                Console.WriteLine("No Review!");
            else
                Console.WriteLine($"Average : {average.Value}");
        }
    }
}

  • DB에서 직접 계산하여 return 값 출력

  • 스칼라 반환 : 값 하나 반환

초기값 설정

초기값을 설정할 때 주의할 점

  • Entity Class 자체의 초기값으로 적용되는 경우
  • DB Table 차원에서 초기값으로 적용되는 경우

-> EF & DB 연동 외에 다른 경로로 DB를 사용한다면, 차이가 날 수 있음
ex) SQL Script

Default Value 설정 방법

  1. Auto-Property Initializer (C# 6.0)
    • 바로 할당
  2. Fluent Api
    • .HasDefaultValue
  3. SQL Fragment
    • .HasDefaultValueSql
  4. Value Generator
    • 일종의 Generator 규칙

Auto-Property Initializer

Entity Class 자체의 초기값으로 적용되는 경우

  • Entity 차원의 초기값 -> SaveChanges로 DB 적용
public DateTime CreatedDate { get; set; } = new DateTime(2020, 1, 1);

Fluent Api

DB Table 차원에서 초기값으로 적용되는 경우

  • DB Table DEFAULT를 적용
    .HasDefaultValue
builder.Entity<Item>()
    .Property("CreateDate")
    .HasDefaultValue(new DateTime(2020, 1, 1));

DateTime.Now와 관련된 부분은 현재 시간이 제대로 들어가지 않음

SQL Fragment (Fluent Api)

DB Table 차원에서 초기값으로 적용되는 경우

  • 새로운 값이 추가되는 시점에 DB쪽에서 실행
    .HasDefaultValueSql
// 임의로 외부에서 직접 할 수 없게 private set으로 제한해야 함
public DateTime CreatedDate { get; private set; } 

builder.Entity<Item>()
    .Property("CreatedDate")
    .HasDefaultValueSql("GETDATE()");

Value Generator

DB Table 차원에서 초기값으로 적용되는 경우

  • EF Core에서 실행됨 - 일종의 Generator 규칙

DataModel.cs

public class PlayerNameGenerator : ValueGenerator<string>
{
    public override bool GeneratesTemporaryValues => false;

    public override string Next(EntityEntry entry)
    {
        string name = $"Player_{DateTime.Now.ToString("yyyyMMdd")}";
        return name;
    }
}
builder.Entity<Player>()
    .Property(p => p.Name)
    .HasValueGenerator((p, e) => new PlayerNameGenerator());

0개의 댓글