다음을 통해 공유


일대다 관계

일대다 관계는 단일 엔터티가 여러 다른 엔터티와 연결된 경우에 사용됩니다. 예를 들어 Blog에는 여러 개의 연결된 Posts가 있을 수 있지만 각 Post는 하나의 Blog하고만 연결될 수 있습니다.

이 문서는 많은 예제를 중심으로 구성되어 있습니다. 예제는 개념도 소개하는 일반적인 사례로 시작합니다. 이후 예제에서는 덜 일반적인 구성 종류를 다룹니다. 여기서 좋은 방법은 처음 몇 가지 예제와 개념을 이해하고 특정 요구 사항에 따라 이후 예제로 이동하는 것입니다. 이 방법을 기반으로 간단한 "필수" 및 "선택적" 일대다 관계로 시작합니다.

아래의 모든 예제에 대한 코드는 OneToMany.cs에서 찾을 수 있습니다.

필요한 일대다 항목

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

일대다 관계는 다음으로 구성됩니다.

  • 주 엔터티에 대한 하나 이상의 기본 또는 대체 키 속성. 이는 관계의 "일" 끝 부분입니다. 예: Blog.Id.
  • 종속 엔터티의 하나 이상의 외래 키 속성. 관계의 "다" 끝 부분입니다. 예: Post.BlogId.
  • 필요에 따라 종속 엔터티를 참조하는 주 엔터티에 대한 컬렉션 탐색. 예: Blog.Posts.
  • 필요에 따라 주 엔터티를 참조하는 종속 엔터티에 대한 참조 탐색. 예: Post.Blog.

따라서 이 예제의 관계에 대해 다음을 수행합니다.

  • 외래 키 속성 Post.BlogId는 null을 허용하지 않습니다. 이렇게 하면 외래 키 속성이 어떤 값으로 설정되어야 하므로 모든 종속(Post)은 일부 보안 주체(Blog)와 관련되어야 하기 때문에 관계가 "필수"가 됩니다.
  • 두 엔터티에는 관계의 반대편에 있는 관련 엔터티를 가리키는 탐색이 있습니다.

참고 항목

필수 관계는 모든 종속 엔터티를 일부 보안 주체 엔터티와 연결해야 합니다. 그러나 보안 주체 엔터티는 종속 엔터티 없이 항상 존재할 수 있습니다. 즉, 필수 관계는 항상 최소 하나의 종속 엔터티가 있음을 나타내지 않습니다. EF 모델에는 방법이 없으며 관계형 데이터베이스에 표준 방법도 없으므로 보안 주체가 특정 개수의 종속 항목과 연결되어 있는지 확인할 수 있습니다. 필요한 경우 애플리케이션(비즈니스) 논리에서 구현해야 합니다. 자세한 내용은 필수 탐색을 참조하세요.

두 탐색(종속 항목에서 보안 주체로의 관계와 보안 주체에서 종속 항목으로의 역)과의 관계를 양방향 관계라고 합니다.

이 관계는 규칙에 의해 검색됩니다. 구체적인 요건은 다음과 같습니다.

  • Blog는 관계의 보안 주체로 검색되고 Post는 종속성으로 검색됩니다.
  • Post.BlogId는 보안 주체의 Blog.Id 기본 키를 참조하는 종속 키의 외래 키로 검색됩니다. Post.BlogId는 null을 허용하지 않으므로 관계가 필요에 따라 검색됩니다.
  • Blog.Posts는 컬렉션 탐색으로 검색됩니다.
  • Post.Blog는 참조 탐색으로 검색됩니다.

Important

C# null 허용 참조 형식을 사용하는 경우 외래 키 속성이 null 허용인 경우 참조 탐색은 null을 허용해야 합니다. 외래 키 속성이 null을 허용하지 않는 경우 참조 탐색이 null을 허용하거나 그렇지 않을 수 있습니다. 이 경우 Post.BlogId는 null을 허용하지 않으며 Post.Blog도 null을 허용하지 않습니다. = null!; 구문은 EF가 일반적으로 Blog 인스턴스를 설정하고 완전히 로드된 관계에 대해 null일 수 없으므로 C# 컴파일러에 대한 의도적인 것으로 표시하는 데 사용됩니다. 자세한 내용은 null 허용 참조 형식 작업을 참조하세요.

규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

위의 예제에서 관계의 구성은 보안 주체 엔터티 형식(Blog)에서 HasMany로 시작한 다음 WithOne으로 이를 따릅니다. 모든 관계와 마찬가지로 종속 엔터티 형식(Post)으로 시작하고 HasOneWithMany를 뒤이어 사용하는 것과 정확히 동일합니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

이러한 옵션 중 어느 것도 다른 옵션보다 낫지 않습니다. 둘 다 정확히 동일한 구성을 생성합니다.

관계를 두 번 구성하고, 보안 주체에서 시작한 다음, 종속에서 다시 시작할 필요는 없습니다. 또한 관계의 보안 주체와 종속된 반쪽을 별도로 구성하려고 하면 일반적으로 작동하지 않습니다. 한쪽 끝 또는 다른 쪽에서 각 관계를 구성한 다음 구성 코드를 한 번만 작성하도록 선택합니다.

선택적 일대일

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; } // Optional foreign key property
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

이는 보안 주체에 대한 외래 키 속성 및 탐색이 이제 null을 허용한다는 점을 제외하고 이전 예제와 동일합니다. 보안 주체(Blog)와 관련되지 않고도 종속(Post)이 존재할 수 있으므로 관계를 "선택 사항"으로 만듭니다.

Important

C# null 허용 참조 형식을 사용하는 경우 외래 키 속성이 null 허용인 경우 참조 탐색은 null을 허용해야 합니다. 이 경우 Post.BlogId는 null을 허용하므로 Post.Blog도 null을 허용해야 합니다. 자세한 내용은 null 허용 참조 형식 작업을 참조하세요.

앞에서와 마찬가지로 이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired(false);
}

섀도 외래 키가 있는 필수 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

외래 키는 데이터베이스에서 관계를 나타내는 방법에 대한 세부 정보이므로 순수하게 개체 지향적인 방식으로 관계를 사용할 때는 필요하지 않으므로 모델의 외래 키 속성을 원하지 않을 수 있습니다. 하지만 예를 들어 엔터티가 직렬화될 경우(예: 와이어를 통해 전송) 외래 키 값은 엔터티가 개체 형식에 없을 때 관계 정보를 그대로 유지하는 유용한 방법이 될 수 있습니다. 따라서 이러한 목적을 위해 .NET 형식에 외래 키 속성을 유지하는 것이 실용적입니다. 외래 키 속성은 프라이빗이 될 수 있으며, 이는 해당 값이 엔터티와 함께 이동하도록 허용하면서 외래 키가 노출되지 않도록 하는 좋은 타협입니다.

이전의 두 예제에서 나아가, 다음 예제에서는 종속 엔터티 형식에서 외래 키 속성을 제거합니다. 따라서 EF는 int 형식의 BlogId라는 섀도 외래 키 속성을 만듭니다.

여기서 유의해야 할 중요한 점은 C# null 참조 형식이 사용 중이므로 참조 탐색의 null 허용 여부를 사용하여 외래 키 속성이 null을 허용 가능한지 여부를 확인하므로 관계가 선택 사항인지 필수인지 여부를 결정합니다. null 허용 참조 형식을 사용하지 않는 경우 섀도 외래 키 속성은 기본적으로 관계를 선택적으로 설정하여 null을 허용합니다. 이 경우 IsRequired를 사용하여 섀도 외래 키 속성을 null을 허용하지 않도록 강제하고 관계를 필수로 만듭니다.

앞에서와 마찬가지로 이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired();
}

섀도 외래 키가 있는 선택적 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public Blog? Blog { get; set; } // Optional reference navigation to principal
}

이전 예제와 마찬가지로 외래 키 속성은 종속 엔터티 형식에서 제거되었습니다. 따라서 EF는 int? 형식의 BlogId라는 섀도 외래 키 속성을 만듭니다. 이전 예제와 달리 이번에는 C# null 허용 참조 형식이 사용되고 종속 엔터티 형식에 대한 탐색이 null을 허용하기 때문에 외래 키 속성이 null 허용으로 만들어집니다. 이렇게 하면 관계가 선택 사항으로 만들어집니다.

C# null 허용 참조 형식을 사용하지 않는 경우 외래 키 속성은 기본적으로 null 허용으로 만들어집니다. 즉, 자동으로 생성된 섀도 속성과의 관계는 기본적으로 선택 사항입니다.

앞에서와 마찬가지로 이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("BlogId")
        .IsRequired(false);
}

보안 주체에 대한 탐색이 없는 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

이 예제에서는 외래 키 속성이 다시 도입되었지만 종속에 대한 탐색이 제거되었습니다.

하나의 탐색(보안 주체에 종속되거나 보안 주체에서 종속으로, 둘 다 아님)과의 관계를 단방향 관계라고 합니다.

앞에서와 마찬가지로 이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

WithOne에 대한 호출에는 인수가 없습니다. 이는 Post에서 Blog로의 탐색이 없다는 것을 EF에 알리는 방법입니다.

탐색 없이 엔터티에서 구성을 시작하는 경우 제네릭 HasOne<>() 호출을 사용하여 관계의 다른 쪽 끝에 있는 엔터티의 형식을 명시적으로 지정해야 합니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne<Blog>()
        .WithMany(e => e.Posts)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

보안 주체에 대한 탐색이 없고 섀도 외래 키가 있는 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
}

이 예제에서는 외래 키 속성과 종속 항목의 탐색을 모두 제거하여 이전 예제 중 두 가지를 결합합니다.

이 관계는 선택적 관계로 규칙에 의해 검색됩니다. 코드에 필수임을 나타내는 데 사용할 수 있는 항목이 없으므로 필요한 관계를 만들려면 IsRequired를 사용하는 최소한의 구성이 필요합니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .IsRequired();
}

필요에 따라 적절한 IsRequired() 또는 IsRequired(false)를 호출하여 탐색 및 외래 키 이름을 명시적으로 구성하는 데 보다 완전한 구성을 사용할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne()
        .HasForeignKey("BlogId")
        .IsRequired();
}

보안 주체에 대한 탐색이 없는 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

이전 두 예제에는 보안 주체에서 종속 항목으로의 탐색이 있었지만 종속에서 보안 주체로의 탐색은 없었습니다. 다음 몇 가지 예제에서는 종속성의 탐색이 다시 도입되고 보안 주체에 대한 탐색은 대신 제거됩니다.

앞에서와 마찬가지로 이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(e => e.Blog)
        .WithMany()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

이 방향으로 탐색이 없음을 나타내기 위해 WithMany()이 인수 없이 호출됩니다.

탐색 없이 엔터티에서 구성을 시작하는 경우 제네릭 HasMany<>() 호출을 사용하여 관계의 다른 쪽 끝에 있는 엔터티의 형식을 명시적으로 지정해야 합니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

탐색이 없는 일대다

경우에 따라 탐색 없이 관계를 구성하는 것이 유용할 수 있습니다. 이러한 관계는 외래 키 값을 직접 변경해야만 조작할 수 있습니다.

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
}

두 형식이 관련되어 있음을 나타내는 탐색이 없으므로 이 관계는 규칙에 의해 검색되지 않습니다. OnModelCreating에서 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne();
}

이 구성에서는 Post.BlogId 속성이 규칙에 따라 외래 키로 검색되고 외래 키 속성이 null을 허용하지 않으므로 관계가 "필수"입니다. 외래 키 속성을 null 허용으로 만들어 관계를 "선택 사항"으로 만들 수 있습니다.

이 관계의 보다 완전한 명시적 구성은 다음과 같습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany<Post>()
        .WithOne()
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

대체 키가 있는 일대다

지금까지의 모든 예제에서 종속의 외래 키 속성은 보안 주체의 기본 키 속성으로 제한됩니다. 대신 외래 키를 다른 속성으로 제한한 다음 주 엔터티 형식의 대체 키가 될 수 있습니다. 예시:

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public int AlternateId { get; set; } // Alternate key as target of the Post.BlogId foreign key
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

EF는 항상 규칙에 따라 기본 키에 대한 관계를 만들기 때문에 이 관계는 규칙에 의해 검색되지 않습니다. HasPrincipalKey에 대한 호출을 사용하여 OnModelCreating에서 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId);
}

HasPrincipalKey는 다른 호출과 결합하여 탐색, 외래 키 속성 및 필수/선택적 특성을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

복합 외래 키가 있는 일대다

지금까지의 모든 예제에서 보안 주체의 기본 또는 대체 키 속성은 단일 속성으로 구성되었습니다. 기본 또는 대체 키는 둘 이상의 속성에서 형성될 수도 있습니다. 이를 "복합 키"라고 합니다. 관계의 보안 주체에 복합 키가 있는 경우 종속의 외래 키도 동일한 수의 속성을 가진 복합 키여야 합니다. 예시:

// Principal (parent)
public class Blog
{
    public int Id1 { get; set; } // Composite key part 1
    public int Id2 { get; set; } // Composite key part 2
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId1 { get; set; } // Required foreign key property part 1
    public int BlogId2 { get; set; } // Required foreign key property part 2
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

이 관계는 규칙에 의해 검색됩니다. 그러나 복합 키 자체는 명시적으로 구성해야 합니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasKey(e => new { e.Id1, e.Id2 });
}

Important

복합 외래 키 값은 해당 속성 값이 null인 경우 null인 것으로 간주됩니다. 하나의 속성이 null이고 다른 속성이 null이 아닌 복합 외래 키는 동일한 값을 가진 기본 또는 대체 키에 대한 일치 항목으로 간주되지 않습니다. 둘 다 null로 간주됩니다.

HasForeignKeyHasPrincipalKey 모두 여러 속성이 있는 키를 명시적으로 지정하는 데 사용할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        nestedBuilder =>
        {
            nestedBuilder.HasKey(e => new { e.Id1, e.Id2 });

            nestedBuilder.HasMany(e => e.Posts)
                .WithOne(e => e.Blog)
                .HasPrincipalKey(e => new { e.Id1, e.Id2 })
                .HasForeignKey(e => new { e.BlogId1, e.BlogId2 })
                .IsRequired();
        });
}

위의 코드에서 HasKeyHasMany에 대한 호출은 중첩된 작성기에 함께 그룹화되었습니다. 중첩된 작성기에서는 동일한 엔터티 형식에 대해 Entity<>()를 여러 번 호출할 필요가 없지만 기능적으로는 Entity<>()를 여러 번 호출하는 것과 동일합니다.

계단식 삭제가 없는 필수 일대다

// Principal (parent)
public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>(); // Collection navigation containing dependents
}

// Dependent (child)
public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; } // Required foreign key property
    public Blog Blog { get; set; } = null!; // Required reference navigation to principal
}

규칙에 따라 필수 관계는 계단식 삭제로 구성됩니다. 즉, 보안 주체가 삭제되면 보안 주체 없이는 데이터베이스에 종속된 종속이 존재할 수 없으므로 모든 종속 항목도 삭제됩니다. 더 이상 존재하지 않는 종속 행을 자동으로 삭제하는 대신 예외를 throw하도록 EF를 구성할 수 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .OnDelete(DeleteBehavior.Restrict);
}

일대다 자체 참조

이전의 모든 예제에서 주 엔터티 형식은 종속 엔터티 형식과 달랐습니다. 꼭 그럴 필요는 없습니다. 예를 들어 아래 형식에서 각 Employee은 다른 Employees과 관련됩니다.

public class Employee
{
    public int Id { get; set; }

    public int? ManagerId { get; set; } // Optional foreign key property
    public Employee? Manager { get; set; } // Optional reference navigation to principal
    public ICollection<Employee> Reports { get; } = new List<Employee>(); // Collection navigation containing dependents
}

이 관계는 규칙에 의해 검색됩니다. 규칙에 따라 관계의 탐색, 외래 키 또는 필수/선택적 특성이 검색되지 않는 경우 이러한 항목을 명시적으로 구성할 수 있습니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .HasOne(e => e.Manager)
        .WithMany(e => e.Reports)
        .HasForeignKey(e => e.ManagerId)
        .IsRequired(false);
}