该文章比较基础, 不多说废话了, 直接切入正题.
该文分以下几点:
- 创建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
完.
下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏: