EF Core反向導航屬性解決多對一關係

多對一是一種很常見的關係,例如:一個班級有一個學生集合屬性,同時,班級有班長、語文課表明、數學課表明等單個學生屬性,若是定義2個實體類,班級SchoolClass和學生Student,那麼,班級SchoolClass類有多個學生Student類的導航屬性,學生Student類有一個班級SchoolClass類的導航屬性。此時就須要使用InverseProperty反向導航屬性去指定經過哪一個屬性創建引用關係,不然數據庫建不起來。git

 

經過一個小DEMO作試驗。github

新建Asp.Net Core MVC網站項目,添加2個實體類以下所示web

 

    //班級
    public class SchoolClass
    {
        //主鍵
        public int ID { get; set; }

        //班級名字
        public string ClassTitle { get; set; }

        //本班級的學生集合
        public List<Student> Students { get; set; }

        //班長
        public Student ClassMonitor { get; set; }

        //語文課表明
        public Student Chinese { get; set; }

        //數學課表明
        public Student Mathematics { get; set; }
    }

    //學生
    public class Student
    {
        //主鍵
        public int ID { get; set; }

        //姓名
        public string Name { get; set; }

        //學生所在的班級
        public SchoolClass MyClass { get; set; }
    }

  

而後經過右鍵菜單添加SchoolClass實體類的控制器,讓系統自動建立數據庫上下文代碼數據庫

 

而後會收到一個錯誤。app

Unable to determine the relationship represented by navigation property 'SchoolClass.Students' of type 'List<Student>'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. StackTrace:async

 

系統沒法判斷SchoolClass多個Student導航屬性的關係,此時能夠在Students屬性上面添加反向導航屬性[InverseProperty("MyClass")],就能夠完成自動化建立控制器了。數據庫設計

 

[InverseProperty("MyClass")]
public List<Student> Students { get; set; }

  

而後在軟件啓動時建立一組測試數據。ide

        public static void Main(string[] args)
        {
            //CreateWebHostBuilder(args).Build().Run();

            IWebHost webHost = CreateWebHostBuilder(args).Build();

            //系統初始化
            AppInit(webHost.Services);

            webHost.Run();
        }

        //系統初始化
        private static void AppInit(IServiceProvider serviceProvider)
        {
            //初始化數據庫
            using (var scope = serviceProvider.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<StudentWebContext>();

                //確保建立數據庫
                context.Database.EnsureCreated();

                if (context.SchoolClass.Any())
                    return;

                var schoolClass61 = new SchoolClass()
                {
                    ClassTitle = "六一班"
                };

                //先保存班級,不然報錯
                //Unable to save changes because a circular dependency was detected in the data to be saved: 'SchoolClass [Added] <- Students MyClass { 'MyClassID' } Student [Added] <- Chinese { 'ChineseID' } SchoolClass [Added]'.
                context.Add(schoolClass61);
                int rows = context.SaveChanges();
                Console.WriteLine($"添加了班級{schoolClass61.ClassTitle}, 影響記錄{rows}");

                var student1 = new Student()
                {
                    Name = "張三",
                };

                var student2 = new Student()
                {
                    Name = "李四",
                };

                var student3 = new Student()
                {
                    Name = "王五",
                };

                var student4 = new Student()
                {
                    Name = "趙六",
                };

                schoolClass61.Students = new List<Student>()
                {
                    student1,
                    student2,
                    student3,
                    student4
                };

                //設置同窗的職位
                schoolClass61.ClassMonitor = student1;
                schoolClass61.Chinese = student2;
                schoolClass61.Mathematics = student3;

                //保存到數據庫
                rows = context.SaveChanges();
                Console.WriteLine($"添加了{schoolClass61.Students.Count}位同窗, 影響記錄{rows}");

            }
        }

  

 

而後修改控制器的Details方法,顯示班級詳細信息時Include加載所有學生集合Students,不須要再加載Chinese等各個課表明導航屬性,由於已經加載了班上的所有學生,EF Core會自動處理這些Student類型的導航屬性。測試

public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var schoolClass = await _context.SchoolClass
                .Include(x => x.Students)
                .FirstOrDefaultAsync(m => m.ID == id);
            if (schoolClass == null)
            {
                return NotFound();
            }

            return View(schoolClass);
        }

  

修改Details頁面顯示班級學生和各個職務的學生。網站

<dt class="col-sm-2">
            班上的同窗
        </dt>
        <dd class="col-sm-10">
            @foreach (var student in Model.Students)
            {
                @student.Name<br />
            }
        </dd>
        <dt class="col-sm-2">
            班長
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ClassMonitor.Name)
        </dd>
        <dt class="col-sm-2">
            語文課表明
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Chinese.Name)
        </dd>
        <dt class="col-sm-2">
            數學課表明
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Mathematics.Name)
        </dd>

  

運行成功。

打開數據庫鏈接,能夠查看系統自動建立的外鍵引用,徹底符合預期。

CREATE TABLE [dbo].[Student] (
    [ID]        INT            IDENTITY (1, 1) NOT NULL,
    [Name]      NVARCHAR (MAX) NULL,
    [MyClassID] INT            NULL,
    CONSTRAINT [PK_Student] PRIMARY KEY CLUSTERED ([ID] ASC),
    CONSTRAINT [FK_Student_SchoolClass_MyClassID] FOREIGN KEY ([MyClassID]) REFERENCES [dbo].[SchoolClass] ([ID])
);

CREATE TABLE [dbo].[SchoolClass] (
    [ID]             INT            IDENTITY (1, 1) NOT NULL,
    [ClassTitle]     NVARCHAR (MAX) NULL,
    [ClassMonitorID] INT            NULL,
    [ChineseID]      INT            NULL,
    [MathematicsID]  INT            NULL,
    CONSTRAINT [PK_SchoolClass] PRIMARY KEY CLUSTERED ([ID] ASC),
    CONSTRAINT [FK_SchoolClass_Student_ChineseID] FOREIGN KEY ([ChineseID]) REFERENCES [dbo].[Student] ([ID]),
    CONSTRAINT [FK_SchoolClass_Student_ClassMonitorID] FOREIGN KEY ([ClassMonitorID]) REFERENCES [dbo].[Student] ([ID]),
    CONSTRAINT [FK_SchoolClass_Student_MathematicsID] FOREIGN KEY ([MathematicsID]) REFERENCES [dbo].[Student] ([ID])
);

  

繼續試驗,再增長一個老師實體類Teacher

    //老師
    public class Teacher
    {
        //主鍵
        public int ID { get; set; }

        //姓名
        public string Name { get; set; }

        //老師做爲班主任管理的班級
        public SchoolClass AdminClass { get; set; }
    }

  

給班級SchoolClass增長班主任、語文老師、數學老師屬性

        //班主任
        public Teacher HeadTeacher { get; set; }

        //語文老師
        public Teacher ChineseTeacher { get; set; }

        //數學老師
        public Teacher MathTeacher { get; set; }

  

修改Details方法,加載老師屬性對象

            var schoolClass = await _context.SchoolClass
                .Include(x => x.Students)
                .Include(x => x.HeadTeacher)
                .Include(x => x.ChineseTeacher)
                .Include(x => x.MathTeacher)
                .FirstOrDefaultAsync(m => m.ID == id);

  

修改Details頁面增長顯示老師

        <dt class="col-sm-2">
            班主任
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.HeadTeacher.Name)
        </dd>
        <dt class="col-sm-2">
            語文老師
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ChineseTeacher.Name)
        </dd>
        <dt class="col-sm-2">
            數學老師
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.MathTeacher.Name)
        </dd>

  

補全StudentWebContext的數據表。

 

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

        public DbSet<SchoolClass> SchoolClass { get; set; }

        public DbSet<Student> Student { get; set; }

        public DbSet<Teacher> Teacher { get; set; }

    }

  

項目啓動時增長老師的測試數據

                //添加老師
                var teacher1 = new Teacher()
                {
                    Name = "孔子"
                };

                var teacher2 = new Teacher()
                {
                    Name = "李白"
                };

                var teacher3 = new Teacher()
                {
                    Name = "祖沖之"
                };

                //設置老師的職位
                schoolClass61.HeadTeacher = teacher1;
                schoolClass61.ChineseTeacher = teacher2;
                schoolClass61.MathTeacher = teacher3;

                //保存到數據庫
                rows = context.SaveChanges();
                Console.WriteLine($"添加了老師同窗, 影響記錄{rows}");

  

打開VS2017的SQL Server對象管理器,經過右鍵菜單粗暴刪除SchoolClass、Student數據表,再次運行項目,再次收到相似的錯誤

Unable to determine the relationship represented by navigation property 'SchoolClass.HeadTeacher' of type 'Teacher'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

 

參照上述方法,給班級SchoolClass的班主任屬性HeadTeacher增長反向導航屬性[InverseProperty("AdminClass")],這個問題就解決了。

        //班主任
        [InverseProperty("AdminClass")]
        public Teacher HeadTeacher { get; set; }

  

再次運行,會收到新的錯誤

The child/dependent side could not be determined for the one-to-one relationship between 'Teacher.AdminClass' and 'SchoolClass.HeadTeacher'. To identify the child/dependent side of the relationship, configure the foreign key property. If these navigations should not be part of the same relationship configure them without specifying the inverse. See http://go.microsoft.com/fwlink/?LinkId=724062 for more details.

 

訪問http://go.microsoft.com/fwlink/?LinkId=724062,自動跳轉到https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships#one-to-one,看介紹:

一對一

一對一關係兩端具備引用導航屬性。 它們遵循相同的約定做爲一個對多關係,但在外鍵屬性,以確保只有一個依賴於與每一個主體上引入了惟一索引。

 

很差理解,有點繞?看示例的代碼,大約是把其中一個實體類的導航屬性改造爲外鍵ID和導航屬性相結合的方式。照辦:

 

        public int AdminClassID { get; set; }

        //老師做爲班主任管理的班級
        public SchoolClass AdminClass { get; set; }

  

再次運行,能夠建立數據庫了,可是報錯:

 

SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Teacher_SchoolClass_AdminClassID". The conflict occurred in database "StudentWebContext", table "dbo.SchoolClass", column 'ID'.

 

大意是AdminClassID屬性不容許爲空。看數據庫設計器Teacher的代碼,AdminClassID是非空的:

 

CREATE TABLE [dbo].[Teacher] (
    [ID]           INT            IDENTITY (1, 1) NOT NULL,
    [Name]         NVARCHAR (MAX) NULL,
    [AdminClassID] INT            NOT NULL,
    CONSTRAINT [PK_Teacher] PRIMARY KEY CLUSTERED ([ID] ASC),
    CONSTRAINT [FK_Teacher_SchoolClass_AdminClassID] FOREIGN KEY ([AdminClassID]) REFERENCES [dbo].[SchoolClass] ([ID]) ON DELETE CASCADE

  

實際上,一位老師,是能夠不擔當任何一個班級的班主任的,所以AdminClassID屬性應該是可空的。再改一下

 

        public int? AdminClassID { get; set; }

  

刪除數據表,再次運行,沒有任何問題了,數據庫Teacher代碼是正確的,

CREATE TABLE [dbo].[Teacher] (
    [ID]           INT            IDENTITY (1, 1) NOT NULL,
    [Name]         NVARCHAR (MAX) NULL,
    [AdminClassID] INT            NULL,
    CONSTRAINT [PK_Teacher] PRIMARY KEY CLUSTERED ([ID] ASC),
    CONSTRAINT [FK_Teacher_SchoolClass_AdminClassID] FOREIGN KEY ([AdminClassID]) REFERENCES [dbo].[SchoolClass] ([ID])

  

Details頁面數據顯示也是正確的。

小結

EF Core多對一關係配置要點:

  1. A實體引用多個B導航屬性,B實體引用一個A導航屬性;
  2. A實體類註明其中一個B導航屬性爲InverseProperty;
  3. B實體類定義A導航屬性的可空外鍵AID?;

  

代碼:https://github.com/woodsun2018/StudentWeb

相關文章
相關標籤/搜索