Chave estrangeira EF Code First sem propriedade de navegação

93

Digamos que eu tenha as seguintes entidades:

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

Qual é a sintaxe de API fluente do código para garantir que ParentId seja criado no banco de dados com uma restrição de chave estrangeira para a tabela Pais, sem a necessidade de ter uma propriedade de navegação ?

Eu sei que se eu adicionar uma propriedade de navegação pai para filho, posso fazer o seguinte:

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

Mas não quero a propriedade de navegação neste caso específico.

RationalGeek
fonte
1
Eu não acho que isso seja realmente possível apenas com EF, você provavelmente precisará usar algum SQL bruto em uma migração manual para configurá-lo
Não gostei de
@LukeMcGregor era isso que eu temia. Se você der essa resposta, ficarei feliz em aceitá-la, presumindo que esteja correta. :-)
RationalGeek
Existe algum motivo específico para não possuir a propriedade de navegação? Tornar a propriedade de navegação privada funcionaria para você - não seria visível fora da entidade, mas agradaria a EF. (Observe que não tentei fazer isso, mas acho que deve funcionar - dê uma olhada neste post sobre mapeamento de propriedades privadas romiller.com/2012/10/01/… )
Pawel
6
Bem, eu não quero porque não preciso disso. Não gosto de colocar coisas desnecessárias extras em um design apenas para satisfazer os requisitos de uma estrutura. Será que me mataria colocar o prop nav? Não. Na verdade, é isso que tenho feito por enquanto.
RationalGeek
Você sempre precisa da propriedade de navegação em pelo menos um lado para construir uma relação. Para mais informações stackoverflow.com/a/7105288/105445
Wahid Bitar

Respostas:

63

Com EF Code First Fluent API isso é impossível. Você sempre precisa de pelo menos uma propriedade de navegação para criar uma restrição de chave estrangeira no banco de dados.

Se você estiver usando Migrações Code First, terá a opção de adicionar uma nova migração baseada em código no console do gerenciador de pacotes ( add-migration SomeNewSchemaName). Se você mudou algo com seu modelo ou mapeamento, uma nova migração será adicionada. Se você não alterou nada force uma nova migração usando add-migration -IgnoreChanges SomeNewSchemaName. A migração conterá apenas métodos Upe vazios Downneste caso.

Depois, você pode modificar o Upmétodo adicionando o seguinte a ele:

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

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

Executar esta migração ( update-databaseno console de gerenciamento de pacote) executará uma instrução SQL semelhante a esta (para SQL Server):

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

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

Como alternativa, sem migrações, você pode simplesmente executar um comando SQL puro usando

context.Database.ExecuteSqlCommand(sql);

onde contexté uma instância de sua classe de contexto derivada e sqlé apenas o comando SQL acima como string.

Esteja ciente de que com tudo isso EF não tem idéia de que ParentIdé uma chave estrangeira que descreve um relacionamento. EF irá considerá-lo apenas como uma propriedade escalar comum. De alguma forma, tudo acima é apenas uma maneira mais complicada e lenta em comparação com apenas abrir uma ferramenta de gerenciamento de SQL e adicionar a restrição manualmente.

Slauma
fonte
2
A simplificação vem da automação: não tenho acesso aos outros ambientes onde meu código está implantado. Ser capaz de realizar essas mudanças no código é bom para mim. Mas eu gosto do snark :)
pomeroy
Acho que você também acabou de definir um atributo na entidade, então no id do pai, basta adicionar [ForeignKey("ParentTableName")]. Isso vinculará a propriedade a qualquer chave que esteja na tabela pai. Agora você tem um nome de tabela embutido no código.
Triynko
2
É claro que não é impossível, veja o outro comentário abaixo Por que essa resposta está marcada como correta
Igor Be
114

Embora este post seja para Entity Frameworknão ser Entity Framework Core, pode ser útil para alguém que deseja alcançar o mesmo usando o Entity Framework Core (estou usando a V1.1.2).

Eu não preciso de propriedades de navegação (embora eles são agradáveis) porque eu estou praticando DDD e eu quero Parente Childser duas raízes agregados separados. Quero que eles possam se comunicar por meio de uma chave estrangeira, não por meio de Entity Frameworkpropriedades de navegação específicas da infraestrutura .

Tudo que você precisa fazer é configurar o relacionamento em um lado usando HasOnee WithManysem especificar as propriedades de navegação (elas não estão lá, afinal).

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

        ......
    }
}

Estou dando exemplos de como configurar propriedades de entidade também, mas o mais importante aqui é HasOne<>, WithMany()e HasForeignKey().

Espero que ajude.

David Liang
fonte
9
Esta é a resposta correta para EF Core, e para aqueles que praticam DDD, esta é uma OBRIGAÇÃO.
Thiago Silva
1
Não estou certo do que mudou para remover a propriedade de navegação. Você pode esclarecer por favor?
andrew.rockwell
3
@ andrew.rockwell: Veja o HasOne<Parent>()e .WithMany()no thie configuração criança. Eles não fazem referência às propriedades de navegação, já que não há propriedades de navegação definidas de qualquer maneira. Vou tentar deixar isso mais claro com minhas atualizações.
David Liang
Impressionante. Obrigado @DavidLiang
andrew.rockwell
2
@ mirind4 não relacionado à amostra de código específica no OP, se você estiver mapeando diferentes entidades raiz agregadas para suas respectivas tabelas de banco de dados, de acordo com DDD, essas entidades raiz devem apenas fazer referência umas às outras por meio de sua identidade e não devem conter uma referência completa para a outra entidade AR. Na verdade, em DDD também é comum tornar a identidade de entidades um objeto de valor em vez de usar adereços com tipos primitivos como int / log / guid (especialmente entidades AR), evitando a obsessão primitiva e também permitindo que ARs diferentes referenciem entidades por meio do valor tipo de ID de objeto. HTH
Thiago Silva
21

Uma pequena dica para quem deseja usar DataAnotations e não deseja expor a propriedade de navegação - use 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; }
}

É isso - a chave estrangeira com cascade:truedepois Add-Migrationserá criada.

tenbits
fonte
1
Pode até ser privado
Marc Wittke
2
Ao criar Child você teria que atribuir Parentou apenas ParentId?
George Mauer
5
As virtualpropriedades @MarcWittke não podem ser private.
LINQ
@GeorgeMauer: essa é uma boa pergunta! Tecnicamente, qualquer um deles funcionaria, mas também se torna um problema quando você tem códigos inconsistentes como este, porque os desenvolvedores (especialmente os novatos) não têm certeza do que transmitir.
David Liang
15

No caso do EF Core, você não precisa necessariamente fornecer uma propriedade de navegação. Você pode simplesmente fornecer uma chave estrangeira em um lado do relacionamento. Um exemplo simples com API Fluent:

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; }
    }
}
Dragão Jonatan
fonte
2

Estou usando .Net Core 3.1, EntityFramework 3.1.3. Tenho pesquisado por aí e a solução que encontrei estava usando a versão genérica HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). você pode criar uma relação um para um assim:

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

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

Espero que isso ajude ou pelo menos forneça algumas outras idéias sobre como usar o HasForeginKeymétodo.

Tecelão Radded
fonte
0

Meu motivo para não usar as propriedades de navegação são as dependências de classe. Separei meus modelos em poucas montagens, que podem ser usadas ou não em diferentes projetos em quaisquer combinações. Portanto, se eu tiver uma entidade que tem propriedade de nagivação para a classe de outro assembly, preciso fazer referência a esse assembly, que desejo evitar (ou qualquer projeto que use parte desse modelo de dados completo carregará tudo com ele).

E eu tenho um aplicativo de migração separado, que é usado para migrações (eu uso automigrações) e criação inicial do banco de dados. Este projeto faz referência a tudo por motivos óbvios.

A solução é estilo C:

  • "copiar" o arquivo com a classe de destino para o projeto de migração via link (arrastar e soltar com a altchave no VS)
  • desabilitar propriedade nagivation (e atributo FK) via #if _MIGRATION
  • defina essa definição de pré-processador no aplicativo de migração e não defina no projeto de modelo, portanto, não fará referência a nada (não faça referência a montagem com Contactclasse no exemplo).

Amostra:

    public int? ContactId { get; set; }

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

Claro que você deve da mesma forma desativar a usingdiretiva e alterar o namespace.

Depois disso, todos os consumidores podem usar essa propriedade como campo de banco de dados normal (e não fazer referência a assemblies adicionais se eles não forem necessários), mas o servidor de banco de dados saberá que é FK e pode usar o cascateamento. Solução muito suja. Mas funciona.

cascavel
fonte