记录一下学习EFCore中的基础知识

安装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 InitialCreateAdd-Migration InitialCreate添加迁移信息 ,在项目中创建一个名为“Migrations”的目录,并生成一些文件
dotnet ef database updateUpdate-Database将最近添加的迁移执行到数据库
dotnet ef migrations removeRemove-Migration将最近的的迁移删除(应避免将已更新数据库的迁移删除)
dotnet ef dbcontext info获取DBContext上下文信息
dotnet ef migrations listGet-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("删除成功");
        }
        
    }
}

官方说明

该文章是学习过程中的记录,知识基本都摘自书中
还望指点错误,以及其他应学习知识点 !

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值