简体   繁体   中英

Entity Framework Fluent API relationship mapping HasRequired().WithRequired() not behaving correctly without Map()

Blog Model

using System.Collections.Generic;

namespace DataLayer
{
    public class Blog
    {
        public int BlogKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set; }
        public virtual Post Post { get; set; }
    }
}

Post Model

using System;
using System.Collections.Generic;

namespace DataLayer
{
    public class Post
    {
        public int PostKey { get; set; }
        public string Title { get; set; }
        public DateTime? DateCreated { get; set; }
        public string Content { get; set; }
        public virtual Blog Blog { get; set; }
    }
}

Model Configurations

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;

namespace DataLayer
{
    public class BlogConfiguration : EntityTypeConfiguration<Blog>
    {
        public BlogConfiguration()
        {
            ToTable("Blog", "dbo");
            HasKey(k => k.BlogKey).Property(p=>p.BlogKey).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

// This will allow having null Post for the Blog
            //HasRequired(p => p.Post).WithRequiredPrincipal(p => p.Blog).WillCascadeOnDelete(false);

// This will NOT allow having no Post for the Blog
            HasRequired(p => p.Post).WithRequiredPrincipal(p => p.Blog).Map(m=>m.MapKey("OtherBlogKeyColumn")).WillCascadeOnDelete(false);
        }
    }

    public class PostConfiguration : EntityTypeConfiguration<Post>
    {
        public PostConfiguration()
        {
            ToTable("Post", "dbo");
            HasKey(k => k.PostKey).Property(p=>p.PostKey).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

        }
    }
}

Client

using DataLayer;
using System;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            MyDbContext c = new MyDbContext();

            //Works when dependent's foreign key column is mapped to the primary key column(this is by default when Map() is not provided). 
            //Doesn't work when foreign key column is mapped to some other column(which is provided by Map())
            Blog blog = new Blog { Title = "world", Post = null, BloggerName = "suyash" };


            //Blog required, Post required
            //Blog blog = new Blog { Title = "work", Post = new Post { Title = "new world post" }, BloggerName = "suyash" };

            c.Blogs.Add(blog);

            c.SaveChanges();

        }
    }
}

I have the models Blog and Post . The relationship to discuss here is HasRequired().WithRequired(). I want Blog to be Principal and Post to be Dependent . Please see the Blog Configuration.

HasRequired(p => p.Post).WithRequiredPrincipal(p => p.Blog).WillCascadeOnDelete(false); allows a null Post with Blog blog = new Blog { Title = "world", Post = null, BloggerName = "suyash" };

But, HasRequired(p => p.Post).WithRequiredPrincipal(p => p.Blog).Map(m=>m.MapKey("OtherBlogKeyColumn")).WillCascadeOnDelete(false); doesn't.

The configuration with Map() works as expected, it throws an error when we try to insert a null Post.

Isn't the whole purpose of HasRequired().WithRequired() is to ensure that both the ends have a value even if Map() was not used. Currently without Map() it works just like HasOptional(Blog).WithRequired(Post).

I want to understand is this a genuine error or am i missing something here.

Required ≠ required

The HasRequired - WithRequired combination promises more than it actually does. It allows you to store a sole principle without an accompanying dependent.

In a relational database (at least in the implementations I know of) there is no way to store two rows in different tables "at the same time" (ie as an atomic operation). So there is no way to enforce a mutually required 1:1 association. The principle entity has to be inserted first, then the dependent entity.

But should this keep EF from making the association required? I think it should be possible to validate the requirements before saving changes, as other validations do. In my opinion they could have enforced HasRequired - WithRequired to be really required. But they didn't.

If we have these two simple classes...

public class Principal
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Dependent Dependent { get; set; }
}

public class Dependent
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Principal Principal { get; set; }
}

...mapped as...

modelBuilder.Entity<Principal>()
            .HasRequired(pr => pr.Dependent)
            .WithRequiredPrincipal(dep => dep.Principal);

...we can do...

var principle1 = new Principal { Name = "Principal1" };
context.Principals.Add(principle1);
context.SaveChanges();

...and EF is happy.

We can't insert a Dependent without a Principle . That's because the Dependent 's primary key is copied from the Principle 's and it's a foreign key at the same time. But that's 1:0..1, not 1:1. If we map the association as...

modelBuilder.Entity<Principal>()
            .HasOptional(pr => pr.Dependent)
            .WithRequired(dep => dep.Principal);

...we get the same data model.

Alternatives?

One way to get a really required 1:1 association is by entity splitting . A common example is:

modelBuilder.Entity<Department>() 
    .Map(m => 
    { 
        m.Properties(t => new { t.DepartmentID, t.Name }); 
        m.ToTable("Department"); 
    }) 
    .Map(m => 
    { 
        m.Properties(t => new { t.DepartmentID, t.Administrator, t.StartDate, t.Budget }); 
        m.ToTable("DepartmentDetails"); 
    });

This also creates a data model with a "Dependent" (here DepartmentDetails ) that shares the "Principle"'s PK and refers to it through a FK. Now we can't create a Department without DepartmentDetails because we can only create a Department object, DepartmentDetails isn't a class, and there are always two records in the database.

But of course this is completely different from having two entities in the class model. Both models have completely different usages. For one, when you query a Department , DepartmentDetails will always be joined in. So this is genuine 1:1 (in the data model), but not an alternative to 1:1 as classes.

Another way is mapping a complex type . A very rudimentary example is:

public class Person
{
    public int Id { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public int Number { get; set; }
    public string Town { get; set; }
}

Without any mapping this creates one table containing all fields from Person and Address (EF infers Address as a complex type because it has no key defined). Now we have two classes, and we can't create a Person without an address (and reverse). But now we can't fetch Addresses from the database. They're a bit too dependent. And Address is not an entity, because it hasn't got an identity. It's a value type . So again, this is an alternative, kind of, but an entirely different usage.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM