Поделиться через


Отношения "один ко многим"

Связи "один ко многим" используются, когда одна сущность связана с любым количеством других сущностей. Например, 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
}

Связь "один ко многим" состоит из:

Таким образом, для связи в этом примере:

  • Свойство Post.BlogId внешнего ключа не допускает значение NULL. Это делает связь "обязательной", так как каждое зависимое (Post) должно быть связано с некоторым субъектом (Blog), так как его свойство внешнего ключа должно иметь некоторое значение.
  • Оба сущности имеют навигации, указывающие на связанную сущность или сущности в другой стороне связи.

Примечание.

Необходимая связь гарантирует, что каждая зависимые сущности должна быть связана с какой-либо основной сущностью. Однако сущность субъекта всегда может существовать без зависимых сущностей. То есть требуемая связь не указывает на то, что всегда будет по крайней мере одна зависимые сущности. В модели EF нет никакого способа, а также нет стандартного способа в реляционной базе данных, чтобы убедиться, что субъект связан с определенным числом зависимых. Если это необходимо, она должна быть реализована в логике приложения (бизнес). Дополнительные сведения см. в разделе "Обязательные навигации ".

Совет

Связь с двумя навигациями, одной из зависимой от субъекта и обратной от субъекта к зависимым, называется двунаправленной связью.

Эта связь обнаруживается по соглашению. Это означает следующее.

  • Blog обнаруживается как субъект в связи и Post обнаруживается как зависимый.
  • Post.BlogId обнаруживается как внешний ключ зависимого, ссылающегося Blog.Id на первичный ключ субъекта. Связь обнаруживается по мере необходимости, так как Post.BlogId не допускает значение NULL.
  • Blog.Posts обнаруживается как навигация по коллекции.
  • Post.Blog обнаруживается как эталонная навигация.

Внимание

При использовании ссылочных типов, допускающих значение NULL, навигация по ссылке должна иметь значение NULL, если свойство внешнего ключа может иметь значение NULL. Если свойство внешнего ключа не допускает значение NULL, навигация по ссылке может иметь значение NULL или нет. В этом случае Post.BlogId не допускается значение NULL, а Post.Blog также не допускает значение NULL. Конструкция = null!; используется для того, чтобы пометить это как преднамеренное для компилятора C#, так как EF обычно задает 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();
}

В приведенном выше примере конфигурация связей начинается с HasMany типа основной сущности (Blog), а затем следует за этим.WithOne Как и во всех отношениях, он точно эквивалентен началу с типа зависимой сущности (Post) и использовать HasOne за ним.WithMany Например:

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. Это делает связь "необязательной", так как зависимый (Post) может существовать без связи с любым субъектом (Blog).

Внимание

При использовании ссылочных типов, допускающих значение 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 создает теневое свойство внешнего ключа, называемое BlogId типом int.

Важно отметить, что используются ссылочные типы , допускающие значение NULL C#, поэтому для определения того, является ли свойство внешнего ключа пустым значением, используется значение NULL, поэтому связь является необязательной или обязательной. Если ссылочные типы, допускающие значение NULL, не используются, свойство теневого внешнего ключа будет иметь значение NULL по умолчанию, что делает связь необязательной по умолчанию. В этом случае используйте для IsRequired принудительного принудительного применения теневое свойство внешнего ключа к ненулевому значению и обязательной связи.

Как и раньше, эта связь обнаруживается по соглашению. В случаях, когда навигации, внешний ключ или обязательный или необязательный характер связи не обнаруживаются по соглашению, эти вещи можно настроить явно. Например:

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 создает теневое свойство внешнего ключа, называемое BlogId типом int?. В отличие от предыдущего примера, на этот раз свойство внешнего ключа создается в качестве значения NULL, так как используются ссылочные типы , допускающие значение NULL, и навигация по типу зависимой сущности допускает значение NULL. Это делает связь необязательной.

Если ссылочные типы ссылок на C# не используются, свойство внешнего ключа также будет создано как допускаемое значение 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 не имеет аргументов. Это способ сказать EF, что навигация не выполняется.Post Blog

Если конфигурация начинается с сущности без навигации, то тип сущности в другом конце связи должен быть явно указан с помощью универсального 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. Отношение можно сделать необязательным, сделав свойство внешнего ключа пустым.

Более полная явная конфигурация этой связи:

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 всегда будет создавать связь с первичным ключом. Его можно настроить явно с OnModelCreating помощью вызова HasPrincipalKey. Например:

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 });
}

Внимание

Составное значение внешнего ключа считается null равным, если какие-либо из его значений свойств имеют значение NULL. Составной внешний ключ с одним свойством NULL и другим, отличным от NULL, не будет считаться совпадением для первичного или альтернативного ключа с теми же значениями. Оба будут рассматриваться null.

HasPrincipalKey Оба HasForeignKey и могут использоваться для явного указания ключей с несколькими свойствами. Например:

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();
        });
}

Совет

В приведенном выше коде вызовы HasKey и HasMany были сгруппированы в вложенный построитель. Вложенные построители удаляют необходимость 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
}

По соглашению необходимые связи настроены для каскадного удаления. Это означает, что при удалении субъекта все его зависимые также удаляются, так как зависимые не могут существовать в базе данных без субъекта. Можно настроить 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);
}