Код EF Перший зовнішній ключ без властивості навігації


91

Скажімо, у мене є такі сутності:

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }
}

Який код першого вільного синтаксису API для забезпечення того, що ParentId створюється в базі даних із обмеженням зовнішнього ключа в таблиці батьків, без необхідності мати властивість навігації ?

Я знаю, що якщо я додаю властивість навігації Parent до Child, то я можу зробити це:

modelBuilder.Entity<Child>()
    .HasRequired<Parent>(c => c.Parent)
    .WithMany()
    .HasForeignKey(c => c.ParentId);

Але я не хочу властивість навігації в цьому конкретному випадку.


1
Я не думаю, що це насправді можливо лише за допомогою EF, вам, мабуть, потрібно використовувати якийсь необроблений SQL в ручному перенесенні для його налаштування
Не любив

@LukeMcGregor, цього я боявся. Якщо ви дасте таку відповідь, я буду радий прийняти її, вважаючи її правильною. :-)
RationalGeek

Чи існує якась конкретна причина відсутності властивості навігації? Якщо б зробити навігаційну власність приватною для вас - не було б видимо за межами сутності, але сподобалося б EF. (Примітка, я не пробував цього, але, думаю, це повинно спрацювати - погляньте на цю публікацію про картографування приватних об’єктів romiller.com/2012/10/01/… )
Павел

6
Ну, я цього не хочу, бо мені це не потрібно. Мені не подобається вкладати зайві непотрібні речі в дизайн, лише щоб задовольнити вимоги фреймворку. Чи не вбило б мене введення навігатора? Ні. Насправді це те, що я робив на даний момент.
RationalGeek

Вам завжди потрібна властивість навігації хоча б на одній стороні, щоб побудувати відношення. Для отримання додаткової інформації stackoverflow.com/a/7105288/105445
Вахід Бітар

Відповіді:


63

З EF Code First Fluent API це неможливо. Завжди потрібна хоча б одна властивість навігації для створення обмеження зовнішнього ключа в базі даних.

Якщо ви використовуєте перші міграції коду, у вас є можливість додати нову міграцію на основі коду на консолі менеджера пакетів ( add-migration SomeNewSchemaName). Якщо ви щось змінили зі своєю моделлю або відображенням, буде додано нову міграцію. Якщо ви нічого не змінили, змусіть нову міграцію за допомогою add-migration -IgnoreChanges SomeNewSchemaName. У цьому випадку міграція міститиме лише пусті Upта Downметоди.

Потім ви можете змінити Upметод, додавши до нього наступне:

public override void Up()
{
    // other stuff...

    AddForeignKey("ChildTableName", "ParentId", "ParentTableName", "Id",
        cascadeDelete: true); // or false
    CreateIndex("ChildTableName", "ParentId"); // if you want an index
}

Запуск цієї міграції ( update-databaseна консолі управління пакетами) запустить оператор SQL, подібний до цього (для SQL Server):

ALTER TABLE [ChildTableName] ADD CONSTRAINT [FK_SomeName]
FOREIGN KEY ([ParentId]) REFERENCES [ParentTableName] ([Id])

CREATE INDEX [IX_SomeName] ON [ChildTableName] ([ParentId])

Як варіант, без міграцій ви можете просто запустити чисту команду SQL за допомогою

context.Database.ExecuteSqlCommand(sql);

де contextє екземпляром вашого похідного класу контексту і sqlє лише наведеною вище командою SQL як рядок.

Майте на увазі, що при всьому цьому EF не має поняття, що ParentIdє зовнішнім ключем, що описує стосунки. EF розглядатиме це лише як звичайну скалярну властивість. Якось все вищезазначене є лише більш складним та повільним способом порівняно із простим відкриттям інструменту управління SQL та додаванням обмежень вручну.


2
Спрощення походить від автоматизації: я не маю доступу до інших середовищ, до яких розгортається мій код. Можливість виконувати ці зміни в коді для мене приємна. Але мені подобається
снарк

Я думаю, ви також просто встановили атрибут для сутності, тому для батьківського ідентифікатора просто додайте [ForeignKey("ParentTableName")]. Це прив’яже властивість до будь-якого ключа в батьківській таблиці. Тепер у вас є твердо закодована назва таблиці.
Трійко

2
Очевидно, що це неможливо, дивіться інший коментар нижче. Чому це навіть позначено як правильну відповідь
Ігор Будь

112

Незважаючи на те, що ця публікація призначена для Entity Frameworkне Entity Framework Core, вона може бути корисною для тих, хто хоче досягти того самого, використовуючи Entity Framework Core (я використовую V1.1.2).

Мені не потрібні навігаційні властивості (хоча вони добре) , тому що я практикую DDD , і я хочу Parentі Childбути два окремих агрегатні коріння. Я хочу, щоб вони могли спілкуватися між собою за допомогою зовнішнього ключа, а не через Entity Frameworkвластивості навігації, що стосуються інфраструктури .

Все, що вам потрібно зробити, це налаштувати відносини на одній стороні, використовуючи HasOneта WithManyбез вказівки властивостей навігації (вони зрештою відсутні).

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    protected override void OnModelCreating(ModelBuilder builder)
    {
        ......

        builder.Entity<Parent>(b => {
            b.HasKey(p => p.Id);
            b.ToTable("Parent");
        });

        builder.Entity<Child>(b => {
            b.HasKey(c => c.Id);
            b.Property(c => c.ParentId).IsRequired();

            // Without referencing navigation properties (they're not there anyway)
            b.HasOne<Parent>()    // <---
                .WithMany()       // <---
                .HasForeignKey(c => c.ParentId);

            // Just for comparison, with navigation properties defined,
            // (let's say you call it Parent in the Child class and Children
            // collection in Parent class), you might have to configure them 
            // like:
            // b.HasOne(c => c.Parent)
            //     .WithMany(p => p.Children)
            //     .HasForeignKey(c => c.ParentId);

            b.ToTable("Child");
        });

        ......
    }
}

Я наводжу приклади, як також налаштувати властивості сутності, але найважливішим тут є HasOne<>, WithMany()і HasForeignKey().

Сподіваюся, це допоможе.


9
Це правильна відповідь для EF Core, а для тих, хто практикує DDD, це ОБОВ’ЯЗКОВО.
Тьяго Сільва

1
Мені незрозуміло, що змінилося для видалення навігаційного властивості. Ви можете пояснити?
andrew.rockwell

3
@ andrew.rockwell: Дивіться конфігурацію дитини HasOne<Parent>()та .WithMany()про неї. Вони взагалі не посилаються на властивості навігації, оскільки ніяких властивостей навігації все одно не визначено. Я постараюся зробити це зрозумілішим завдяки своїм оновленням.
Девід Лян

Приголомшливо Дякую @DavidLiang
andrew.rockwell

2
@ mirind4, не пов'язаний із конкретним зразком коду в OP, якщо ви зіставляєте різні сукупні кореневі сутності до відповідних таблиць DB, згідно з DDD, ці кореневі entites повинні посилатися один на одного через свою ідентичність і не повинні містити повне посилання до іншого суб’єкта АР. Насправді в DDD також загальноприйнято робити ідентичність сутностей об'єктом значення, замість того, щоб використовувати реквізити з примітивними типами, такими як int / log / GUID (особливо сутності AR), уникаючи примітивної одержимості, а також дозволяючи різним AR посилатись на сутності через значення тип ідентифікатора об’єкта. HTH
Тіаго Сільва

21

Невелика підказка для тих, хто хоче використовувати DataAnotations і не хоче виставляти властивість навігації - використовувати protected

public class Parent
{
    public int Id { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }

    protected virtual Parent Parent { get; set; }
}

Ось і все - буде створений зовнішній ключ з cascade:trueпісля Add-Migration.


1
Це може навіть приватно
Марк Вітке

2
Створюючи Child, вам довелося б призначити Parentчи просто ParentId?
Джордж Мауер

5
virtualВластивості @MarcWittke бути не можуть private.
LINQ

@GeorgeMauer: це гарне запитання! Технічно будь-який з них міг би працювати, але це також стає проблемою, коли у вас є суперечливі коди, подібні цьому, тому що розробники (особливо нові, хто приходить) не впевнені, що їм
Девід Ліанг,

14

У випадку з EF Core вам не обов'язково потрібно надавати властивість навігації. Ви можете просто надати зовнішній ключ на одній стороні відносин. Простий приклад з Fluent API:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;

namespace EFModeling.Configuring.FluentAPI.Samples.Relationships.NoNavigation
{
    class MyContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

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

    public class Blog
    {
         public int BlogId { get; set; }
         public string Url { get; set; }
    }

    public class Post
    {
         public int PostId { get; set; }
         public string Title { get; set; }
         public string Content { get; set; }

        public int BlogId { get; set; }
    }
}

2

Я використовую .Net Core 3.1, EntityFramework 3.1.3. Я шукав навколо, і Рішення, яке я придумав, використовувало загальну версію HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). Ви можете створити відношення один до одного приблизно так:

builder.entity<Parent>()
.HasOne<Child>()
.WithOne<>()
.HasForeginKey<Child>(c => c.ParentId);

builder.entity<Child>()
    .Property(c => c.ParentId).IsRequired();

Сподіваюся, це допомагає або принаймні дає деякі інші ідеї щодо використання HasForeginKeyметоду.


0

Моя причина НЕ з допомогою навігаційних властивостей є залежностями класу. Я розділив свої моделі на декілька збірок, які можна використовувати або не використовувати в різних проектах у будь-яких комбінаціях. Отже, якщо у мене є сутність, яка має властивість нагівації до класу з іншої збірки, мені потрібно зробити посилання на цю збірку, якої я хочу уникати (або будь-який проект, який використовує частину цієї повної моделі даних, буде нести все з собою).

І у мене є окрема програма міграції, яка використовується для міграцій (я використовую автоматичну міграцію) та початкового створення БД. Цей проект посилається на все з очевидних причин.

Рішення у стилі С:

  • "скопіювати" файл із цільовим класом до проекту міграції за посиланням (перетягування та падіння за допомогою altключа в VS)
  • вимкнути властивість нагівації (і атрибут FK) через #if _MIGRATION
  • встановіть це визначення препроцесора в програмі міграції, а не встановлюйте в проекті моделі, тому він не буде посилатися ні на що (не посилайтеся на збірку з Contactкласом у прикладі).

Зразок:

    public int? ContactId { get; set; }

#if _MIGRATION
    [ForeignKey(nameof(ContactId))]
    public Contact Contact { get; set; }
#endif

Звичайно, слід таким же чином відключити usingдирективу та змінити простір імен.

Після цього всі споживачі можуть використовувати це властивість як звичайне поле БД (і не посилатися на додаткові збірки, якщо вони не потрібні), але сервер БД буде знати, що він є FK, і може використовувати каскад. Дуже брудне рішення. Але працює.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.