1

Problème: J'ai des entités maître et de détail. Lorsque j'initialise un maître (myMaster), il crée une instance de Details (myMaster.Detail) et les deux semblent persister dans la base de données lors de l'ajout de myMaster. Cependant, lorsque je recharge le contexte et accède à myMasterReloaded.detail ses propriétés ne sont pas initialisées. Cependant, si je tire directement le détail du contexte, cela semble magiquement initialiser myMasterReloaded.detail. J'ai distillé ceci avec un exemple de test unitaire minimal ci-dessous. Est-ce une "caractéristique" ou manque-t-il des détails conceptuels importants?Problème d'enregistrement avec Entity Framework (besoin d'aide conceptuelle)

//DECLARE CLASSES 
public class Master 
{ 
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)] 
    public Guid MasterId { get; set; } 
    public Detail Detail { get; set; } 
    public Master() { Detail = new Detail(); } 
} 

public class Detail 
{ 
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)] 
    public Guid DetailId { get; set; } 
    public Master MyMaster{ get; set; } 
} 

public class MyDbContext : DbContext 
{ 
    public DbSet<Master> Masters { get; set; } 
    public DbSet<Detail> Details { get; set; } 
    protected override void OnModelCreating(ModelBuilder modelBuilder) 
    { 
     modelBuilder.Entity<Master>() 
      .HasOptional(x => x.Detail) 
      .WithOptionalPrincipal(x => x.MyMaster) 
      .WillCascadeOnDelete(true); 
    } 
} 

//PERFORM UNIT TEST 
[TestMethod] 
public void UnitTestMethod() 
{ 
    //Start with fresh DB 
    var context = new MyDbContext(); 
    context.Database.Delete(); 
    context.Database.CreateIfNotExists(); 

    //Create and save entities 
    var master = context.Masters.Create();    
    context.Masters.Add(master); 
    context.SaveChanges(); 

    //Reload entity 
    var contextReloaded = new MyDbContext(); 
    var masterReloaded = contextReloaded.Masters.First(); 

    //This should NOT Pass but it does.. 
    Assert.AreNotEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId); 

    //Let's say 'hi' to the instance of details in the db without using it. 
    contextReloaded.Details.First(); 

    //By simply referencing the instance above, THIS now passes, contracting the first Assert....WTF?? 
    Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId); 
} 

(C'est le point d'achoppement pour un ensemble d'entités plus sophistiqué. Je l'ai simplement distillée, jusqu'à son cas le plus simple, je ne peux pas remplacer simplement les détails d'un type complexe).

Cheers, Rob

Répondre

0

Matt Hamilton avait raison (voir ci-dessus). Le problème était:

  1. La propriété Détail devrait pas instancier dans le constructeur, ni par les getters/setters via un élément de support. S'il est pratique d'instancier une entité contenant des propriétés dans votre nouvelle instance, il peut être utile d'avoir une méthode initialize distincte qui ne sera pas exécutée automatiquement par Entity Framework car elle reconstruit les objets à partir de la base de données.
  2. La propriété Detail doit être déclarée virtual dans la classe Master pour que cela fonctionne correctement.

Ce qui suit PASS (Comme prévu/espoir)

public class Master 
{ 
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)] 
    public Guid MasterId { get; set; } 

    //Key new step: Detail MUST be declared VIRTUAL 
    public virtual Detail Detail { get; set; } 
} 

public class Detail 
{ 
    [Key, DatabaseGenerated(DatabaseGenerationOption.Identity)] 
    public Guid DetailId { get; set; } 
    //Set this to be VIRTUAL as well 
    public virtual Master MyMaster { get; set; } 
} 

public class MyDbContext : DbContext 
{ 
    public DbSet<Master> Masters { get; set; } 
    public DbSet<Detail> Details { get; set; } 
    protected override void OnModelCreating(ModelBuilder modelBuilder) 
    { 
     //This sets up a BI-DIRECTIONAL relationship 
     modelBuilder.Entity<Master>() 
      .HasOptional(x => x.Detail) 
      .WithOptionalPrincipal(x => x.MyMaster) 
      .WillCascadeOnDelete(true); 
    } 
} 


[TestMethod] 
public void UnitTestMethod() 
{ 

    var context = new MyDbContext(); 
    context.Database.Delete(); 
    context.Database.CreateIfNotExists(); 

    //Create and save entities 
    var master = context.Masters.Create(); 

    //Key new step: Detail must be instantiated and set OUTSIDE of the constructor 
    master.Detail = new Detail(); 
    context.Masters.Add(master); 
    context.SaveChanges(); 

    //Reload entity 
    var contextReloaded = new MyDbContext(); 
    var masterReloaded = contextReloaded.Masters.First(); 

    //This NOW Passes, as it should 
    Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId); 

    //This line is NO LONGER necessary 
    contextReloaded.Details.First(); 

    //This shows that there is no change from accessing the Detail from the context 
    Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId); 
    } 

Enfin, il est également pas nécessaire d'avoir une relation bidirectionnelle. La référence à « MyMaster » peut être retiré en toute sécurité à partir de la classe de détail et l'application suivante peut être utilisée à la place:

modelBuilder.Entity<Master>() 
      .HasOptional(x => x.Detail) 
      .WithMany() 
      .IsIndependent(); 

Avec ce qui précède, l'exécution context.Details.Remove (master.Detail), a donné lieu à maître. Détail == null étant vrai (comme vous pouvez l'espérer/espérer).

Je pense qu'une partie de la confusion a émergé du mappage X-vers-plusieurs où vous pouvez initialiser une liste virtuelle d'entités dans le constructeur (par exemple, en appelant myDetails = new List();), parce que vous n'instanciez pas les entités elles-mêmes.

Soit dit en passant, au cas où quelqu'un éprouve quelques difficultés avec un à plusieurs carte unidirectionnelle de maître à une LISTE des détails, ce qui suit a fonctionné pour moi:

modelBuilder.Entity<Master>() 
      .HasMany(x => x.Details) 
      .WithMany() 
      .Map((x) => 
         { 
          x.MapLeftKey(m => m.MasterId, "MasterId"); 
          x.MapRightKey(d => d.DetailId, "DetailId"); 
         }); 

Cheers, Rob

2

Je pense c'est parce que lorsque vous rechargerez le Maître, vous n'avez pas envie, chargé le Détail, de sorte que le Détail entité ne sera pas dans l'entité Cadre "graphique" (mémoire interne). La seule chose dans le graphique sera une seule entité Maître.

Mais lorsque vous requête la Détail (« Disons salut »), il est chargé dans le graphique et la référence a été réglée en fonction de l'association FK donc votre passe test final que le Maître est maintenant lié à la Détail.

Je peux me tromper, mais c'est ce que ça donne.

Avez-vous chargement paresseux activé? Sinon, vous devez charger avec empressement les relations dont vous avez besoin.

Au lieu de cela:

var masterReloaded = contextReloaded.Masters.First(); 

Essayez ceci:

var masterReloaded = contextReloaded.Masters.Include(x => x.Detail).First(); 
+0

Cela et le fait que tout nouveau maître créera automatiquement un nouveau détail (dans son ctor), chargeant ainsi un maître de la base de données va essayer de créer une nouvelle instance de détail. –

+0

À moins que EF4 ne contourne en quelque sorte le ctor par défaut lors de l'instanciation d'entités. Tout est possible je suppose. –

+0

Yup. Ne devrait pas être la création d'entités dans le ctor. Laissez EF faire ce pour quoi il a été conçu. – RPM1984

Questions connexes