[EntityFramework Core] 관계 1 시도

소개



이번에는 외래 키 설정, "포함"방법 등과 같은 관계에 대해 EF Core를 사용해 보겠습니다.

환경


  • ASP.NET Core 버전 6.0.100-preview.1.21103.13
  • Microsoft.EntityFrameworkCore 버전 6.0.0-preview.1.21102.2
  • Npgsql.EntityFrameworkCore.PostgreSQL 버전 6.0.0-preview1
  • Microsoft.EntityFrameworkCore.Design 버전 6.0.0-preview.1.21102.2
  • Microsoft.AspNetCore.Mvc.NewtonsoftJson 버전 6.0.0-preview.1.21103.6

  • DB 테이블





    DB 마이그레이션



    먼저 DB 마이그레이션 시 외래키 설정을 해보도록 하겠습니다.

    속성만 포함(ID 없음)



    Author.cs




    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    namespace BookStoreSample.Models
    {
        [Table("author")]
        public record Author
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
            // Many to 1
            public List<Book> Books { get; init; } = new List<Book>();
        }
    }
    


    Book.cs




    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    namespace BookStoreSample.Models
    {
        [Table("book")]
        public record Book
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
            // 1 to many
            public Author? Author { get; init; }
            // Many to 1
            public List<BookPrice> BookPrices { get; init; } = new List<BookPrice>();
            // Many to many
            public List<BookStore> BookStores { get; init; } = new List<BookStore>();
        }
    }
    


    BookStore.cs




    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BookStoreSample.Models
    {
        [Table("book_store")]
        public record BookStore
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
            // Many to many
            public List<Book> Books { get; init; } = new List<Book>();
        }
    }
    


    BookStoreContext.cs




    using Microsoft.EntityFrameworkCore;
    namespace BookStoreSample.Models
    {
        public class BookStoreContext: DbContext
        {
            public BookStoreContext(DbContextOptions<BookStoreContext> options): base(options)
            {   
            }
            public DbSet<Author> Authors => Set<Author>();
            public DbSet<BookStore> BookStores => Set<BookStore>();
            public DbSet<Book> Books => Set<Book>();
            public DbSet<BookPrice> BookPrices => Set<BookPrice>();
        }
    }
    


    결과


  • "book"테이블에 "AuthorId"("author"의 외래 키)라는 열이 추가되었습니다.
  • "BookBookStore"라는 테이블이 생성되었습니다. "BookId"와 "BookStoreId"가 있습니다.

  • 마이그레이션을 통해 외래 키를 추가하려는 경우 아래와 같은 작업을 수행할 필요가 없습니다.

    BookStoreContext.cs




    ...
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Book>()
                    .HasOne(b => b.Author!)
                    .WithMany(a => a!.Books)
                    .HasForeignKey(b => b.AuthorId);
            }
    ...
    


    ID만 있는(속성 없음)



    Author.cs




    ...
        [Table("author")]
        public record Author
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
        }
    ...
    


    Book.cs




    ...
        [Table("book")]
        public record Book
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
            [Required]
            [Column("author_id")]
            [ForeignKey("author")]
            // 1 to many
            public int AuthorId { get; init; }
    ...
    


    BookStore.cs




    ...
        [Table("book_store")]
        public record BookStore
        {
            [Key]
            [Column("id")]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public int Id { get; init; }
            [Required]
            [Column("name")]
            public string Name { get; init; } = "";
        }
    ...
    


    "book"테이블과 "book_store"테이블을 연결하는 데이터가 없기 때문에 "stored_book"테이블을 추가합니다.

    StoredBook.cs




    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace BookStoreSample.Models
    {
        [Table("stored_book")]
        public record StoredBook
        {
            [Required]
            [Column("bookstore_id")]
            [ForeignKey("book_store")]
            public int BookStoreId { get; init; }
            [Required]
            [Column("book_id")]
            [ForeignKey("book")]
            public int BookId { get; init; }
        }
    }
    


    결과


  • "author_id"와 같은 열이 생성됩니다. 그러나 그들은 테이블을 참조하지 않습니다.

  • "ForeignKey"속성으로 외래 키를 추가할 수 없습니다.
  • ForeignKeyAttribute Class (System.ComponentModel.DataAnnotations.Schema) | Microsoft Docs

  • 요약


  • 속성을 추가하여 외래 키를 설정할 수 있습니다.
  • 외래 키의 이름을 제어하려면 "AuthorId"와 같은 ID 속성도 추가해야 합니다.
  • 외래 키를 설정하기 위해 "OnModelCreating"메서드에서 아무것도 할 필요가 없습니다.

  • 만들다



    물론 참조 테이블의 ID로 새 레코드를 삽입할 수 있습니다.

    BookService.cs




    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using BookStoreSample.Applications;
    using BookStoreSample.Models;
    using Microsoft.EntityFrameworkCore;
    
    namespace BookStoreSample.Books
    {
        public class BookService: IBookService
        {
            private readonly BookStoreContext context;
            public BookService(BookStoreContext context)
            {
                this.context = context;
            }
            public async Task<UploadResult> CreateBookAsync()
            {
                using var transaction = await context.Database.BeginTransactionAsync();
                try
                {
                    Author author = await context.Authors.FirstAsync(a => a.Id == 2);
                    Book newBook = new Book
                    {
                        Name = "SampleBook",
                        AuthorId = author.Id,
                    };
                    await context.Books.AddAsync(newBook);
                    await context.SaveChangesAsync();
                    var newPrice = new BookPrice
                    {
                        BookId = newBook.Id,
                        Price = 2000m,
                        StartDate = DateTime.Today,
                    };
                    await context.BookPrices.AddAsync(newPrice);
                    await context.SaveChangesAsync();
                    await transaction.CommitAsync();
                    return UploadResultFactory.GetSucceeded();
                }
                catch(Exception ex)
                {
                    await transaction.RollbackAsync();
                    return UploadResultFactory.GetFailed(ex.Message);
                }            
            }
        }
    }
    


    하지만 테이블 인스턴스와 함께 삽입할 수도 있습니다.

    BookService.cs




    ...
            public async Task<UploadResult> CreateBookAsync()
            {
                using var transaction = await context.Database.BeginTransactionAsync();
                try
                {
                    Author author = await context.Authors.FirstAsync(a => a.Id == 2);
                    Book newBook = new Book
                    {
                        Name = "SampleBook",
                        Author = author,
                        BookPrices = new List<BookPrice>
                        {
                            new BookPrice
                            {
                                Price = 3000m,
                                StartDate = DateTime.Today
                            }
                        }
                    };
                    await context.Books.AddAsync(newBook);
                    await context.SaveChangesAsync();
                    await transaction.CommitAsync();
                    return UploadResultFactory.GetSucceeded();
                }
                catch(Exception ex)
                {
                    await transaction.RollbackAsync();
                    logger.LogError(ex.Message);
                    return UploadResultFactory.GetFailed(ex.Message);
                }
    
            }
    ...
    


    ID 변경



    삽입하기 전에 일부 ID를 변경하면 어떻게 됩니까?

    기본 키 변경



    기본 키를 변경할 수 없습니다. 아니면 예외가 발생합니다.

    BookService.cs




    ...
                    Author author = await context.Authors.FirstAsync(a => a.Id == 2);
                    author.Id = 30;
    
                    Book newBook = new Book
                    {
                        Name = "SampleBook",
                        AuthorId = author,
                    };
                    await context.Books.AddAsync(newBook);
                    await context.SaveChangesAsync();
    ...
    


    결과




    The property 'Author.Id' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.
    


    다른 레코드를 참조하는 기존 레코드 추가



    BookService.cs




    ...
                    Author author = await context.Authors.FirstAsync(a => a.Id == 2);
                    BookPrice existedPrice = await context.BookPrices.FirstAsync(p => p.BookId == 2);
    
                    Book newBook = new Book
                    {
                        Name = "SampleBook",
                        Author = author,
                        BookPrices = new List<BookPrice>
                        {
                            existedPrice
                        }
                    };
                    await context.Books.AddAsync(newBook);
                    await context.SaveChangesAsync();
    ...
    


    결과



    새 "BookPrice"레코드가 삽입되지 않았습니다.
    그러나 "existedPrice"의 "BookId"는 "newBook"의 ID로 변경되었습니다.

    읽다



    EF Core에서 기본적으로 자식 테이블은 부모 레코드를 자동으로 포함하지 않습니다.

    BookService.cs




    ...
            public async Task<List<BookStore>> GetBookStoresAsync()
            {
                return await context.BookStores.ToListAsync();
            }
    ...
    


    결과




    [{
        "id":1,
        "name":"Store1",
        "books":[]
    },
    {
        "id":2,
        "name":"Store2",
        "books":[]
    },
    {
        "id":3,
        "name":"Store3","books":[]}]
    


    "Include()"를 사용하여 하위 테이블을 포함할 수 있습니다.

    BookService.cs




    ...
            public async Task<List<BookStore>> GetBookStoresAsync()
            {
                return await context.BookStores.Include(s => s.Books)
                    .ToListAsync();
            }
    ...
    


    결과




    [{
        "id":1,
        "name":"Store1",
        "books":[{
            "bookStoreId":1,
            "bookId":2,
            "book":null
        }]
    }]
    


    손자 포함?



    "ThenInclude()"를 사용하여 손자를 포함할 수 있습니다.

    BookService.cs




    ...
            public async Task<List<BookStore>> GetBookStoresAsync()
            {
                return await context.BookStores.Include(s => s.Books)
                    .ThenInclude(s => s.Book)
                    .ThenInclude(b => b!.Author)
                    .ToListAsync();
            }
    ...
    


    결과




    [{
        "id":1,
        "name":"Store1",
        "books":[{
            "bookStoreId":1,
            "bookId":2,
            "book":{
                "id":2,
                "name":"SampleBook",
                "authorId":2,
                "author":{
                    "id":2,
                    "name":"SampleAuthor",
                    "books":[{
                        "id":3,
                        "name":"SampleBook",
                        "authorId":2,
                        "bookPrices":[],
                        "stores":[{
                            "bookStoreId":1,
                            "bookId":3
                        }]
                    }]
    ...
    


    자원


  • Relationships - EF Core | Microsoft Docs
  • Relationships - EF Core Entity Framework Core | Entity Framework Core Tutorial and Documentation
  • Loading Related Data - EF Core | Microsoft Docs
  • c# - EFCore 2.0 - Include & ThenInclude : child collection -- grandchild collection - Stack Overflow
  • 좋은 웹페이지 즐겨찾기