Share via


엔터티 관계 처리

완료된 프로젝트 다운로드

이 섹션에서는 EF가 관련 엔터티를 로드하는 방법과 모델 클래스에서 순환 탐색 속성을 처리하는 방법에 대한 몇 가지 세부 정보를 설명합니다. (이 섹션에서는 배경 지식을 제공하며 자습서를 완료하는 데 필요하지 않습니다. 원하는 경우 5부로 건너뜁니다.)

즉시 로드 및 지연 로드

관계형 데이터베이스에서 EF를 사용하는 경우 EF가 관련 데이터를 로드하는 방법을 이해하는 것이 중요합니다.

EF에서 생성하는 SQL 쿼리를 확인하는 것도 유용합니다. SQL을 추적하려면 생성자에 다음 코드 BookServiceContext 줄을 추가합니다.

public BookServiceContext() : base("name=BookServiceContext")
{
    // New code:
    this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}

GET 요청을 /api/books에 보내면 다음과 같이 JSON이 반환됩니다.

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": null
  },
  ...

책에 유효한 AuthorId가 포함되어 있더라도 Author 속성이 null임을 알 수 있습니다. EF가 관련 Author 엔터티를 로드하지 않기 때문입니다. SQL 쿼리의 추적 로그는 다음을 확인합니다.

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

SELECT 문은 Books 테이블에서 가져오며 Author 테이블을 참조하지 않습니다.

참고로, 다음은 책 목록을 반환하는 클래스의 메서드 BooksController 입니다.

public IQueryable<Book> GetBooks()
{
    return db.Books;
}

JSON 데이터의 일부로 작성자를 반환하는 방법을 살펴보겠습니다. Entity Framework에서 관련 데이터를 로드하는 방법에는 즉시 로드, 지연 로드 및 명시적 로드의 세 가지 방법이 있습니다. 각 기술에는 장부가 있으므로 작동 방식을 이해하는 것이 중요합니다.

즉시 로드

즉시 로드하면 EF는 초기 데이터베이스 쿼리의 일부로 관련 엔터티를 로드합니다. 즉시 로드를 수행하려면 System.Data.Entity.Include 확장 메서드를 사용합니다.

public IQueryable<Book> GetBooks()
{
    return db.Books
        // new code:
        .Include(b => b.Author);
}

그러면 쿼리에 Author 데이터를 포함하도록 EF에 지시합니다. 이 변경을 수행하고 앱을 실행하는 경우 이제 JSON 데이터는 다음과 같습니다.

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": {
      "AuthorId": 1,
      "Name": "Jane Austen"
    }
  },
  ...

추적 로그는 EF가 Book 및 Author 테이블에서 조인을 수행했음을 보여 줍니다.

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent2].[AuthorId] AS [AuthorId1], 
    [Extent2].[Name] AS [Name]
    FROM  [dbo].[Books] AS [Extent1]
    INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[AuthorId]

지연 로드

지연 로드를 사용하면 해당 엔터티의 탐색 속성이 역참조되면 EF에서 관련 엔터티를 자동으로 로드합니다. 지연 로드를 사용하도록 설정하려면 탐색 속성을 가상으로 만듭니다. 예를 들어 Book 클래스에서 다음을 수행합니다.

public class Book
{
    // (Other properties)

    // Virtual navigation property
    public virtual Author Author { get; set; }
}

이제 다음 코드를 고려합니다.

var books = db.Books.ToList();  // Does not load authors
var author = books[0].Author;   // Loads the author for books[0]

지연 로드를 사용하도록 설정하면 의 속성에 books[0] 액세스하면 Author EF가 작성자에 대한 데이터베이스를 쿼리합니다.

지연 로드에는 여러 데이터베이스 여행이 필요합니다. EF는 관련 엔터티를 검색할 때마다 쿼리를 보내기 때문입니다. 일반적으로 직렬화하는 개체에 대해 지연 로드를 사용하지 않도록 설정하려고 합니다. serializer는 관련 엔터티 로드를 트리거하는 모델의 모든 속성을 읽어야 합니다. 예를 들어 EF가 지연 로드를 사용하도록 설정된 책 목록을 직렬화할 때의 SQL 쿼리는 다음과 같습니다. EF가 3명의 작성자를 위해 세 개의 별도 쿼리를 만드는 것을 볼 수 있습니다.

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

지연 로드를 사용하려는 경우가 여전히 있습니다. 즉시 로드하면 EF가 매우 복잡한 조인을 생성할 수 있습니다. 또는 데이터의 작은 하위 집합에 관련 엔터티가 필요할 수 있으며 지연 로드가 더 효율적입니다.

serialization 문제를 방지하는 한 가지 방법은 엔터티 개체 대신 DDO(데이터 전송 개체)를 직렬화하는 것입니다. 이 방법은 문서의 뒷부분에 나와 있습니다.

명시적 로드

명시적 로드는 코드에서 관련 데이터를 명시적으로 가져오는 것을 제외하고 지연 로드와 유사합니다. 탐색 속성에 액세스할 때 자동으로 발생하지 않습니다. 명시적 로드를 사용하면 관련 데이터를 로드하는 시기를 더 자세히 제어할 수 있지만 추가 코드가 필요합니다. 명시적 로드에 대한 자세한 내용은 관련 엔터티 로드를 참조하세요.

Book 및 Author 모델을 정의할 때 Book-Author 관계에 대한 클래스의 Book 탐색 속성을 정의했지만 다른 방향으로 탐색 속성을 정의하지 않았습니다.

해당 탐색 속성을 Author 클래스에 추가하면 어떻게 되나요?

public class Author
{
    public int AuthorId { get; set; }
    [Required]
    public string Name { get; set; }

    public ICollection<Book> Books { get; set; }
}

아쉽게도 모델을 직렬화할 때 문제가 발생합니다. 관련 데이터를 로드하면 원형 개체 그래프가 만들어집니다.

Author 클래스를 로드하고 그 반대로 원형 개체 그래프를 만드는 Book 클래스를 보여 주는 다이어그램

JSON 또는 XML 포맷터가 그래프를 직렬화하려고 하면 예외가 throw됩니다. 두 포맷터는 서로 다른 예외 메시지를 throw합니다. 다음은 JSON 포맷터의 예입니다.

{
  "Message": "An error has occurred.",
  "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type 
      'application/json; charset=utf-8'.",
  "ExceptionType": "System.InvalidOperationException",
  "StackTrace": null,
  "InnerException": {
    "Message": "An error has occurred.",
    "ExceptionMessage": "Self referencing loop detected with type 'BookService.Models.Book'. 
        Path '[0].Author.Books'.",
    "ExceptionType": "Newtonsoft.Json.JsonSerializationException",
    "StackTrace": "..."
     }
}

XML 포맷터는 다음과 같습니다.

<Error>
  <Message>An error has occurred.</Message>
  <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type 
    'application/xml; charset=utf-8'.</ExceptionMessage>
  <ExceptionType>System.InvalidOperationException</ExceptionType>
  <StackTrace />
  <InnerException>
    <Message>An error has occurred.</Message>
    <ExceptionMessage>Object graph for type 'BookService.Models.Author' contains cycles and cannot be 
      serialized if reference tracking is disabled.</ExceptionMessage>
    <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
    <StackTrace> ... </StackTrace>
  </InnerException>
</Error>

한 가지 해결 방법은 다음 섹션에서 설명하는 DTO를 사용하는 것입니다. 또는 그래프 주기를 처리하도록 JSON 및 XML 포맷터를 구성할 수 있습니다. 자세한 내용은 순환 개체 참조 처리를 참조하세요.

이 자습서에서는 탐색 속성이 Author.Book 필요하지 않으므로 그대로 둘 수 있습니다.