基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2

来个目录吧:
第一章-入门
第二章- Entity Framework Core Nuget包管理
第三章-创建、修改、删除、查询
第四章-排序、过滤、分页、分组
第五章-迁移,EF Core 的codefirst使用
暂时就这么多。后面陆续更新吧

Entity Framework Core Nuget包管理

如果你创建项目的时候启用了个人身份验证的话,项目中就已经包含了EFCore的支持。
如果你是单纯的空项目想将EFCore添加到你的项目中话,你需要安装一下的Nuget程序包:

vs2017可以直接进行编辑项目的.csproj文件,安装所需软件包。
···
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0-msbuild3-final" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="1.0.0-msbuild3-final" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0-msbuild3-final" />
</ItemGroup>
···
(您可以编辑的.csproj文件右击解决方案资源管理器中的项目名称并选择编辑 ContosoUniversity.csproj)。

我又来了,我亲测了下.NETCORE1.1目前不支持Microsoft.EntityFrameworkCore.Tools.DotNet这样玩,这里先略过

创建数据模型

创建Contoso大学实体前,说下他们的关联关系吧。

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

Student 和Enrollment 的实体关系为一对多。
Course和Enrollment的实体关系同样为一对多。

简单来说就是一个学生可以参加任意一门课程,而一门课程可以有很多个学生。(当然同一个课程该学生只能参加一次,反之亦然)

然后我们开始创建实体吧。

Student 实体

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

我们在根目录新建一个“Models”文件夹,创建一个“Student”的类文件,复制以下代码替换掉内容。

using System;
using System.Collections.Generic;
namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Id 属性将作为Student类对应的数据库表的主键,默认情况下EF框架都会将ID或者classnameID作为主键。

Enrollments属性是一个导航属性。

老外翻译太绕了,导航属性就是方便你从一个对象导航到关联对象,也可以用于设置对象之间的关联。

这里Enrollments就是Student实体的导航属性,可以通过Enrollments属性将实体Enrollment和Student中的关联信息获取出来。换句话说:一个学生在数据库中有2行登记信息,(每行包含了Student实体的ID),那么该Student可以从导航属性Enrollments中获取到2行登记信息。

如果一个导航属性包含了多个实体(如:一对多,多对多的关系),那么他们的类型必须一个list类型,可以添加、删除、修改。比如:ICollection<T> 。当然你还可以声明为List<T>或者HashSet<T>.如果你声明为ICollection<T> .EF会默认创建类型为```HashSet<T>``

Enrollment 实体

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

同样在Models文件中,创建一个类“Enrollment” 然后把代码替换为如下:

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}


我们将EnrollmentID作为Enrollment的主键,使用的是classnameID而不是像Student实体中的ID。这里暂时不解释,后面会提到这个问题。

我们声明了一个枚举Grade属性。而在Enrollment实体中Grade是个可空类型,说明他是一个默认可以为空的值,可以在后面根据具体的业务情况来进行赋值处理。

StudentID 作为属性外键,对应的导航属性为Student。ENrollment和Student的关联关系为一对一,所以Enrollment只能持有一个Student实体。(而Student拥有了Enrollment的多个导航属性)

CourseID作为Course的导航属性外键。同样的Enrollment和Course也是一对一的关系。

在这里StudentID作为Student导航属性的外键,等同于Student实体中的ID主键

Course 实体

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

在Models文件中,创建一个Course类,然后替换为如下代码:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

在这里Enrollments作为导航属性,一个课程会有多个不同学生的登记信息,所以是一对多的关系。

创建数据库上下文(Database Context)

我们需要创建一个作为EF框架用来连接数据库上下文的类。我们创建的类是从System.Data.Entity.DbContext中派生出来的。你可以定义哪些实体包含在EF的数据模型中。你也可以自定义特定的EF行为。在这个项目中,我们创建一个类名“SchoolContext”

  • 在根目录创建一个文件夹“Data”。
  • 在“Data”这个文件夹中,创建一个新的类“SchoolContext”,然后替换代码为下面:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}

我们为每个实体都创建了一个DbSet的属性。在EF框架中,实体集通常对应数据库中的表,一个实体对应表中的一行数据。

在这里你可以忽略掉**DbSet<Enrollment> and DbSet<Course> **,它同样会生成表。因为Student会引用Enrollment实体,而Enrollment中包含了Course实体。同样被会引用。EF在生成表的时候会包含他们的引用实体。

创建数据库的时候,数据库的表名会跟 DbSet的属性名一致。属性名称通常会是复数(如 student的表名是Students),但是大多数开发者不同意将表名和实体名字区分开,这样容易混淆。下面的教程就会教会你怎么通过指定你个性化的DbContext表名
把下面的代码,复制到最后的DbSet属性下面

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }

使用依赖注入的方式来注入DbContext

ASP.NET Core默认实现了依赖注入。在程序启动的时候使用依赖注入将服务(EF的数据库上下文)注入。这些服务的组件(如MVC控制器)通过构造函数的参数实现,下面我们会逐步实现。

首先我们打开“Startup.cs”类,然后将“SchoolContext”注入到ConfigureServices方法中。

services.AddDbContext<SchoolContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

连接字符串的名称是通过DbContextOptionsBuilder对象的方法进行上下文调用的。

而在ASP.NET CORE中的连接字符串是通过“appsettings.json”文件实现的。
如下代码:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

SQL Server Express LocalDB

上面的链接字符串说下吧,连接字符串指定SQL Server LocalDB数据库。LocalDB是一个轻量级版本的SQL Server Express数据库引擎,用于开发环境、而不是生产。LocalDB开始于需求和运行在用户模式下,所以没有复杂的配置。默认情况下,LocalDB创建。

mdf数据库文件在C:/用户/ wer_ltm 文件夹中
我肯定不是这样干的。我们修改下链接字符串

Data Source=.; Database=MaterialCirculation; User ID=sa; Password=123;

初始化数据库并且添加一些测试数据到数据库中

EF框架默认生成的数据库 是一个空数据库,为了我们的测试、开发方便我们添加一些测试数据到数据库中。

这里我们使用EnsureCreated方法来自动创建数据库。在后面的教程中,我们会通过Codefirst迁移的方式来修改数据库而不是传统的删除并重新创建一个数据库的方式来改变模型架构。

在"Data"文件夹中,创建一个“DbInitializer”类,然后把现有代码替换为以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
            new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
            new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
            new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
            new Course{CourseID=1050,Title="Chemistry",Credits=3,},
            new Course{CourseID=4022,Title="Microeconomics",Credits=3,},
            new Course{CourseID=4041,Title="Macroeconomics",Credits=3,},
            new Course{CourseID=1045,Title="Calculus",Credits=4,},
            new Course{CourseID=3141,Title="Trigonometry",Credits=4,},
            new Course{CourseID=2021,Title="Composition",Credits=3,},
            new Course{CourseID=2042,Title="Literature",Credits=4,}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
            new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
            new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
            new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
            new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
            new Enrollment{StudentID=3,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=1050,},
            new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
            new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
            new Enrollment{StudentID=6,CourseID=1045},
            new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }}

上面的代码会先检查数据库是否有任何的学生信息,如果没有的话,他会假定数据库需要新的测试种子数据,这里选择了将数据添加到数组中,没有选择List<T>集合来进行性能的优化。

在Startup.cs中,修改Configure 中的方法,以便程序在启动的时候调用Seed方法。

  • 首先修改Configure 方法,添加构造参数"SchoolContext "到方法中,这样ASP.NET 的依赖注入可以提供服务给DbInitializer类。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SchoolContext context)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

添加代码** DbInitializer.Initialize(context);方法,在整个Configure**方法的最下面。

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });

    DbInitializer.Initialize(context);//记得添加这行
}

现在,当你第一次运行程序的时候会给你创建测试数据。每当你更改实体的时候,也就是数据模型的时候,可以删除数据库。更新种子数据并重新创建一个新的数据库。在以后的教程中,你会学会如何通过codefirst的方式通过迁移的方式来进行数据的修改和创建。

创建一个控制器和视图

接下来,我们使用visual studio 中脚手架功能,添加MVC的控制器和视图,将使用EF的查询和保存数据。

通过脚手架功能,我们可以自动创建一个CRUD的功能。你可以通过修改脚手架生成的代码来满足你的业务要求。当你的类发生变化的时候,你可以通过脚手架重新生成代码。

在VS2017 脚手架被叫做了基架 ,在我看来一样的难听。。。

  • 右键选择“Controllers”文件夹,然后选择添加>新搭基建项目

  • 在对话框中,

    • 选择“视图使用EntityFramework的MVC控制器”
      -- 点击添加
  • 添加控制器对话框中
    -- 模型类:选择Student
    -- 数据上下文类:选择 SchoolContext
    -- 接收默认的StudentController作为名称
    -- 点击添加

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

当你点击“添加的时候”,VS 基架引擎会自动生成一个“StudentController.cs”文件和一组视图文件(.cshtml)。

打开StudentController控制器,你会发现SchoolContext 作为了构造函数的参数。

namespace ContosoUniversity.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

我们之前在“Startup.cs”中已经配置了依赖注入。现在
ASP.NET会通过依赖注入的方式,将SchoolContext 注入到控制器中。

控制器中包含了一个Index的Action 方法,它会将数据库中的所有学生信息都显示出来。

await _context.Students.ToListAsync() 方法会从Student实体通过读取数据库上下文属性获取学生列表。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

这里是采用了异步方法。我们在后面讲解

我们先打开“Views/Students/Index.cshtml ”视图文件:

@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

好了 按 CTRL + F5 运行项目或选择调试 > 开始执行(不调试)。

在菜单上选择Student按钮。然后就可以看到我们的数据信息。

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

查看数据库

如果你是改了连接字符串的话,直接打开数据库看表吧。如果你没有更改,那就不要跳过这里了。

我们刚刚说过了,你如果采用的是免费版本的话,现在要查看数据库就需要打开
工具-连接数据库

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

展开表信息,然后选择Student 然后右键表,点击查看数据。

基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-2
Paste_Image.png

公约

由于EF框架的公约/设定,为了让EntityFramework能够为您创建一个完整的数据库,你只需要编写很少的代码。

  • DbSet 属性的名称作为表的名称。对于不是由DbSet属性引用的实体,实体类名将作为表的名称
  • 实体属性名称会作为表的列名称
  • 名为ID或者classnameID的实体属性会被识别为主键属性
  • 如果属性被命名,属性则会被EF作为外键属性。(例如:StudentID对于Student导航属性,因为Student实体的主键为ID)。外键属性也可以随意命名。(例如:EnrollmentID因为Enrollment实体的主键是EnrollmentID)

一些常规的设定是可以进行覆盖的。例如你可以显示指定表的名称,就如我们之前做的,自定义表的名称。
当然你也可以设置列名称并将任何属性设置为主键或者外键。在后面的教程中我们会涉及。

关于异步代码

异步编程是ASP.NET Core和EF Core的默认模式。

Web服务器的线程数量是有限的,在高负载的情况下,可能所有的线程都被占用了。当发生这种情况的时候,服务器不能处理新的请求,直到线程被释放出来。再以前使用同步代码请求,许多线程可能被绑定,他们实际上没有做任何工作,因为他们正在等待I/O完成。使用异步写代码,当进程等待I/O完成的时候,它的线程就会被释放,服务器用于处理其他请求。因此,异步代码使服务器资源能够更有效地使用,并且服务器能够无延迟地处理更多流量。

异步代码在运行时引入少量开销,但是对于低流量情况,性能命中是可以忽略的,而对于高流量情况,潜在的性能改进是巨大的。

在以下代码中,async关键字,Task<T>返回值,await关键字和ToListAsync方法使代码异步执行。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

  • 该async关键字告诉编译器生成方法不会回调和自动创建Task<IActionResult>返回的对象。
  • 返回类型Task<IActionResult>表示正在进行的工作与类型的结果IActionResult。
  • 该await关键字使编译器将该方法拆分为两部分。第一部分以异步启动的操作结束。第二部分被放入一个在操作完成时被调用的回调方法中。
  • ToListAsync是ToList扩展方法的异步版本。

当你使用EntityFramework的异步代码的时候,你需要注意一些事情:

  • 只有查询或者发送命令到数据库的时候才能使用异步语句。如:ToListAsync,SingleOrDefaultAsync,和SaveChangesAsync。不包含 类型为IQueryable,修改命令。比如
var students = context.Students.Where(s => s.LastName == "Davolio").
  • EF的上下文不是线程安全的:不要尝试执行/并行多个操作。当您调用任何异步EF方法的时候,始终使用“await”关键字。

  • 如果你想使用异步的性能优势,请确保你使用的任何包(例如分页),他们调用的Entity Framework 方法,使用async发送到数据库中。

上一篇:jzoj7177-鱼跃龙门【exgcd】


下一篇:linux vim配置和相关+服务器管理杂谈(只记录坑)