zoukankan      html  css  js  c++  java
  • Entity Framework Core 2.0 入门

    该文章比较基础, 不多说废话了, 直接切入正题.

    该文分以下几点:

    • 创建Model和数据库
    • 使用Model与数据库交互
    • 查询和保存关联数据

    EF Core支持情况

    EF Core的数据库Providers:

    此外还即将支持CosmosDB和 Oracle.

    EFCore 2.0新的东西:

    查询:

    • EF.Functions.Like()
    • Linq解释器的改进
    • 全局过滤(按类型)
    • 编译查询(Explicitly compiled query)
    • GroupJoin的SQL优化.

     映射:

    • Type Configuration 配置
    • Owned Entities (替代EF6的复杂类型)
    • Scalar UDF映射
    • 分表

    性能和其他

    • DbContext Pooling, 这个很好
    • Raw SQL插入字符串.
    • Logging
    • 更容易定制配置

    1.创建数据库和Model

    准备.net core项目

    项目结构如图:

    由于我使用的是VSCode, 所以需要使用命令行:

    mkdir LearnEf && cd LearnEf
    dotnet new sln // 创建解决方案
    
    mkdir LearnEf.Domains && cd LearnEf.Domains
    dotnet new classlib // 创建LearnEf.Domains项目
    
    cd ..
    mkdir LearnEf.Data && cd LearnEf.Data
    dotnet new classlib // 创建LearnEf.Data项目
    
    cd ..
    mkdir LearnEf.UI && cd LearnEf.UI
    dotnet new console // 创建控制台项目
    
    cd ..
    mkdir LearnEf.Tests && cd LearnEf.Tests
    dotnet new xunit // 创建测试项目

    为解决方案添加项目:

    dotnet sln add LearnEf.UI/LearnEf.UI.csproj
    dotnet sln add LearnEf.Domains/LearnEf.Domains.csproj
    dotnet sln add LearnEf.Data/LearnEf.Data.csproj
    dotnet sln add LearnEf.Tests/LearnEf.Tests.csproj

    为项目之间添加引用:

    LearnEf.Data依赖LearnEf.Domains:

    cd LearnEf.Data
    dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj

    LearnEf.Console依赖LearnEf.Domains和LearnEf.Data:

    cd ../LearnEf.UI
    dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

    LearnEf.Test依赖其它三个项目:

    cd ../LearnEf.Tests
    dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj ../LearnEf.UI/LearnEf.UI.csproj

    (可能需要执行dotnet restore)

    在Domains项目下直接建立两个Model, 典型的一对多关系Company和Department:

    using System;
    using System.Collections.Generic;
    
    namespace LearnEf.Domains
    {
        public class Company
        {
            public Company()
            {
                Departments = new List<Department>();
            }
    
            public int Id { get; set; }
            public string Name { get; set; }
            public DateTime StartDate { get; set; }
            public List<Department> Departments { get; set; }
        }
    }
    namespace LearnEf.Domains
    {
        public class Department
        {
            public int Id { get; set; }
            public int CompanyId { get; set; }
            public Company Company { get; set; }
        }
    }

    添加Entity Framework Core库:

    首先Data项目肯定需要安装这个库, 而我要使用sql server, 参照官方文档, 直接在解决方案下执行这个命令:

    dotnet add ./LearnEf.Data package Microsoft.EntityFrameworkCore.SqlServer
    dotnet restore

    创建DbContext:

    在Data项目下创建MyContext.cs:

    using LearnEf.Domains;
    using Microsoft.EntityFrameworkCore;
    
    namespace LearnEf.Data
    {
        public class MyContext : DbContext
        {
            public DbSet<Company> Companies { get; set; }
            public DbSet<Department> Departments { get; set; }
        }
    }

    指定数据库Provider和Connection String:

    在EFCore里, 必须明确指定Data Provider和Connection String.

    可以在Context里面override这个Onconfiguring方法:

    有一个错误, 应该是Server=localhost;

    (这里无需调用父类的方法, 因为父类的方法什么也没做).

    UseSqlServer表示使用Sql Server作为Data Provider. 其参数就是Connection String.

    在运行时EfCore第一次实例化MyContext的时候, 就会触发这个OnConfiguring方法. 此外, Efcore的迁移Api也可以获得该方法内的信息.

    EF Core迁移:

    简单的来说就是 Model变化 --> 创建migration文件 --> 应用Migration到数据库或生成执行脚本.

    添加Migration (迁移):

    由于我使用的是VSCode+dotnet cli的方法, 所以需要额外的步骤来使dotnet ef命令可用.

    可以先试一下现在的效果:

    可以看到, dotnet ef 命令还不可用.

    所以参考官方文档: https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

    可执行项目(Startup project)需要EFCore迁移引擎库, 所以对LearnEf.UI添加这个库:

    dotnet add ./LearnEf.UI package Microsoft.EntityFrameworkCore.Design
    dotnet restore

    然后打开LearnEf.UI.csproj 添加这段代码, 这个库是EF的命令库:

     <ItemGroup>
        <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
      </ItemGroup>

    最后内容如下:

    然后再执行dotnet ef命令, 就应该可用了:

    现在, 添加第一个迁移:

    cd LearnEf.UI
    dotnet ef migrations add Initial --project=../LearnEf.Data

    --project参数是表示需要使用的项目是哪个.

    命令执行后, 可以看到Data项目生成了Migrations目录和一套迁移文件和一个快照文件:

    检查这个Migration.

    前边带时间戳的那两个文件是迁移文件.

    另一个是快照文件, EFCore Migrations用它来跟踪所有Models的当前状态. 这个文件非常重要, 因为下次你添加迁移的时候, EFcore将会读取这个快照并将它和Model的最新版本做比较, 就这样它就知道哪些地方需要有变化.

    这个快照文件解决了老版本Entity Framework的一个顽固的团队问题.

    使用迁移文件创建脚本或直接生成数据库.

    生成创建数据库的SQL脚本:

    dotnet ef migrations script --project=../LearnEf.Data/LearnEf.Data.csproj

    Sql脚本直接打印在了Command Prompt里面. 也可以通过指定--output参数来输出到具体的文件.

    这里, 常规的做法是, 针对开发时的数据库, 可以通过命令直接创建和更新数据库. 而针对生产环境, 最好是生成sql脚本, 然后由相关人员去执行这个脚本来完成数据库的创建或者更新.

    直接创建数据库:

    dotnet ef database update --project=../LearnEf.Data/LearnEf.Data.csproj --verbose

    --verbose表示显示执行的详细过程, 其结果差不多这样:

    这里的执行过程和逻辑是这样的: 如果数据库不存在, 那么efcore会在指定的连接字符串的地方建立该数据库, 并应用当前的迁移. 如果是生成的sql脚本的话, 那么这些动作必须由您自己来完成.

    然后查看一下生成的表. 

    不过首先, 如果您也和我一样, 没有装Sql server management studio或者 Visual Studio的话, 请您先安装VSCode的mssql这个扩展:

    重启后, 建立一个Sql文件夹, 然后建立一个Tables.sql文件, 打开命令面板(windows: Shift+Ctrl+P, mac: Cmd+Shift+P), 选择MS SQL: Connect.

    然后选择Create Connection Profile:

    输入Sql的服务器地址:

    再输入数据库名字:

    选择Sql Login(我使用的是Docker, 如果windows的话, 可能使用Integrated也可以):

    输入用户名:

    密码:

    选择是否保存密码:

    最后输入档案的名字:

    随后VSCode将尝试连接该数据库, 成功后右下角会这样显示 (我这里输入有一个错误, 数据库名字应该是LearnEF):

    随后在该文件中输入下面这个sql语句来查询所有的Table:

    --  Table 列表
    SELECT * FROM INFORMATION_SCHEMA.TABLES
    WHERE TABLE_TYPE='BASE TABLE';

    执行sql的快捷键是windows: Shift+Ctrp+E, mac: Cmd+Shift+E, 或者鼠标右键.

    结果如图:

    OK表是创建成功了(还有一个迁移历史表, 这个您应该知道).

    接下来我看看表的定义:

    -- Companies表:
    exec sp_help 'Companies';

    其中Name字段是可空的并且长度是-1也就是nvarchar(Max).

    Departments表的Name字段也是一样的.

    再看看那个MigrationHistory表:

    -- MigrationHistory:
    SELECT * FROM dbo.__EFMigrationsHistory;

    可以看到, efcore到migration 历史表里面只保存了MigrationId.

    在老版本到ef里, migration历史表里面还保存着当时到迁移的快照, 创建迁移的时候还需要与数据库打交道. 这就是我上面提到的如果团队使用ef和源码管理的话, 就会遇到这个非常令人头疼的问题.

    如果使用asp.net core的话.

    在解决方案里再建立一个asp.net core mvc项目:

    mkdir LearnEf.Web && cd LearnEf.Web
    dotnet new mvc

    在解决方案里添加该项目:

    dotnet sln add ./LearnEf.Web/LearnEf.Web.csproj

    为该项目添加必要的引用:

    cd LearnEf.Web
    dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

    为测试项目添加该项目引用:

    cd ../*Tests
    dotnet add reference ../LearnEf.Web/LearnEf.Web.csproj

    操作完之后, 我们可以做以下调整, 去掉MyContext里面的OnConfiguring方法, 因为asp.net core有内置的依赖注入机制, 我可以把已经构建好的DbContextOptions直接注入到构造函数里:

    这样的话, 我们可以让asp.net core来决定到底使用哪个Data Provider和Connection String:

    这也就意味着, Web项目需要引用EfCore和Sql Provider等, 但是不需要, 因为asp.net core 2.0这个项目模版引用了AspNetCore.All这个megapack, 里面都有这些东西了.

    虽然这个包什么都有, 也就是说很大, 但是如果您使用Visual Studio Tooling去部署的话, 那么它只会部署那些项目真正用到的包, 并不是所有的包.

    接下来, 在Web项目的Startup添加EfCore相关的配置:

     public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
                services.AddDbContext<MyContext>
                    (options => options.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;"));
            }

    这句话就是把MyContext注册到了asp.net core的服务容器中, 可以供注入, 同时在这里指定了Data Provider和Connection String.

    与其把Connection String写死在这里, 不如使用appSettings.json文件:

    然后使用内置的方法读取该Connection String:

     public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc();
                services.AddDbContext<MyContext>
                    (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            }

    回到命令行进入Web项目, 使用dotnet ef命令:

    说明需要添加上面提到的库, 这里就不重复了.

    然后, 手动添加一个Migration叫做InitialAspNetCore:

    dotnet ef migrations add InitialAspNetCore --project=../LearnEf.Data

    看一下迁移文件:

    是空的, 因为我之前已经使用UI那个项目进行过迁移更新了. 所以我要把这个迁移删掉:

    dotnet ef migrations remove --project=../LearnEf.Data

    然后这两个迁移文件就删掉了:

    多对多关系和一对一关系:

    这部分的官方文档在这: https://docs.microsoft.com/en-us/ef/core/modeling/relationships

    对于多对多关系, efcore需要使用一个中间表, 我想基本ef使用者都知道这个了, 我就直接贴代码吧.

    建立一个City.cs:

    namespace LearnEf.Domains
    {
        public class City
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }
    }

    Company和City是多对多的关系, 所以需要建立一个中间表,叫做 CompanyCity:

    namespace LearnEf.Domains
    {
        public class CompanyCity
        {
            public int CompanyId { get; set; }
            public int CityId { get; set; }
            public Company Company { get; set; }
            public City City { get; set; }
        }
    }

    修改Company:

    修改City:

    尽管Efcore可以推断出来这个多对多关系, 但是我还是使用一下FluentApi来自定义配置一下这个表的主键:

    MyContext.cs:

    using LearnEf.Domains;
    using Microsoft.EntityFrameworkCore;
    
    namespace LearnEf.Data
    {
        public class MyContext : DbContext
        {
            public MyContext(DbContextOptions<MyContext> options)
                : base(options)
            {
    
            }
            public DbSet<Company> Companies { get; set; }
            public DbSet<Department> Departments { get; set; }
            public DbSet<CompanyCity> CompanyCities { get; set; }
    
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<CompanyCity>()
                    .HasKey(c => new { c.CompanyId, c.CityId });
            }
    
            // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            // {
            //     optionsBuilder.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;");
            //     base.OnConfiguring(optionsBuilder);
            // }
        }
    }

    完整的写法应该是:

    其中红框里面的部分不写也行.

    接下来建立一个一对一关系, 创建Model叫Owner.cs:

    namespace LearnEf.Domains
    {
        public class Owner
        {
    public int Id { get; set;}
    public int CompanyId { get; set; } public string Name { get; set; } public Company Company { get; set; } } }

    修改Company:

    配置关系:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<CompanyCity>()
                    .HasKey(c => new { c.CompanyId, c.CityId });
    
                modelBuilder.Entity<CompanyCity>().HasOne(x => x.Company)
                    .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CompanyId);
    
                modelBuilder.Entity<CompanyCity>().HasOne(x => x.City)
                    .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CityId);
    
                modelBuilder.Entity<Owner>().HasOne(x => x.Company).WithOne(x => x.Owner)
                    .HasForeignKey<Owner>(x => x.CompanyId);
            }

    这里面呢, 这个Owner对于Company 来说 是可空的. 而对于Owner来说, Company是必须的. 如果针对Owner想让Company是可空的, 那么CompanyId的类型就应该设置成int?.

    再添加一个迁移:

    dotnet ef migrations add AddRelationships --project=../LearnEf.Data

    查看迁移文件:

    查看一下快照;

    没问题, 那么更新数据库:

    dotnet ef database update AddRelationships --project=../LearnEf.Data --verbose

    更新成功:

    对现有数据库的反向工程

    这部分请查看官方文档吧, 很简单, 我实验了几次, 但是目前还没有这个需求.

    使用Model与数据库交互

    输出Sql语句.

    对于asp.net core 2.0项目, 参考官方文档: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?tabs=aspnetcore2x

    实际上, 项目已经配置好Logging部分了, 默认是打印到控制台和Debug窗口的. 源码: https://github.com/aspnet/MetaPackages/blob/dev/src/Microsoft.AspNetCore/WebHost.cs

    而对于console项目, 文档在这: https://docs.microsoft.com/en-us/ef/core/miscellaneous/logging

    需要对LearnEf.Data项目添加这个包: 

    cd LearnEf.Data
    dotnet add package Microsoft.Extensions.Logging.Console
    dotnet restore

    然后为了使用console项目, 需要把MyContext改回来:

    这部分首先是使用LoggerFactory创建了一个特殊的Console Logger. .net core的logging可以显示很多的信息, 这里我放置了两个过滤: 第一个表示只显示Sql命令, 第二个表示细节的显示程度是Information级别.

    最后还要在OnConfiguring方法里告诉modelBuilder使用MyLoggerFactory作为LoggerFactory.

    这就配置好了.

    插入数据.

    这部分很简单, 打开UI项目的Program.cs:

    这里都懂的, 创建好model之后, 添加到context的DbSet属性里, 这时context就开始追踪这个model了.

    SaveChanges方法, 会检查所有被追踪的models, 读取他们的状态. 这里用到是Add方法, context就会知道这个model的状态是new, 所以就应该被插入到数据库. 然后它就根据配置会生成出相应的sql语句, 然后把这个SQL语句执行到数据库. 如果有返回数据的话, 就取得该数据.

    下面就运行一下这个console程序:

    dotnet run --project=./LearnEf.UI

    看下控制台:

    可以看到输出了sql语句, 而且这个出入动作后, 做了一个查询把插入数据生成的Id取了回来.

    默认情况下log不显示传进去的参数, 这是为了安全. 但是可以通过修改配置来显示参数:

    然后控制台就会显示这些参数了:

    批量插入操作.

    可以使用AddRange添加多条数据. 其参数可以是params或者集合.

    可以看到这个和之前Add的Sql语句是完全不同的:

    这个语句我不是很明白.

    批量添加不同类型的数据:

    使用context的AddRange或Add方法, DbContext可以推断出参数的类型, 并执行正确的操作. 上面的方法就是使用了DbContext.AddRange方法, 一次性添加了两种不同类型的model.

    这两个方法对于写一些通用方法或者处理复杂的情况是很有用的.

    Sql Server对于批量操作的限制是, 一次只能最多处理1000个SQL命令, 多出来的命令将会分批执行.

    如果想更改这个限制, 可以这样配置参数:

    简单查询.

    针对DbSet, 使用Linq的ToList方法, 会触发对数据库对查询操作:

    首先把Company的ToString方法写上:

    这样方便输入到控制台.

    然后写查询方法:

    看结果:

    EfCore到查询有两类语法, 一种是Linq方法, 另一种是Linq查询语法:

    这种是Linq方法:

    下面这种是Linq查询语法:

    我基本都是使用第一种方法.

    除了ToList(Async)可以触发查询以外, 遍历foreach也可以触发查询:

    但是这种情况下, 可能会有性能问题. 因为:

    在遍历开始的时候, 数据库连接打开, 并且会一直保持打开的状态, 直到遍历结束.

    所以如果这个遍历很耗时, 那么可能会发生一些问题.

    最好的办法还是首先执行ToList, 然后再遍历.

    查询的过滤.

    这部分和以前的EF基本没啥变化.

    这个很简单, 不说了.

    这里列一下可触发查询的Linq方法:

    还有个两个方法是DbSet的方法, 也可以触发查询动作:

    上面这些方法都应该很熟悉, 我就不写了.

    过滤的条件可以直接家在上面的某些方法里面, 例如:

    通过主键查询, 就可以用DbSet的Find方法:

    这个方法有个优点, 就是如果这条数据已经在Context里面追踪了, 那么查询的时候就不查数据库了, 直接会返回内存中的数据.

    EF.Functions.Like 这个方法是新方法, 就像是Sql语句里面的Like一样, 或者字符串的Contains方法:

    这个感觉更像Sql语句, 输出到Console的Sql语句如下:

    这里还要谈的是First/FirstOrDefault/Last/LastOrDefaut方法.

    使用这些方法必须先使用OrderBy/OrderByDescending排序. 虽然不使用的话也不会报错, 但是, 整个过程就会变成这样, context把整个表的数据家在到内存里, 然后返回第一条/最后一条数据. 如果表的数据比较多的话, 那么就会有性能问题了.

    更新数据.

    很简单, context所追踪的model属性变化后, SaveChanges就会更新到数据库.

    当然, 多个更新操作和插入等操作可以批量执行.

    离线更新.

    就是这种情况, 新的context一开始并没有追踪one这个数据. 通过使用Update方法, 追踪并设置状态为update. 然后更新到数据库.

    可以看到, 在这种情况下, EfCore会更新该model到所有属性.

    Update同样也有DbSet的UpdateRange方法, 也有context到Update和UpdateRange方法, 这点和Add是一样的.

    还有一种方法用于更新, 这个以后再说.

    删除数据.

    DbContext只能删除它追踪的model.

    非常简单, 从log可以看到, 删除动作只用到了主键:

    如果是删除的离线model, 那么Remove方法首先会让Dbcontext追踪这个model, 然后设置状态为Deleted.

    删除同样有RemoveRange方法.

    Raw SQL查询/命令:

    这部分请看文档:

    命令: DbContext.Database.ExecuteSqlCommand();

    查询: DbSet.FromSql() https://docs.microsoft.com/en-us/ef/core/querying/raw-sql;

    这个方法目前还有一些限制, 它只能返回实体的类型, 并且得返回domain model所有的属性, 而且属性的名字必须也得一一对应. SQL语句不可以包含关联的导航属性, 但是可以配合Include使用以达到该效果(https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#including-related-data).

    更多的传递参数方式还需要看文档.

    查询和保存关联数据.

    插入关联数据.

    我之前忘记在Department里面添加Name字段了, 现在添加一下, 具体过程就不写了.

    插入关联数据有几种情况:

    1.直接把要添加的Model的导航属性附上值就可以了, 这里的Department不需要写外键.

    看一下Sql:

    这个过程一共分两步: 1 插入主表, 2,使用刚插入主表数据的Id, 插入子表数据.

    2.为数据库中的数据添加导航属性.

    这时, 因为该数据是被context追踪的, 所以只需在它的导航属性添加新记录, 然后保存即可.

    3.离线数据添加导航属性.

    这时候就必须使用外键了.

    预加载关联数据 Eager Loading.

    也就是查询的时候一次性把数据和其导航属性的数据一同查询出来.

    看看SQL:

    这个过程是分两步实现的, 首先查询了主表, 然后再查询的子表. 这样做的好处就是性能提升.

    (FromSql也可以Include).

    预加载子表的子表:

    可以使用ThenInclude方法, 这个可以老版本ef没有的.

    这里查询Department的时候, 将其关联表Company也查询了出来, 同时也把Company的关联表Owner也查询了出来.

    查询中映射关联数据.

    使用Select可以返回匿名类, 里面可以自定义属性.

    这个匿名类只在方法内有效.

    看下SQL:

    可以看到SQL中只Select了匿名类里面需要的字段.

    如果需要在方法外使用该结果, 那么可以使用dynamic, 或者建立一个对应的struct或者class.

    使用关联导航属性过滤, 但是不加载它们.

    SQL:

    这个比较简单. 看sql一切就明白了.

    修改关联数据.

    也会分两种情况, 被追踪和离线数据.

    被追踪的情况下比较简单, 直接修改关联数据的属性即可:

    看一下SQL:

    确实改了.

    这种情况下, 删除关联数据库也很简单:

    看下SQL:

    删除了.

    下面来看看离线状态下的操作.

    这里需要使用update, 把该数据添加到context的追踪范围内.

    看一下SQL:

    这个就比较怪异了.

    它update了该departmt和它的company以及company下的其他department和company的owner. 这些值倒是原来的值.

    这是因为, 看上面的代码, 查询的时候department的关联属性company以及company下的departments和owner一同被加载了.

    尽管我只update了一个department, 但是efcore把其他关联的数据都识别出来了.

    从DbContext的ChangeTracker属性下的StateManger可以看到有多少个变化.

    这一点非常的重要.

    如何避免这个陷阱呢?

    可以这样做: 直接设置dbContext.Entry().State的值

    这时, 再看看SQL:

    嗯. 没错, 只更新了需要更新的对象.

    2.1版本将于2018年上半年发布, 请查看官网的路线图: https://github.com/aspnet/EntityFrameworkCore/wiki/roadmap

    完. 

  • 相关阅读:
    Python--IO模型
    python queue, pipe, manage
    python多线程,event,互斥锁,死锁,递归锁,信号量
    day34 异常处理、断言、socket之ftp协议
    day34 反射、面向对象内置方法:如__str__、面向对象的软件开发
    PHP基础入门(二)【PHP函数基础】
    PHP基础入门详解(一)【世界上最好用的编程语言】
    H5简单拖放(Drag/Drop)
    手机APP ~ MUI——创建页面方法
    bootstrap栅格系统
  • 原文地址:https://www.cnblogs.com/cgzl/p/8543772.html
Copyright © 2011-2022 走看看