安装dotnet-ef工具
dotnet-ef是对.Net命令行工具dotnet 的扩展,
例如创建并应用从旧模型到新模型的迁移,以及从现有数据库模型生成代码
命令如下
- 检查是否安装dotnet-ef为全局工具
dotnet tool list --global
- 卸载现有版本
dotnet tool uninstall --global dotnet ef
- 安装最新版本
dotnet tool install --global dotnet-ef
- 安装指定版本
dotnet tool install --global dotnet-ef --version 5.0.0
- 安装SqlServer的 NuGet包
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
同样添加 指定版本的包 需要末尾添加 --version 5.0.0
- 安装设计包(用于迁移)
dotnet add package Microsoft.EntityFrameworkCore.Design
CodeFirst模式定义EF Core 模型
EFCore使用约定、注解特性和Fluent API语句的结合,在运行时构建实体模型。
实体类表示表的结构,类的实例表示表中的一行
EF Core 约定
我们编写的代码都需要遵循以下约定
- 表的名称与 DBContext类(例如Products类)中的DbSet属性名匹配
- 数据库列的名称与类中的属性名匹配,例如ProductID
- 假定.Net类型为string 是数据库的 nvarchar类型
- 对于名为ID的属性,如果类名为Product,就可以将此属性重命名为ProductID,那么这个属性是主键
EF Core 注解特性
约定通常不足以将类完全映射到数据库
可以向模型添加更多只能特性的简单方法就是应用注解特性
例如数据库中的名称中的最大长度为 40个字符,并不能为空
ProductName NVARCHAR(40) NOT NULL
Description "NTEXT"
在类中,可以应用特性来指定名称的长度和不能为空
[Required]
[StringLength(40)]
public string ProductName {get; set;}
.NET类型和数据库类型之间没有明显的映射时,可以使用特性加上映射关系
[Column(TypeName = "ntext")]
public string Description { get; set;}
EF Core Fluent API
使用Fluent API可以替代注解特性,也可以用来作为特性的补充
使用Fluent API具有绝对的优先级
将注解特性在数据库上下文类的onModelCreating方法中替换为等效的FluentAPI语句
比如:
[Required]
[StringLength(40)]
public string ProductName {get; set;}
可以替换为
modelBuilder.Entity<Procuct>()
.Property(procucts => procucts.ProductName)
.IsRequired()
.HasMaxLength(40);
数据播种
可以使用Fluent API提供初始数据以填充数据库
例如如果想要确保新数据库在Product表中至少有一行,调用HasData方法
modelBuilder.Entity<Category>()
.HasData(new Procuct{ProcuctID = 1,ProcuctName = "Bob",Cost = 8.99M })
创建实体类(产品和产品类别)
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
/// <summary>
/// 类别
/// </summary>
public class Category
{
[Key] //主键
public int categoryID{ get ; set ; }
public string categoryName{ get ; set ; }
[Column(TypeName ="ntext")] //定义类型为 NTEXT
public decimal? Description{ get ; set ; }
/// <summary>
/// 定义导航属性,一对多定义 virtual定义可以使用延迟加载 允许继承覆盖属性并提供额外的特性
/// </summary>
/// <value></value>
public virtual ICollection<Procuct> Procucts {get;set;}
public Category(){
//若要使开发人员能够将产品添加到类别,我们必须将导航属性初始化为空集合
this.Procucts = new HashSet<Procuct>();
}
}
/// <summary>
/// 产品
/// </summary>
public class Procuct
{
[Key] //这里可以不用声明 因为类目加 ID默认就是主键
public int ProcuctID { get;set;}
[Required]
[StringLength(40)]
public string ProcuctName {get; set;}
[Column("UnitPrice",TypeName ="money")] //将属性命名为 UnitPrice 类型为 money
public decimal? Cost {get; set;}
[Column("UnitsInStock")]
public short? Stock {get; set;}
public bool Discoutinued {get; set;}
public int categoryID {get; set;}
/// <summary>
/// 允许覆盖属性 提供额外的特性
/// </summary>
/// <value></value>
public virtual Category Category {get; set;}
}
创建DBContext上下文类
上下文类必须继承 DbContext类,并且在类中声明实体的DbSet属性
using System;
using Microsoft.EntityFrameworkCore;
public class MyDBContext :DbContext
{
public DbSet<Procuct> procucts {get;set;}
public DbSet<Category> categories {get;set;}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){
//server=LAPTOP-89N28DCH;database = 数据库名称;uid=;pwd=
optionsBuilder.UseSqlServer("Server=jlf;Database=CodeFirstTest;Trusted_Connection=True;"); //Window身份验证
}
protected override void OnModelCreating(ModelBuilder modelBuilder){
//这里写 Fluent API语句
}
}
迁移
迁移常用命令
VSCode迁移命令 | VS 迁移命令 | 说明 |
---|---|---|
dotnet ef migrations add InitialCreate | Add-Migration InitialCreate | 添加迁移信息 ,在项目中创建一个名为“Migrations”的目录,并生成一些文件 |
dotnet ef database update | Update-Database | 将最近添加的迁移执行到数据库 |
dotnet ef migrations remove | Remove-Migration | 将最近的的迁移删除(应避免将已更新数据库的迁移删除) |
dotnet ef dbcontext info | 获取DBContext上下文信息 | |
dotnet ef migrations list | Get-Migration | 列出所有迁移 |
使用迁移
dotnet ef migrations add InitialCreate
在终端执行以上命令生成迁移后会生成以下几个迁移信息文件
dotnet ef database update
这时执行update命令即可将迁移执行到数据库
后续在开发过程中必然会更改实体模型,按照以上步骤先 add 后 update更新数据库
使用DBFirst方式构建模型(反向工程)
这时需要先创建数据库表!
Category产品类别 & Products 产品
然后看下边的语句
dotnet ef dbcontext scaffold
"Server=jlf;Database=Test;Trusted_Connection=True;" //数据库连接字符串,在构建语句直接声明,系统会警告不安全,应把连接字符串放到web.config里
Microsoft.EntityFrameworkCore.SqlServer //数据库提供者
--table Categories --table Products //需要生成那些表
--output-dir AutoGenModels //生成的文件夹
--namespace WorkingWithEFCore.AutoGenModels //命名空间
--data-annotations //使用数据注解和FluentAPI
--context MyContext //上下文别名
这时可以看到,AutoGenModels文件夹中生成了三个文件,包括上下文
需要注意:
- EFCore使用InverseProperty属性来表示外键
- EFCore中INDEX特性来指明那些字段拥有索引
- dotnet-ef目前不能使用可空引用类型
- 类是使用 partial声明的,这样就可以通过创建partial类来添加额外的代码
查询EFCore模型
现在有了使用SqlServer数据库映射来的模型,可以使用一些简单的linq查询来获取数据了
简单查询实体
using Microsoft.EntityFrameworkCore;
using System.Linq;
using(var db = new MyContext()){
IQueryable<Category> cats = db.Categories;
foreach (Category item in cats)
{
System.Console.WriteLine(item.CategoryName);
}
}
使用using对上下文类进行封装
查询导航属性
using(var db = new MyContext()){
IQueryable<Category> cats = db.Categories.Include(c=>c.Products); //使用 Include 访问Categories类中的导航属性Products
foreach (Category item in cats)
{
System.Console.WriteLine($"{item.CategoryName} 类别有 {item.Products.Count()} 个产品");
}
}
获取实体时 只是 db.Categories获取不到实体中的导航属性,也就是外键关系的表的数据,需要使用 Include 来获取
过滤导航属性中的数据
IQueryable cats = db.Categories.Include(c => c.Products.Where(p => p.UnitsInStock > 100)); //过滤产品中的库存数据 >100
排序和过滤
using(var db = new MyContext()){
decimal price = 100.23M;
//过滤UnitPrice大于100的数据,并根据UnitPrice倒序排序
IQueryable<Product> products = db.Products.Where(p => p.UnitPrice > price).OrderByDescending(p => p.UnitPrice);
foreach (var item in products)
{
System.Console.WriteLine($"{item.ProductId},{item.ProductName},{item.UnitPrice}");
}
}
获取生成的SQL(EFCore 5.0)
在上边方法中加入 以下代码输出生成的SQL
System.Console.WriteLine(products.ToQueryString());
控制台输入的就是查询的sql
记录EFCore操作
- 定义两个类,一个实现 ILoggerProvider 接口,一个实现ILogger接口
- 各自实现接口的方法
- ConsoleLoggerProvider类会返回ConsoleLogger实例,因此没有任何非托管资源,Dispose不需要实现
- ConsoleLogger 类的log方法将日志写入控制台
using Microsoft.Extensions.Logging;
public class ConsoleLoggerProvider : ILoggerProvider
{
//创建
public ILogger CreateLogger(string categoryName)
{
return new ConsoleLogger();
}
public void Dispose()
{
//throw new NotImplementedException();
}
}
public class ConsoleLogger : ILogger
{
//开启逻辑操作的作用域
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
//检查是否启用了给定的 logLevel。 (那些日志信息需要跟踪)本方法将 Trace Information None 排除
public bool IsEnabled(LogLevel logLevel)
{
switch(logLevel){
case LogLevel.Trace: //日志的详细信息 ?
case LogLevel.Information: //跟踪应用程序的常规流的日志
case LogLevel.None:
return false;
case LogLevel.Debug:
case LogLevel.Warning: //
case LogLevel.Error: //错误
case LogLevel.Critical: // 描述不可恢复的应用程序或系统崩溃或灾难性事件的日志 需要立即关注的故障
return true;
default:
return true;
}
}
//
/// <summary>
/// 写入日志 这里暂时控制台输出 应该写入文件
/// </summary>
/// <param name="logLevel">日志级别 LogLevel 枚举中的值</param>
/// <param name="eventId">事件ID 比如LINQ转SQL查询的事件ID是 20100</param>
/// <param name="state">要写入的条目</param>
/// <param name="exception">与此条目相关的异常</param>
/// <param name="formatter"></param>
/// <typeparam name="TState"></typeparam>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
//if(eventId.Id = 20100) 可以指定事件去处理日志,20100是linq转sql
System.Console.WriteLine($"级别:{logLevel} 事件ID:{eventId.Id}");
if(state != null){
System.Console.WriteLine($"状态:{state}");
}
if(exception != null){
System.Console.WriteLine($"异常:{state}");
}
}
}
然后在数据库上下文的using快中添加语句获取日志工厂,并注册自定义控制台日志记录器
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using(var db = new MyContext()){
var loggerFactory = db.GetService<ILoggerFactory>(); //创建日志工厂
loggerFactory.AddProvider(new ConsoleLoggerProvider()); //注册自定义日志方法
///下边 写排序或者过滤之类的LINQ方法
}
使用查询标记进行日志批注
IQueryable<Product> products = db.Products.TagWith("日志记录")
.Where(p => p.UnitPrice > price).OrderByDescending(p => p.UnitPrice)
EF Core2.2引入了查询标记特性,以允许向日志中添加SQL注释
模式匹配 LIKE
using (var db = new MyContext()){
// var loggerFactory = db.GetService<ILoggerFactory>();
// loggerFactory.AddProvider(new ConsoleLoggerProvider()); //注册日志
System.Console.WriteLine("输入产品名称:");
string input = Console.ReadLine();
IQueryable<Product> products = db.Products.Where(p => EF.Functions.Like(p.ProductName,$"%{input}%"));
foreach (var item in products)
{
System.Console.WriteLine($"id:{item.ProductId},名称:{item.ProductName}");
}
}
提示用户输入产品名称,然后使用 EF.Functions.Like 方法搜索 p.ProductName 属性的任何位置
定义全局过滤器
modelBuilder.Entity<Product>().HasQueryFilter(p => p.ProductName.Contains("Chang"));
在数据库上下文的OnModelCreating方法中声明以下语句,过滤出Product中的ProductName中包含Chang的数据
这时再执行上边的模糊查询,同时也会筛选条件内数据
EFCore加载模式
EFCore通常使用三种加载模式:延迟加载、立即加载、显示加载
立即加载实体
立即加载就是将实体类中的相关数据(包括外键表)一次性查询出来
使用Include查询可以实现立即加载
using(var db = new MyContext()){
IQueryable<Category> cats = db.Categories.Include(c=>c.Products); //使用 Include 访问Categories类中的导航属性Products
foreach (Category item in cats)
{
System.Console.WriteLine($"{item.CategoryName} 类别有 {item.Products.Count()} 产品");
}
}
可以将Include方法后的代码注释一下执行看,这里查询是获取不到导航属性的
延迟加载
延迟加载: 每当我们尝试读取导航属性时,延迟加载将检查它们是否已加载,如果没有加载,就立即执行sql语句加载它们,将当前的导航属性加载出,其余导航属性不动,然后返回导航属性查出来的数据
使用代理的延迟加载 (全局)
使用全局代理的延迟加载,必须每个导航属性带有 virtual修饰
public virtual Category Category { get; set ; }
- 为代理引用NuGet包
dotnet add package Microsoft.EntityFrameworkCore.Proxies --version 5.0.0
- 配置延迟代理以使用代理
在数据库上下文OnConfiguring中输入 optionsBuilder.UseLazyLoadingProxies() - 使用时应该避免使用循环处理导航属性
避免使用下边注释的代码循环处理导航属性,应一次性将数据取出,防止多次循环数据库
using(var db = new MyContext()){
IQueryable<Category> cats = db.Categories; //.Include(c=>c.Products); //使用 Include 访问Categories类中的导航属性Products
ICollection<Product> items = cats.FirstOrDefault().Products;
// var product = items.Products;
// foreach (Category item in cats)
// {
// System.Console.WriteLine($"{item.CategoryName} 类别有 {item.Products.Count} 产品");
// }
}
不使用代理的延迟加载
-
使用依赖注入的方式为单个对象使用延迟加载,这里的导航属性没有必须使用virtual修饰
-
首先需要导入Abstractions包
-
然后将ILazyLoader 注入到类中,使用LazyLoader .Load()方法检查实体,并在使用时更新实体数据,将实体使用ref 引用出
dotnet add package Microsoft.EntityFrameworkCore.Abstractions --version 5.0.0
using Microsoft.EntityFrameworkCore.Infrastructure;
public class Blog
{
private ICollection<Post> _posts;
public Blog()
{
}
private Blog(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts
{
get => LazyLoader.Load(this, ref _posts);
set => _posts = value;
}
}
public class Post
{
private Blog _blog;
public Post()
{
}
private Post(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog
{
get => LazyLoader.Load(this, ref _blog);
set => _blog = value;
}
}
这里借用 官网的例子
显示加载
显示加载与延迟加载的工作方式类似,不同之处在于显示加载可以控制加载那些相关数据以及何时加载
using(var db = new MyContext()){
IQueryable<Category> cats = db.Categories;
db.ChangeTracker.LazyLoadingEnabled = false; //禁用延迟加载
//返回唯一元素 (如果存在多个则会异常)
var categorySingle = db.Categories.Single(b => b.CategoryName == "Beverages");
/// <summary>
/// Entry:获取提供的实体,提供对实体操作的跟踪
/// Collection :将此实体与其他实体相关联的属性
/// Load 加载实体中的数据
/// </summary>
/// <returns></returns>
db.Entry(categorySingle).Collection(c => c.Products).Load();
System.Console.WriteLine($"{categorySingle.CategoryName} has {categorySingle.Products.Count}");
}
显示加载主要在于Load方法
/// <summary>
///categorySingle 提供一个符合条件的实体
/// Entry:获取提供的实体,提供对实体操作的跟踪
/// Collection :将此实体与其他实体相关联的属性
/// Load 加载实体中的数据
/// </summary>
/// <returns></returns>
db.Entry(categorySingle).Collection(c => c.Products).Load();
使用EF Core 操作数据
操作数据就相对简单了很多,DbContext能够自动维护更改和跟踪,因此本地实体可以跟踪多个更改,包括添加新实体,修改实体和删除实体
插入实体
using(var db = new MyContext()){
Product product = new Product{
ProductId = 78,
ProductName = "Bob",
UnitPrice = 2.3M
};
db.Products.Add(product);
if(db.SaveChanges() == 1){
System.Console.WriteLine("新增成功");
}
}
修改实体
using(var db = new MyContext()){
Product updateProduct = db.Products.FirstOrDefault(p => p.ProductName =="Chang");
updateProduct.UnitPrice += 1M;
if(db.SaveChanges() == 1){
System.Console.WriteLine("修改成功");
}
}
删除实体
using(var db = new MyContext()){
Product updateProduct = db.Products.FirstOrDefault(p => p.ProductName.StartsWith("C"));
db.Products.RemoveRange(updateProduct);
int affected = db.SaveChanges();
if(affected == 1){
System.Console.WriteLine("删除成功");
}
}
池化数据库上下文
事务
每次调用 SaveChanges方法时,都会启动隐式事务,以便出现问题时回滚所有的修改
事务通过应用锁来防止在发生一系列更改时进行读写操作,从而维护数据库的完整性
定义显示事务
using(var db = new MyContext()){
using(IDbContextTransaction t = db.Database.BeginTransaction()){ //开始事务
Product updateProduct = db.Products.FirstOrDefault(p => p.ProductName.StartsWith("C"));
db.Products.RemoveRange(updateProduct);
int affected = db.SaveChanges();
t.Commit(); //提交事务
if(affected == 1){
System.Console.WriteLine("删除成功");
}
}
}
该文章是学习过程中的记录,知识基本都摘自书中
还望指点错误,以及其他应学习知识点 !