티스토리 뷰

// Models/Author.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; // Data Annotations도 함께 사용 가능

namespace CoreMVC.Models
{
    public class Author
    {
        public int Id { get; set; } // 기본 키

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        // 탐색 속성 (Navigation Property)
        // 이 저자가 쓴 도서들의 컬렉션 (일대다 관계의 '다' 부분)
        public ICollection<Book> Books { get; set; } = new List<Book>();
    }
}

 

// Models/Book.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; // [ForeignKey]를 위해 필요

namespace CoreMVC.Models
{
    public class Book
    {
        public int Id { get; set; } // 기본 키

        [Required]
        [StringLength(200)]
        public string Title { get; set; }

        // 외래 키 속성 (저자 ID)
        public int AuthorId { get; set; }

        // 탐색 속성 (Navigation Property)
        // 이 도서의 저자 (일대다 관계의 '일' 부분)
        [ForeignKey("AuthorId")] // 외래 키를 명시적으로 지정 (선택 사항, 컨벤션이 유추 가능)
        public Author Author { get; set; }

        // 탐색 속성 (다대다 관계를 위한 조인 엔티티 컬렉션)
        public ICollection<BookTag> BookTags { get; set; } = new List<BookTag>();
    }
}

 

// Models/Tag.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace CoreMVC.Models
{
    public class Tag
    {
        public int Id { get; set; } // 기본 키

        [Required]
        [StringLength(50)]
        public string Name { get; set; }

        // 탐색 속성 (다대다 관계를 위한 조인 엔티티 컬렉션)
        public ICollection<BookTag> BookTags { get; set; } = new List<BookTag>();
    }
}

 

// Models/BookTag.cs (다대다 관계를 위한 조인 엔티티)
namespace CoreMVC.Models
{
    // 이 클래스 자체는 기본 키가 없습니다. OnModelCreating에서 복합 키를 정의합니다.
    public class BookTag
    {
        // 외래 키 속성 (Book의 ID)
        public int BookId { get; set; }
        // 탐색 속성 (Book)
        public Book Book { get; set; }

        // 외래 키 속성 (Tag의 ID)
        public int TagId { get; set; }
        // 탐색 속성 (Tag)
        public Tag Tag { get; set; }
    }
}

 

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using CoreMVC.Models; // 정의한 모델 클래스들을 참조합니다.

namespace CoreMVC.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options)
        {
        }

        // 각 엔티티에 대한 DbSet을 정의합니다.
        public DbSet<Author> Authors { get; set; }
        public DbSet<Book> Books { get; set; }
        public DbSet<Tag> Tags { get; set; }
        public DbSet<BookTag> BookTags { get; set; } // 다대다 관계의 조인 엔티티

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder); // 기본 컨벤션 적용

            // --- 일대다 (Author <-> Book) 관계 구성 ---
            // Author는 여러 Book을 가질 수 있고 (HasMany), Book은 하나의 Author를 가집니다 (WithOne).
            modelBuilder.Entity<Author>()
                .HasMany(a => a.Books) // Author는 Books 컬렉션을 가짐
                .WithOne(b => b.Author) // Book은 하나의 Author를 가짐
                .HasForeignKey(b => b.AuthorId) // Book의 AuthorId가 외래 키임을 명시
                .OnDelete(DeleteBehavior.Cascade); // Author 삭제 시 관련 Book도 함께 삭제 (기본값)
                                                    // .OnDelete(DeleteBehavior.Restrict); // 삭제 제한 (SQL Server 기본값)
                                                    // .OnDelete(DeleteBehavior.SetNull); // 외래 키를 NULL로 설정 (AuthorId가 Nullable이어야 함)

            // --- 다대다 (Book <-> Tag) 관계 구성 (조인 엔티티 BookTag를 통해) ---
            // 1. 조인 엔티티 BookTag의 복합 기본 키 설정
            modelBuilder.Entity<BookTag>()
                .HasKey(bt => new { bt.BookId, bt.TagId }); // BookId와 TagId의 조합을 복합 키로 지정

            // 2. BookTag <-> Book 관계 설정
            modelBuilder.Entity<BookTag>()
                .HasOne(bt => bt.Book) // BookTag는 하나의 Book을 가짐
                .WithMany(b => b.BookTags) // Book은 여러 BookTag를 가질 수 있음
                .HasForeignKey(bt => bt.BookId); // BookTag의 BookId가 외래 키임을 명시

            // 3. BookTag <-> Tag 관계 설정
            modelBuilder.Entity<BookTag>()
                .HasOne(bt => bt.Tag) // BookTag는 하나의 Tag를 가짐
                .WithMany(t => t.BookTags) // Tag는 여러 BookTag를 가질 수 있음
                .HasForeignKey(bt => bt.TagId); // BookTag의 TagId가 외래 키임을 명시

            // --- 특정 컬럼에 데이터베이스 특정 타입 지정 ---
            // (선택 사항) 예를 들어, Book의 Title 컬럼을 특정 NVARCHAR 타입으로 제한 (기본값은 Max Length)
            modelBuilder.Entity<Book>()
                .Property(b => b.Title)
                .HasColumnType("nvarchar(200)"); // SQL Server의 NVARCHAR(200)으로 정확히 매핑

            // --- 인덱스 생성 ---
            // (선택 사항) 예를 들어, Author의 Name에 인덱스를 생성하여 검색 성능 향상
            modelBuilder.Entity<Author>()
                .HasIndex(a => a.Name)
                .IsUnique(); // Name이 고유해야 한다면 IsUnique() 추가 (UNIQUE INDEX)

            // --- 초기 데이터 시딩 (선택 사항) ---
            modelBuilder.Entity<Author>().HasData(
                new Author { Id = 1, Name = "제인 오스틴" },
                new Author { Id = 2, Name = "조지 오웰" }
            );

            modelBuilder.Entity<Book>().HasData(
                new Book { Id = 1, Title = "오만과 편견", AuthorId = 1 },
                new Book { Id = 2, Title = "1984", AuthorId = 2 },
                new Book { Id = 3, Title = "동물 농장", AuthorId = 2 }
            );

            modelBuilder.Entity<Tag>().HasData(
                new Tag { Id = 1, Name = "클래식" },
                new Tag { Id = 2, Name = "소설" },
                new Tag { Id = 3, Name = "디스토피아" },
                new Tag { Id = 4, Name = "풍자" }
            );

            modelBuilder.Entity<BookTag>().HasData(
                new BookTag { BookId = 1, TagId = 1 }, // 오만과 편견 -> 클래식
                new BookTag { BookId = 1, TagId = 2 }, // 오만과 편견 -> 소설
                new BookTag { BookId = 2, TagId = 2 }, // 1984 -> 소설
                new BookTag { BookId = 2, TagId = 3 }, // 1984 -> 디스토피아
                new BookTag { BookId = 3, TagId = 2 }, // 동물 농장 -> 소설
                new BookTag { BookId = 3, TagId = 3 }, // 동물 농장 -> 디스토피아
                new BookTag { BookId = 3, TagId = 4 }  // 동물 농장 -> 풍자
            );
        }
    }
}

 

코드 설명:

1. 일대다 관계 (Author <-> Book)

  • modelBuilder.Entity<Author>() .HasMany(a => a.Books) .WithOne(b => b.Author) .HasForeignKey(b => b.AuthorId) .OnDelete(DeleteBehavior.Cascade);
    • Author 엔티티는 Books 컬렉션(HasMany(a => a.Books))을 가집니다.
    • 각 Book 엔티티는 하나의 Author(WithOne(b => b.Author))를 가집니다.
    • Book 테이블의 AuthorId 속성이 외래 키(HasForeignKey(b => b.AuthorId))임을 명시합니다.
    • OnDelete(DeleteBehavior.Cascade)는 저자가 삭제될 때 해당 저자의 모든 도서도 함께 삭제되도록 설정합니다 (데이터 무결성 유지). 다른 옵션으로는 Restrict, SetNull 등이 있습니다.

2. 다대다 관계 (Book <-> Tag) - 조인 엔티티 BookTag를 통해

다대다 관계는 BookTag라는 중간 조인(Join) 엔티티를 사용하여 구성합니다.

  • modelBuilder.Entity<BookTag>().HasKey(bt => new { bt.BookId, bt.TagId });
    • BookTag 엔티티는 BookId와 TagId를 조합한 **복합 기본 키(Composite Primary Key)**를 가짐을 정의합니다. 이는 BookId와 TagId의 모든 고유한 조합이 BookTag 테이블의 고유한 행이 됨을 의미합니다. 데이터 어노테이션으로는 복합 키를 정의할 수 없습니다.
  • modelBuilder.Entity<BookTag>() .HasOne(bt => bt.Book) .WithMany(b => b.BookTags) .HasForeignKey(bt => bt.BookId);
    • BookTag 엔티티가 Book 엔티티와 일대다 관계를 가짐을 정의합니다. BookTag는 하나의 Book을 참조하고, Book은 여러 BookTag를 가집니다. BookId가 외래 키입니다.
  • modelBuilder.Entity<BookTag>() .HasOne(bt => bt.Tag) .WithMany(t => t.BookTags) .HasForeignKey(bt => bt.TagId);
    • BookTag 엔티티가 Tag 엔티티와 일대다 관계를 가짐을 정의합니다. BookTag는 하나의 Tag를 참조하고, Tag는 여러 BookTag를 가집니다. TagId가 외래 키입니다.

3. 기타 고급 매핑 예시

  • HasColumnType("nvarchar(200)"): Book의 Title 속성이 데이터베이스에서 정확히 NVARCHAR(200) 타입으로 매핑되도록 합니다. EF Core는 기본적으로 문자열 속성을 NVARCHAR(MAX) 또는 NVARCHAR(길이)로 유추하지만, 정확한 타입을 명시하는 데 유용합니다.
  • HasIndex(a => a.Name).IsUnique(): Author 엔티티의 Name 속성에 데이터베이스 인덱스를 생성하도록 지시합니다. IsUnique()를 추가하면 고유 인덱스가 되어, Name 컬럼에 중복된 값이 들어갈 수 없게 됩니다.

이 예제 코드를 통해 OnModelCreating과 Fluent API를 사용하여 데이터 어노테이션만으로는 정의하기 어려운 복잡한 데이터베이스 관계와 스키마 구성을 어떻게 할 수 있는지 이해하는 데 도움이 되기를 바랍니다.


간단하게 할 수 있는 방법은?

EF Core는 기본적으로 **컨벤션(Conventions)**을 통해 많은 관계를 자동으로 유추합니다.

  • 단순한 일대다 관계:
    • 예를 들어, Author와 Book 모델에 AuthorId 외래 키와 탐색 속성만 잘 정의되어 있다면, OnModelCreating에서 명시적으로 HasMany().WithOne().HasForeignKey()를 호출하지 않아도 EF Core가 대부분의 경우 관계를 올바르게 유추합니다.
    • 예시:
      // Author.cs
      public class Author {
      	public int Id { get; set; } 
          public string Name { get; set; } 
          public ICollection<Book> Books { get; set; } 
      }
      
      // Book.cs
      public class Book { 
      	public int Id { get; set; } 
          public string Title { get; set; } 
          public int AuthorId { get; set; } 
          public Author Author { get; set; } 
      }
      이 경우 OnModelCreating에서 특별히 설정하지 않아도 일대다 관계가 잘 매핑됩니다.
  • 다대다 관계의 단순화 (EF Core 5.0 이상):
    • EF Core 5.0부터는 조인 엔티티를 명시적으로 만들지 않고도 두 엔티티 간의 다대다 관계를 직접 구성할 수 있는 기능이 추가되었습니다. 이 경우 EF Core가 내부적으로 조인 테이블을 생성합니다.
    • 예시 (EF Core 5.0+):
      // Book.cs
      public class Book { 
      	public int Id { get; set; } 
          public string Title { get; set; } 
          public ICollection<Tag> Tags { get; set; } 
      }
      
      // Tag.cs
      public class Tag { 
      	public int Id { get; set; } 
          public string Name { get; set; } 
          public ICollection<Book> Books { get; set; } 
      }
      
      // AppDbContext.cs
      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          modelBuilder.Entity<Book>()
              .HasMany(b => b.Tags)
              .WithMany(t => t.Books);
      }
      이 방식은 BookTag와 같은 조인 엔티티를 직접 관리할 필요가 없어 코드가 훨씬 간결해집니다. 하지만 조인 테이블에 추가적인 컬럼(예: CreatedDate)이 필요하다면 여전히 명시적인 조인 엔티티를 사용해야 합니다.

다대다 관계와 조인 테이블: 추가 컬럼의 의미

 

다대다 관계와 조인 테이블: 추가 컬럼의 의미

다대다 관계와 조인 테이블: 추가 컬럼의 의미간단하게 설명하자면, "조인 테이블에 추가적인 컬럼(예: CreatedDate)이 필요하다면 여전히 명시적인 조인 엔티티를 사용해야 합니다"라는 말은 이런

jangjeonghun.tistory.com

 

EF Core 저장 프로시저 결과 매핑 전략

 

EF Core 저장 프로시저 결과 매핑 전략

저장 프로시저 결과 매핑 전략키 없는 엔티티 타입 (Keyless Entity Type)으로 매핑 (권장):저장 프로시저의 결과가 BookTag 엔티티의 컬럼과 완전히 일치하지 않거나, 더 많은 조인된 정보(예: Book.Title, T

jangjeonghun.tistory.com

 

 

728x90
반응형