Skip to main content

6 EF Core

数据库迁移指令

安装dotnet ef

dotnet tool install --global dotnet-ef

生成迁移代码

dotnet ef migrations add "迁移名称"

更新到数据库中

dotnet ef database update ["迁移名称(可选, 回滚到指定迁移处)"]

删除最后一次迁移脚本

dotnet ef migrations remove

生成SQL代码

dotnet ef migrations script "迁移名称(起始, 默认是第一次迁移)" "迁移名称(结束, 默认是最后一次迁移)"

批量修改/删除

Nuget 包: Zack.EFCore.Batch(.NET 7 之前, .NET 7 之后已支持批量修改/删除)

Fluent API

  • 视图与实体类映射(不推荐使用)
    • builder.ToView("blogsView")
  • 表与实体类映射
    • builder.ToTable("T_Books")
  • 排除属性映射
    • builder.Ignore(b => b.Name2)
  • 配置列名
    • builder.Property(b => b.BlogId).HasColumnName("blog_id")
  • 配置列数据类型
    • builder.Property(b => b.BlogId).HasColumnType("varchar(200)")
  • 配置最大长度
    • builder.Property(b => b.Name).HasMaxLength(20)
  • 移除主键递增
    • builder.Property(b => b.Id).ValueGeneratedNever()
  •  配置不为空
    • builder.Property(b => b.BlogId).IsRequired()
  • 配置转换器
    • builder.Property(b => b.Currency).HasConversion<string>()
    • Currency 字段为枚举类型时, 保存到数据库中会转换成数字(枚举值)保存, 调用 HasConversion() 方法来实现保存数据库为字符串(枚举名)
  • 配置只读
    • private string? remark;
      public string? Remark => remark;
      
      builder.Property(p => p.Remark).HasField("remark")
    • 实现从数据库中读取到值, 在代码中只允许对该值进行读取, 而无法进行修改
  •  配置自定义列名
    • builder.Property("passwordHash").HasColumnName("PasswordHash")
  • 配置默认值
    • builder.Property(b => b.Name).HasDefaultValue("Def")
  • 配置主键
    • 默认把名字为 Id 或者"实体类型+Id"的属性作为主键, 可以用 HasKey() 来配置其它属性作为主键
    • builder.HasKey(b => b.Number)
    • 支持复合主键, 但不建议使用
  • 配置索引
    • builder.HasIndex(b => b.Url)
    • 支持复合索引, 如builder.HasIndex(b => new { b.FirstName, p.LastName } )
    • 默认情况下定义的索引不是唯一索引, 使用 IsUnique() 把索引设置为唯一索引
    • 还可以使用 IsClustered() 把索引设置为聚集索引

通过日志输出SQL语句的三种方式

// 使用简单日志
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
  //...
  optionsBuilder.UseLoggerFactory(_loggerFactory);
  //...
}


// 使用日志框架
using Microsoft.Extensions.Logging;

private static ILoggerFactory _loggerFactory = LoggerFactory.Create(p => p.AddConsole());

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
  //...
  optionsBuilder.UseLoggerFactory(_loggerFactory);
  //...
}


// 使用 ToQueryString
var query = ctx.Persons.AsQueryable();
// ...进行筛选
...
// 获取SQL语句
query.ToQueryString();

表的三种关系

一对多

在从表配置时使用(推荐使用): .HasOne(...).WithMany(...)

或在主表配置是使用: .HasMany(...).WithOne(...)

方法都传入参数后, 属于双向导航属性

如果只想获得外键ID而不要使用 Join 来进行额外一次的查询, 使用 HasForeignKey(p => p.外键ID属性) 来指定外键ID字段

使用单向导航属性(WithMany 不传参数即可): HasOne(p => p.xxx).WithMany();


双向导航属性: 如文章表和评论表的主从表关系

单向导航属性: 如用户表(基础表)和用户请假表的关系


特殊的一对多(自引用, 组织架构树)

public class OrgUnit
{
    public long Id { get; set; }

    public string Name { get; set; }

    public OrgUnit Parent { get; set; }

    public List<OrgUnit> Children { get; set; } = new();
}

public class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit>
{
    public void Configure(EntityTypeBuilder<OrgUnit> builder)
    {
        builder.ToTable("OrgUnit");

        builder.Property(p => p.Name).HasMaxLength(50).IsUnicode().IsRequired();

        // 根节点没有 Parent, 所以不能加 IsRequired()
        // 表示说, 有一个 父节点, 这个父节点拥有许多个 子节点
        builder.HasOne<OrgUnit>(p => p.Parent).WithMany(p => p.Children);
    }
}

一对一

.HasOne(...).WithOne(...).HasForeignKey<...>(...)

配置到哪个实体类中都没问题, 同时必须声明外键属性(其中一个实体类存在外键属性字段即可)

多对多

.HasMany(...).WithMany(...).UsingEntity(p => p.ToTable("xxx"))

配置到哪个实体类中都没问题, 同时需要指定中间关系表的表名

客户端评估和服务器评估

IQueryable<Order> query = ctx.Orders;

// (服务器评估)IQueryable 会将 Where 筛选数据转换成SQL语句(在数据库中进行操作)
IQueryable<Order> order1 = query.Where(p => p.Name == "HELLO");

// (客户端评估)IEnumerable 会导致数据全部查询出来, 然后才进行 Where 筛选(在内存中进行操作)
IEnumerable<Order> order2 = query.Where(p => p.Name == "HELLO");

// 使用order1... 使用...order2

应尽量避免使用客户端评估(但有些情况下使用客户端评估效率会更好)

异步遍历IQueryable

  • 使用 ToListAsync(), ToArrayAsync()
    • 推荐在结果集不大的情况下使用
  • 使用异步 foreach
    • await foreach(var item in ctx.Books.AsAsyncEnumerable())
  • 一般也不需要这样做异步遍历, 普通遍历即可(除非遇到性能瓶颈, 可以使用异步遍历来优化)

执行SQL语句

执行非查询SQL语句

int rowAffected = await ctx.Database.ExecuteSqlInterpolatedAsync($"SELECT * FROM Users WHERE Id = {id}");

ExecuteSqlInterpolatedAsync() 方法参数是 FormattableString 类型, 因此传参时会进行参数化处理, 不会造成 SQL 注入攻击

还有 ExecuteSqlRaw() 可以执行 SQL 语句, 但需要自己处理查询参数, 不推荐使用

执行和实体相关的SQL语句

var titlePattern = "%中%";
IQueryable<Order> orders = ctx.Orders.FromSqlInterpolated(@$"SELECT * FROM Order WHERE title LIKE {titlePattern}");

方法返回的是一个 IQueryable<T> 类型, 不会立即执行 SQL 语句, 因此没有异步方法

把只能用原生 SQL 语句写的逻辑用这个方法去执行, 对于之后的分页/分组/二次过滤/Include() 等其它逻辑尽可能使用 EF Core 标准操作去实现

 

同样地还有 FromSqlRaw() 方法, 也不推荐使用

局限性

  • SQL 查询必须返回实体类型对应数据库表的所有列
  • 结果集中的列名必须与属性映射到的列名相匹配
  • 只能单表查询, 不能使用 Join 语句进行关联查询, 但可以在查询后面使用 Include() 来进行关联数据的获取

执行任意SQL语句(使用ADO.NET)

using (DbCommand cmd = conn.CreateCommand())
{
    cmd.CommandText = "SELECT * FROM Order GROUP BY Id";
    using (var reader = await cmd.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            long id = reader.GetInt64(0);
            string title = reader.GetString(1);
            Console.WriteLine($"{id} - {title}");
        }
    }
}
不推荐使用, 如果实在需要执行这类语句, 推荐使用 Dapper 执行原生复杂 SQL

跟踪实体的状态

  • 已添加(Added)
  • 未改变(Unchanged)
  • 已修改(Modified)
  • 已删除(Deleted)
  • 分离(Detached)
var newOrder = new Order();
// 通过上下文的 Entry() 方法, 可以获取到状态, 同时还可以进行状态修改
EntityEntry entry = ctx.Entry(newOrder);
Console.WriteLine(entry.State);
// 输出实体快照信息和修改情况
Console.WriteLine(entry.DebugView.LongView);

如果查询出来的数据不需要进行修改, 可以调用 AsNoTracking() 方法让 EF 不跟踪实体, 提高性能

全局查询筛选器

// 在实体配置类中调用 HasQueryFilter()
builder.HasQueryFilter(p => p.IsDeleted == false);

// 如果在查询时不想要应用全局查询筛选器, 调用 IgnoreQueryFilters() 方法即可
ctx.Articles.IgnoreQueryFilters().Where(p => p.IsDeleted).AsNoTracking().ToList();

在查询时如果其它列是索引, 而全局查询筛选的列不是索引, 就有可能会有性能问题

并发控制

避免多个用户同时操作资源造成的并发冲突问题

最好的解决方案是使用非数据库解决方案

悲观并发控制

一般采用行锁, 表锁等排他锁对资源进行锁定, 确保同时只有一个使用者操作被锁定的资源

EF Core 没有封装悲观并发控制的使用, 需要编写原生 SQL 语句来使用悲观并发控制(不同数据库语法不同)

MySQL 进行行锁

SELECT * FROM Order WHERE Id = 1 FOR UPDATE

如果有其它查询操作也使用 for update 来查询这条数据时, 查询操作会被挂起, 直到针对这条数据的更新操作完成从而释放这个行锁, 其它查询操作才会继续执行 

锁是独占, 排他的, 当系统并发量很大时, 会严重影响性能, 还可能导致死锁

using (var tx = ctx.Database.BeginTransaction())
{
    // 启用事务, 然后通过FOR UPDATE在查询时进行加行锁, 完成事务之后释放锁
    var house = ctx.Houses.FromSqlInterpolated($"SELECT * FROM Houses WHERE Id = 1 FOR UPDATE").Single();

    house.Owner = "YOU";

    ctx.SaveChanges();
    tx.Commit();
}

乐观并发控制(并发令牌)

原理

UPDATE Order SET Owner='新值'
WHERE ... AND Owner='旧值'

当 Update 时, 如果数据库中的 Owner 值已经被其它操作者更新为其它值了, 那么 WHERE 语句的值就会为 FALSE, 因此这个 UPDATE 语句影响的行数就为 0, EF Core 就知道此时发生并发冲突, SaveChanges() 方法就会抛出 DbUpdateConcurrencyException 异常

使用示例

// 在实体配置类中指定列为并发令牌
builder.Property(p => p.Owner).IsConcurrencyToken();



// 乐观并发控制-使用并发令牌
var houseData = ctx.Houses.Find(1);
houseData.Owner = "YOU";

try
{
    ctx.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
    Console.WriteLine("出现并发冲突");
  
    var data = ex.Entries.First();
    var newValue = data.GetDatabaseValues().GetValue<string>("Owner");
    Console.WriteLine($"最新的值: {newValue}}");
}

SQL Server 的另一种并发令牌(ROWVERSION)

可以用 byte[] 类型的属性做并发令牌属性, 然后使用 IsRowVersion() 把这个属性设置为 ROWVERSION 类型(SQL Server 特有的列类型)

对于 ROWVERSION 类型的列, 在每次插入或更新时, 数据库会自动为这一列生成新值

在 SQL Server 中, Timestamp 和 ROWVERSION 是同一种类型的不同名称而已(在 SQL Server 中列的类型为 Timestamp)


乐观并发控制能够避免悲观锁带来的性能下降, 死锁等问题, 因此推荐使用乐观并发控制

如果使用 SQL Server, 可以采用 ROWVERSION 列

如果是在其它数据库中, 也可以指定列为 GUID 值, 在修改其它属性值的同时, 手动更新该列的值

// 在实体中添加 byte[] 类型的属性
public byte[] RowVer { get; set; }

// 在实体配置类中指定列为 ROWVERSION 列类型
builder.Property(p => p.RowVer).IsRowVersion();

表达式树

Expression 对象存储了运算逻辑, 它把运算逻辑保存成 AST(Abstract Syntax Tree, 抽象语法树), 可以在运行时动态分析运算逻辑

可以调用 Compile() 方法把 Expression 对象编译成 Func 对象

// 表达式树无法写成方法体
Expression<Func<Book, bool>> exp = book => book.Price > 10;
Func<Book, bool> func = book => book.Price > 10;

// Where() 方法返回类型是 IQueryable<>, 在 SQL 语句中会带有查询条件(服务器评估)
ctx.Books.Where(exp).ToArray();
// 此时 Where() 方法返回类型是 IEnumerable<>, 所以会直接查询出所有数据, 在内存中进行筛选操作(客户端评估)
ctx.Books.Where(func).ToArray();

查看表达式树

安装 Nuget 包: ExpressionTreeToString

Expression<Func<Book, bool>> exp = book => book.Price > 10 || book.Price < 20;
Console.WriteLine(exp.ToString("Object notation", "C#"));
// Console.WriteLine() 输出日志
var book = new ParameterExpression {
    Type = typeof(Book),
    IsByRef = false,
    Name = "book"
};

new Expression<Func<Book, bool>> {
    NodeType = ExpressionType.Lambda,
    Type = typeof(Func<Book, bool>),
    Parameters = new ReadOnlyCollection<ParameterExpression> {
        book
    },
    Body = new BinaryExpression {
        NodeType = ExpressionType.OrElse,
        Type = typeof(bool),
        Left = new BinaryExpression {
            NodeType = ExpressionType.GreaterThan,
            Type = typeof(bool),
            Left = new MemberExpression {
                Type = typeof(double),
                Expression = book,
                Member = typeof(Book).GetProperty("Price")
            },
            Right = new ConstantExpression {
                Type = typeof(double),
                Value = 10
            }
        },
        Right = new BinaryExpression {
            NodeType = ExpressionType.LessThan,
            Type = typeof(bool),
            Left = new MemberExpression {
                Type = typeof(double),
                Expression = book,
                Member = typeof(Book).GetProperty("Price")
            },
            Right = new ConstantExpression {
                Type = typeof(double),
                Value = 20
            }
        }
    },
    ReturnType = typeof(bool)
}

动态构建表达式树

使用 Expression 的工厂方法来得到节点对象

// 构建: book => book.Price > 5;
var bookParam = Expression.Parameter(typeof(Book), "book");

var constant5 = Expression.Constant(5.0, typeof(double));
var memberPrice = Expression.MakeMemberAccess(bookParam, typeof(Book).GetProperty(nameof(Book.Price)));
var greaterThanExpression = Expression.GreaterThan(memberPrice, constant5);

var lambdaExp = Expression.Lambda<Func<Book, bool>>(greaterThanExpression, bookParam);

Console.WriteLine(lambdaExp.ToString("Object notation", "C#"));

常用的表达式树工厂方法

工厂方法

说明

BinaryExpression Add(Expression left, Expression right) 加法运算, 比如 a + b
BinaryExpression AndAlso(Expression left, Expression right) 短路的与运算, 比如 a && b
IndexExpression ArrayAccess(Expression array, IEnumerable<Expression> indexes) 数组元素访问, 比如 items[5]
MethodCallExpression Call(...) 方法访问, 比如 s.Contains("zack")
ConditionalExpression Condition(Expression test, Expression ifTrue, Expression ifFalse) 三元条件运算符, 如 a == 1 ? "yang" : "zhongke"
ConstantExpression Constant(object value[, Type type]) 常量表达式, 比如 5
UnaryExpression Convert(Expression expression, Type type) 类型转换, 如 (int)count
BinaryExpression GreaterThan(Expression left, Expression right) 大于运算符, 如 a > 3
BinaryExpression GreaterThanOrEqual(Expression left,Expression right) 大于等于运算符, 如 a >= 3
BinaryExpression LessThan(Expression left, Expression right) 小于运算符, 如 a < 3
BinaryExpression LessThanOrEqual(Expression left, Expression right) 小于等于运算符, 如 a <= 3
BinaryExpression MakeBinary(ExpressionType binaryType, Expression left, Expression right) 创建二元运算, 通过 binaryType 参数指定运算符, 如 a + 5, 6 * a
BinaryExpression NotEqual(Expression left, Expression right) 不等于运算, 如 a != b
BinaryExpression OrElse(Expression left, Expression right) 短路或运算, 如 a || b
ParameterExpression Parameter(Type type, string name) 表达式的参数

更简单地构建表达式树

// Console.WriteLine(exp.ToString("Factory methods", "C#"))
// 通过这个输出, 可以直接写到代码中, 实现更加简单地构建表达式树

// 通过 using static [类名], 实现不需要写类名, 即可直接使用该类的静态方法
using static System.Linq.Expressions.Expression


// book => book.Price > 5
var bookParam = Parameter(
    typeof(Book),
    "book"
);

var exp = Lambda<Func<Book, bool>>(
    GreaterThan(
        MakeMemberAccess(bookParam,
            typeof(Book).GetProperty("Price")
        ),
        Constant(5.0, typeof(double))
    ),
    bookParam
);

Console.WriteLine(exp.ToString("Object notation", "C#"));

构建示例

// 动态 Where 参数查询
IEnumerable<Book> QueryBooks(string propertyName, object value)
{
    using (var ctx = new TestDbContext())
    {
        var bookParam = Parameter(
            typeof(Book),
            "book"
        );

        var property = typeof(Book).GetProperty(propertyName)!;
        var propertyType = property.PropertyType;
        var makeMemberAccess = MakeMemberAccess(bookParam, property);
        var constantExpression = Constant(System.Convert.ChangeType(value, propertyType));

        Expression expBody;

        if (value.GetType().IsPrimitive)
        {
            // 值类型
            expBody = Equal(
                left: makeMemberAccess,
                right: constantExpression
            );
        }
        else
        {
            // 引用类型
            expBody = MakeBinary(
                binaryType: ExpressionType.Equal,
                left: makeMemberAccess,
                right: constantExpression,
                liftToNull: false,
                method: typeof(string).GetMethod("op_Equality")
            );
        }

        Expression<Func<Book, bool>> exp = Lambda<Func<Book, bool>>(expBody, bookParam);

        // e => e.AuthorName == "John Doe";
        // Console.WriteLine(exp.ToString("Factory methods", "C#"));
        return ctx.Books.Where(exp).ToList();
    }
}



// 动态 Select 投影字段
IEnumerable<object[]> QuerySelect<T>(params string[] propertyNames) where T : class
{
    using (var ctx = new TestDbContext())
    {
        var param = Parameter(typeof(T));

        var propExprList = new List<Expression>();

        foreach (var propertyName in propertyNames)
        {
            Expression exp = Convert(MakeMemberAccess(param, typeof(T).GetProperty(propertyName)), typeof(object));

            propExprList.Add(exp);
        }

        var newArrayExp = NewArrayInit(typeof(object), propExprList);
        var selectExp = Lambda<Func<T, object[]>>(newArrayExp, param);

        // ctx.Set<T>().Select(p => new object[] { p.Price... }).ToList();
        // Console.WriteLine(selectExp.ToString("Factory methods", "C#"));
        return ctx.Set<T>().Select(selectExp).ToList();
    }
}

一般只有在编写不特定于某个实体类的通用框架的时候, 由于无法在编译器确定要操作的类名, 属性等, 所以才需要编写动态构建表达式树的代码. 否则为了提高代码的可读性和可维护性, 要尽量避免动态构建表达式树, 而是用 IQueryable 的延迟执行特性来动态构造

System.Linq.Dynamic.Core

安装 Nuget 包: ExpressionTreeToString

var priceL = 5;
var priceH = 600;
var name = "abc";
var books = ctx.Books.Where($"Price >= @0 and Price <=@1 and AuthorName=@2", priceL, priceH, name).Select<Book>("new(Price,Title,AuthorName)").ToList();
foreach (var book in books)
{
    Console.WriteLine($"{book.Price} - {book.Title} - {book.AuthorName}");
}