Code EF Première clé étrangère sans propriété de navigation

91

Disons que j'ai les entités suivantes:

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

Quelle est la première syntaxe API fluide du code pour imposer la création de ParentId dans la base de données avec une contrainte de clé étrangère sur la table Parents, sans avoir besoin d'une propriété de navigation ?

Je sais que si j'ajoute une propriété de navigation Parent to Child, je peux le faire:

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

Mais je ne veux pas de la propriété de navigation dans ce cas particulier.

RationalGeek
la source
1
Je ne pense pas que cela soit réellement possible avec seulement EF, vous devez probablement utiliser du SQL brut dans une migration manuelle pour le configurer
Pas aimé le
@LukeMcGregor c'est ce que je craignais. Si vous en faites une réponse, je serai heureux de l'accepter, en supposant qu'elle soit correcte. :-)
RationalGeek
Y a-t-il une raison spécifique pour ne pas avoir la propriété de navigation? Rendre la propriété de navigation privée fonctionnerait pour vous - ne serait pas visible en dehors de l'entité mais plairait à EF. (Notez que je n'ai pas essayé cela mais je pense que cela devrait fonctionner - jetez un œil à cet article sur la cartographie des propriétés privées romiller.com/2012/10/01/… )
Pawel
6
Eh bien, je n'en veux pas parce que je n'en ai pas besoin. Je n'aime pas avoir à mettre des éléments inutiles dans un design juste pour satisfaire les exigences d'un framework. Est-ce que ça me tuerait de mettre dans le support de navigation? Non. En fait, c'est ce que j'ai fait pour le moment.
RationalGeek
Vous avez toujours besoin d'une propriété de navigation d'au moins un côté pour créer une relation. Pour plus d'informations, stackoverflow.com/a/7105288/105445
Wahid Bitar

Réponses:

63

Avec EF Code First Fluent API, c'est impossible. Vous avez toujours besoin d'au moins une propriété de navigation pour créer une contrainte de clé étrangère dans la base de données.

Si vous utilisez Code First Migrations, vous avez la possibilité d'ajouter une nouvelle migration basée sur le code sur la console du gestionnaire de packages ( add-migration SomeNewSchemaName). Si vous avez modifié quelque chose avec votre modèle ou votre mappage, une nouvelle migration sera ajoutée. Si vous n'avez rien changé, forcez une nouvelle migration en utilisant add-migration -IgnoreChanges SomeNewSchemaName. La migration ne contiendra que des méthodes Upet des vides Downdans ce cas.

Ensuite, vous pouvez modifier la Upméthode en y ajoutant le suivant:

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

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

L'exécution de cette migration ( update-databasesur la console de gestion des packages) exécutera une instruction SQL similaire à celle-ci (pour SQL Server):

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

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

Sinon, sans migrations, vous pouvez simplement exécuter une commande SQL pure en utilisant

context.Database.ExecuteSqlCommand(sql);

contextest une instance de votre classe de contexte dérivée et sqlest simplement la commande SQL ci-dessus sous forme de chaîne.

Sachez qu'avec tout cela EF n'a aucun indice qui ParentIdest une clé étrangère qui décrit une relation. EF ne le considérera que comme une propriété scalaire ordinaire. D'une manière ou d'une autre, tout ce qui précède n'est qu'un moyen plus compliqué et plus lent que d'ouvrir simplement un outil de gestion SQL et d'ajouter la contrainte à la main.

Slauma
la source
2
La simplification vient de l'automatisation: je n'ai pas accès aux autres environnements sur lesquels mon code est déployé. Pouvoir effectuer ces changements de code est bien pour moi. Mais j'aime le snark :)
pomeroy
Je pense que vous venez également de définir un attribut sur l'entité, donc sur l'id parent, ajoutez simplement [ForeignKey("ParentTableName")]. Cela liera la propriété à la clé de la table parent. Maintenant, vous avez un nom de table codé en dur.
Triynko
2
Ce n'est clairement pas impossible, voir l'autre commentaire ci-dessous Pourquoi est-ce même marqué comme bonne réponse
Igor Be
111

Bien que cet article soit pour Entity Frameworknon Entity Framework Core, il pourrait être utile pour quelqu'un qui veut réaliser la même chose en utilisant Entity Framework Core (j'utilise V1.1.2).

Je ne ai pas besoin des propriétés de navigation (bien qu'ils soient bien) parce que je pratique DDD et je veux Parentet Childd'être deux racines globales distinctes. Je veux qu'ils puissent se parler via une clé étrangère et non via des Entity Frameworkpropriétés de navigation spécifiques à l'infrastructure .

Tout ce que vous avez à faire est de configurer la relation d'un côté en utilisant HasOneet WithManysans spécifier les propriétés de navigation (elles ne sont pas là après tout).

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

        ......
    }
}

Je donne également des exemples sur la façon de configurer les propriétés des entités, mais le plus important ici est HasOne<>, WithMany()et HasForeignKey().

J'espère que cela aide.

David Liang
la source
8
C'est la bonne réponse pour EF Core, et pour ceux qui pratiquent le DDD, c'est un MUST.
Thiago Silva
1
Je ne sais pas exactement ce qui a changé pour supprimer la propriété de navigation. Pouvez-vous clarifier s'il vous plait?
andrew.rockwell
3
@ andrew.rockwell: Voir la configuration enfant HasOne<Parent>()and .WithMany()on thie. Ils ne font pas du tout référence aux propriétés de navigation, car aucune propriété de navigation n'est définie de toute façon. Je vais essayer de le rendre plus clair avec mes mises à jour.
David Liang
Impressionnant. Merci @DavidLiang
andrew.rockwell
2
@ mirind4 sans rapport avec l'exemple de code spécifique dans l'OP, si vous mappez différentes entités racine agrégées à leurs tables DB respectives, selon DDD, ces entités racines ne doivent se référencer que via leur identité et ne doivent pas contenir de référence complète à l'autre entité AR. En fait, dans DDD, il est également courant de faire de l'identité des entités un objet de valeur au lieu d'utiliser des accessoires avec des types primitifs comme int / log / guid (en particulier les entités AR), évitant l'obsession primitive et permettant également à différents AR de référencer des entités via la valeur type d'ID d'objet. HTH
Thiago Silva
21

Petit conseil pour ceux qui veulent utiliser DataAnotations et ne veulent pas exposer la propriété de navigation - utilisez 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; }
}

C'est tout - la clé étrangère avec cascade:trueaprès Add-Migrationsera créée.

dix bits
la source
1
Il peut même en privé
Marc Wittke
2
Lors de la création de l'enfant, auriez-vous à attribuer Parentou simplement ParentId?
George Mauer
5
Les virtualpropriétés @MarcWittke ne peuvent pas être private.
LINQ
@GeorgeMauer: c'est une bonne question! Techniquement, l'un ou l'autre fonctionnerait, mais cela pose également des problèmes lorsque vous avez des codes incohérents comme celui-ci, car les développeurs (en particulier les nouveaux arrivants) ne savent pas quoi transmettre.
David Liang
14

Dans le cas d' EF Core, vous n'avez pas nécessairement besoin de fournir une propriété de navigation. Vous pouvez simplement fournir une clé étrangère d'un côté de la relation. Un exemple simple avec l'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; }
    }
}
Dragon Jonatan
la source
2

J'utilise .Net Core 3.1, EntityFramework 3.1.3. J'ai cherché et la solution que j'ai trouvée utilisait la version générique de HasForeginKey<DependantEntityType>(e => e.ForeginKeyProperty). vous pouvez créer une relation un à un comme ceci:

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

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

J'espère que cela aide ou du moins fournit d'autres idées sur la façon d'utiliser la HasForeginKeyméthode.

Tisserand radded
la source
0

Ma raison de ne pas utiliser les propriétés de navigation est les dépendances de classe. J'ai séparé mes modèles en quelques assemblages, qui peuvent être utilisés ou non dans différents projets dans toutes les combinaisons. Donc, si j'ai une entité qui a une propriété de nagivation à la classe d'un autre assemblage, je dois faire référence à cet assemblage, ce que je veux éviter (ou tout projet qui utilise une partie de ce modèle de données complet transportera tout avec lui).

Et j'ai une application de migration distincte, qui est utilisée pour les migrations (j'utilise des automatisations) et la création de base de données initiale. Ce projet fait référence à tout par des raisons évidentes.

La solution est de style C:

  • "copier" le fichier avec la classe cible vers le projet de migration via un lien (glisser-déposer avec altclé dans VS)
  • désactiver la propriété de nagivation (et l'attribut FK) via #if _MIGRATION
  • définir cette définition de préprocesseur dans l'application de migration et ne pas définir dans le projet de modèle, de sorte qu'il ne référencera rien (ne référencez pas l'assembly avec la Contactclasse dans l'exemple).

Échantillon:

    public int? ContactId { get; set; }

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

Bien sûr, vous devez de la même manière désactiver la usingdirective et modifier l'espace de noms.

Après cela, tous les consommateurs peuvent utiliser cette propriété comme un champ DB habituel (et ne pas référencer des assemblys supplémentaires s'ils ne sont pas nécessaires), mais le serveur DB saura qu'il s'agit de FK et peut utiliser la mise en cascade. Solution très sale. Mais ça marche.

crotale
la source