EF Core使用

118 阅读8分钟

EF Core连接不同数据库

MySql

Mysql.EntityFrameworkCore:官方、不开源、下载量相对较低

Pomelo.EntityFrameworkCore.MySql(推荐):开源开发者、一直活跃

var serverVersion = new MySqlServerVersion(new Version(5, 6, 0));
optionsBuilder.UseMySql(connectionString, serverVersion)

PostgreSQL

optionsBuilder.UseNpgsql(connectionString)

SQL Server

Microsoft.EntityFrameworkCore.SqlServer

optionsBuilder.UseSqlServer(connStr);

反向工程

反向工程类似DBFirst,根据数据库来反向生成实体类,适合通过已有数据库表来生成实体类。这样一定是一个贫血模型,不要使用!

命令

//Scaffold-DbContext 连接串 数据库类型 
//-force表示覆盖已有文件 
Scaffold-DbContext 'Data Source=N30001084B002\SQL;AttachDbFilename=D: \My Visual Studio\MySQLServer\MyInstance\DepartmentMIS.mdf; 	Integrated Security=False;Connect Timeout=30; 	User Id =yanwenlong;Password =nrec1234.' Microsoft.EntityFrameworkCore.SqlServer -force

注意

  1. 生成的实体类可能不能满足项目要求,可能需要手动修改或者增加配置,比如T_Books表名生成了TBook类
  2. FluentAPI方式把所有的表附加配置写到DbContext派生类的OnModelCreating中,而非单独的Config类
  3. 若再次运行反向工程工具,对文件所做的任何更改都将丢失

实体跟踪机制

代码查看快照跟踪机制的过程

//获得实体在EF CORE中的跟踪对象EntityEntry
EntityEntry e1=ctx.Entry(a1);
//State属性代表实体的状态
e1.State
//实体的变化信息(快照值和当前值对比)
e1.DebugView.LongView

取消实体跟踪

当我们只显示,不修改数据的时候,不需要跟踪。只需要在查询时加上AsNoTracking即可,这样对象状态为Detached

注意,这样对象被修改时不会被更新到数据库

var items=ctx.Articles.AsNoTracking().Take(3)

实体的状态

  • 已添加(Added):DbContext正在跟踪此实体,但数据库中尚不存在该实体
  • 未改变(Unchanged):DbContext正在跟踪此实体,该实体存在于数据库中,其属性值和数据库中读取到的值一致,未发生改变
  • 已修改(Modified):DbContext正在跟踪此实体,并存在于数据库中,并且其部分或全部属性值已修改
  • 已删除(Deleted):DbContext正在跟踪此实体,并存在于数据库中,但是在下次调用SaveChanges时要从数据库中删除对应数据
  • 已分离(Detached):DbContext未跟踪此实体,new一个对象,但是和DbContext未产生关系

SavaChanges操作对实体的影响

  • 已分离、未改变的实体,会忽略
  • 已添加的实体,会插入到数据库
  • 已修改的实体,会更新到数据库
  • 已删除的实体,会从数据库删除

状态跟踪机制的妙用

使用EF Core时,更改数据其实会执行两条SQL语句,先查询再更新

var a = ctx.Articles.Where(a => a.Id == 9).Single();//select
a.Price = 99;
ctx.SaveChanges();//update

可以采用状态跟踪,直接生成udpate、delete语句

Article a = new Article { Id = 6, Price = 99 };
var entry1=ctx.Entry(a);
entry1.Property("Price").IsModified = true;
ctx.SaveChanges();

Article a = new Article { Id = 6 };
ctx.Entry(a).State = EntityState.Deleted;
ctx.SaveChanges();

警告:

  1. 不推荐使用,因为会有坑。只要能看懂即可
  2. 当项目复杂后,代码可读性、可维护性不同,而且使用不当有可能造成不容易发现的Bug。带来的性能提升也是微乎其微的,因此不推荐使用,知道即可

EF Core对实体操作的秘密

EF Core不是通过实体对象的属性的get、set来进行属性的读写

基于性能和对充血模型支持的考虑,EF Core在读写属性的时候,如果可能,它会直接跳过get、set,而直接操作真正存储属性值的成员变量。EF Core在读写实体对象的属性时,会查找属性对应的成员变量,如果能找到,EF Core会直接读写这个成员变量的值,而不是通过set和get代码块来读写

EF Core会尝试按照命名规则去直接读写属性对应的成员变量

EF Core会尝试按照命名规则(具体的规则有很多)去直接读写属性对应的成员变量,只有无法根据命名规则找到对应成员变量的时候,EF Core才会通过属性的get、set代码块来读写属性值

应用场景:在充血模型中会用到

如何取消这种行为

可以在FluentAPI中通过UsePropertyAccessMode()方法来修改默认的这个行为,不建议修改

将IQueryable的数据一次性加载

什么时候需要将IQueryable的数据一次性加载?

1、遍历IQueryable并且进行数据处理的过程很耗时

2、如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回IQueryable的。必须一次性加载返回

//出了此函数,MyDbContext对象已经被销毁,而SQL还未执行,故无法得到返回值
static IQueryable<Article> QueryNotYZK() {
    using var ctx = new MyDbContext();
    return ctx.Articles.Where(a =>! a.Title.Contains("YZK"));
}

3、多个IQueryable的遍历嵌套

注意,很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。对于SQL Server连接字符串中加上MultipleActiveResultSets=true即可,其他数据库不支持这个

//会报错,因为同时遍历两个IQueryable,其实就是搞了两个DataReader
foreach(var a in ctx.Articles) {
    Console.WriteLine(a.Name);
    foreach(var c in ctx.Comments) {
        Console.WriteLine(c.Name);
    }
}

主键策略

EF Core支持多种主键生成策略:自动增长、Guid、Hi/Lo算法等

自增主键

自增字段的代码中不能为Id赋值,必须保持默认值0,否则运行的时候就会报错。

优点:简单

缺点:数据库迁移以及分布式系统中比较麻烦、并发性能差。

Guid主键

Guid算法生成一个全局唯一的Id。适合于分布式系统,在进行多数据库数据合并的时候很简单

优点:简单、高并发、全局唯一

缺点:磁盘空间占用大、搜索效率低

注意事项

  1. Guid值不连续。使用Guid类型做主键的时候,不能把主键设置为聚集索引。因为聚集索引是按顺序保存主键的,因此用Guid做主键性能差。若设为聚集索引则插入时,数据要重新排列,效率非常低
  2. 有的数据库支持部分的连续Guid,比如SQL Server中的NewSequentialId()可以生成部分连续Guid,但也不能解决问题
  3. MySQL中的InnoDB引擎中主键是强制使用聚集索引的,不要使用Guid做主键
  4. SQLServer对于Guid主键默认创建聚集索引,因此会造成每次插入新数据,都会数据库重排。因此我们取消主键的默认的聚集索引。Guid对应于uniqueidentifier类型
builder.HasKey(e => e.Id).IsClustered(false);

混合自增和Guid

不是复合主键

自增列做物理主键,而用Guid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据时候把Guid当主键用。在和其它表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列

优点:不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值被预测带来的安全性问题

Hi/Lo算法

EF Core支持Hi/Lo算法来优化自增列。主键值由两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。比如,服务端Hi位5000 5100,本地1 2,结果5001 5002。

不同进程或者集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证可以在本地高效率的生成主键值。

缺点:不是EF Core的标准,有的数据库不支持。但SQL Server支持

FluentApi 自定义配置扩展方法

值对象扩展

namespace Microsoft.EntityFrameworkCore.Metadata.Builders {
    //配置类的扩展,不是校验类。用于一些通用的配置
    public static class ConfigExtensions {
        public static EntityTypeBuilder<TEntity> OwnsOneRecordInfo<TEntity>(this EntityTypeBuilder<TEntity> entityTypeBuilder,
            Expression<Func<TEntity, RecordInfo>> navigationExpression, int maxLength = 200) where TEntity : class {
            entityTypeBuilder.OwnsOne(navigationExpression, dp => {
                dp.Property(c => c.UserName).IsRequired().IsUnicode(true).HasMaxLength(maxLength);
                dp.Property(c => c.DateTime).IsRequired().IsUnicode(false);
            });
            entityTypeBuilder.Navigation(navigationExpression).IsRequired();
            return entityTypeBuilder;
        }
    }
}
//调用时, EntityTypeBuilder<Equip> builder
builder.OwnsOneRecordInfo(e => e.RecordCreationInfo);

常见数据类型配置的扩展

//进度数据,decimal
public static PropertyBuilder IsMyProcess<TEntity>(this PropertyBuilder builder,
   int maxLength = 10) where TEntity : class {
    builder.IsRequired().IsUnicode(false).HasMaxLength(maxLength);
    return builder;
}
//int数据
public static PropertyBuilder IsMyInteger<TEntity>(this PropertyBuilder builder,
   int maxLength = 10) where TEntity : class {
    builder.IsRequired().IsUnicode(false).HasMaxLength(maxLength);
    return builder;
}
//日期数据DateTime
public static PropertyBuilder IsMyDateTime<TEntity>(this PropertyBuilder builder,
   int maxLength = 50) where TEntity : class {
    builder.IsRequired().IsUnicode(false).HasMaxLength(maxLength);
    return builder;
}
//普通中文字符串,非无限长
public static PropertyBuilder IsMyCHString<TEntity>(this PropertyBuilder builder, bool required = true,
   int maxLength = 200) where TEntity : class {
    builder.IsRequired(required).IsUnicode(true).HasMaxLength(maxLength);
    return builder;
}
//普通英文字符串,非无限长
public static PropertyBuilder IsMyENString<TEntity>(this PropertyBuilder builder, bool required = true,
   int maxLength = 200) where TEntity : class {
    builder.IsRequired(required).IsUnicode(false).HasMaxLength(maxLength);
    return builder;
}
//是枚举,(数据库存字符串,非数值)
public static PropertyBuilder IsMyEnum<TEntity>(this PropertyBuilder builder
   ) where TEntity : class {
    builder.IsRequired().IsUnicode().HasConversion<string>();
    return builder;
}
//是布尔
public static PropertyBuilder IsMyBoolean<TEntity>(this PropertyBuilder builder
   ) where TEntity : class {
    builder.IsRequired().IsUnicode(false);
    return builder;
}

自引用的实体关系配置

什么是自引用的组织结构树?

是一对多的特殊情况,同一类型的组织单元,可以有自己的父节点或子节点。是同一类型的一对多关系,即一个组织单元对应多个组织单元

class OrgUnit
{
	public long Id { get; set; }
	public string Name { get; set; }
	public OrgUnit Parent { get; set; }//父是自己的类型
	public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();//子也是自己的类型
}

builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsRequired().IsUnicode().HasMaxLength(100);
builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children);