C Sharp/实体框架

维基教科书,自由的教学读本

命令行工具[编辑]

首先安装dotnet-ef:

dotnet tool install --global dotnet-ef
dotnet ef DbContext scaffold "Filename=Northwind.db" Microsoft.EntityFrameworkCore.Sqlite --table Categories --table Products --output-dir AutoModel --namespace AutoModel --data-annotations --context Northwind

基本概念[编辑]

MVC[编辑]

实体是概念模式下的一个有意义的概念,可对应数据库中的一张表或多张表联合。

Model是实体的类定义。

EF Core的代码要遵守下面的约定:

  • 在运行时构建实体模型。
  • 实体类表示表的结构,类的实例表示表中的一行。
  • 表名和DbContext类中DbSet的属性名匹配;
  • 列名和类中的属性名匹配;
  • string类型和nvarchar匹配
  • 主键、外键字段的名字,一般是类名加上ID。名为ID的属性,可以将其重命名为类名+ID,然后假定这个是主键。如果这个属性是整数或者Guid类型,那就可以假定为IDENTITY类型。

约定还不足以完成对映射的搭建时,可借助C#的特性data annotation可以进一步帮助构建模型。例如:

[Required]
[StringLength(40)]
public string ProductName {get;set;}

[Column(TypeName = "money")]
public decimal? UnitPrice {get;set;}

Data Context[编辑]

DbContext类用于表示数据库,这个类知道怎么样和数据库通信,并且将C#代码转化为SQL语句,以便查询和操作数据。

在DbContext类里,必须有一些DbSet<T>属性,这些属性表示数据库中的表。为了表示每个表对应的类,DbSet使用泛型来指明类,这些类表示表中的一行,类的属性表示表中的列。

DbContext类里应该还包括OnConfiguring方法来链接数据库。OnModelCreating方法可以用来编写Fluent API语句来替代特性修饰实体类。

例如:

class Northwind : DbContext {
    private string DBPath = "./Northwind.db";
    
    public DbSet<Category> Categories {get;set;}
    public DbSet<Product> Products {get;set;}
    
    protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlite($"Data Source={DBPath}");
	
    //覆盖并实现OnModelCreating方法
    protected override void OnModelCreating(ModelBuilder model)
    {
        model.Entity<Category>()
            .Property(c => c.CategoryName)
            .IsRequired()
            .HasMaxLength(15)
            .HasQueryFilter(p => !p.Discontinued);
        model.Entity<Product>()
            .Property(p => p.Cost)
            .HasConversion<double>();
    }
}

表达式树[编辑]

下例把一颗表达式树中的加法改为减法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;  
using System.Linq.Expressions;
namespace ConsoleApp
{

    public class OperationsVisitor : ExpressionVisitor
    {
        public Expression Modify(Expression expression)
        {
            return Visit(expression);
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            if (b.NodeType == ExpressionType.Add)
            {
                Expression left = this.Visit(b.Left);
                Expression right = this.Visit(b.Right);
                return Expression.Subtract(left, right);
            }

            return base.VisitBinary(b);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Expression<Func<int, int, int>> lambda = (a, b) => a + b * 2;

            var operationsVisitor = new OperationsVisitor();
            Expression modifyExpression = operationsVisitor.Modify(lambda);
        }
    }
}

查询[编辑]

EF Core使用LINQ to Entities做查询。

前置概念[编辑]

var user = new User(); //局部变量的隐式类型化

var user = new User() //对象初始化指在实例化对象时,即可对对象的属性进行赋值
 {
     ID = Guid.NewGuid(),
     Account = "Apollo"
 };

var user = new { ID = Guid.NewGuid(), Name = "Apollo" };//匿名类型指的是不显示声明类型的细节,而是根据上下文环境需要,临时声明满足需要的类型。由于该类型是临时需要的,所以不必为之命名。

//扩展方法是微软为扩展已有框架而创造的一个语法糖。被扩展的对象可以不知道扩展方法的存在,就能在行为上得到扩展。
public static class UserExt
 {
     public static void Drink(this User user, object water)
     {
         //…
     }
 }

//Lambda表达式是由委托及匿名方法发展而来的,它可将表达式或代码块(匿名方法)赋给一个变量,从而以最少量的输入实现编码目的。
//Lambda表达式一般配合IEnumerable<T>的静态扩展方法使用,完成对象集合的快捷查询操作。
var user = db.Users.FirstOrDefault(o => o.Account == "Apollo");

//System.Linq.Enumerable静态类声明了一套标准查询操作符(Standard Query Operators,SQO)方法集合。标准查询操作符的语法和标准SQL很相似。基本语法如下:
using (var db = new EntityContext())
 {
     var roles = from o in db.Users
                where o.Account == "Apollo"
                select o.Roles;
     
}
//编译器会将上述表达式转化为下列以Lambda表达式为参数的显式的扩展方法调用序列:
using (var db = new EntityContext())
 {
     var roles = db.Users.Where(o => o.Account == "Apollo").Select(o => o.Roles);
 }

流利语法[编辑]

使用LINQ的流利语法。例如:

modelBuilder.Entity<Product>()
    .Property(p => p.ProductName)
    .IsRequired()
    .HasMaxLength(40);

流利API[编辑]

在DbContext的OnModelCreating中,使用ModelBuilder参数实例,可以配置比数据标示属性更多的选项。其优先级为:

Fluent API > data annotations > 缺省习惯.
public partial class StoreDBContext : DbContext
{
    public virtual DbSet<OrderDetails> OrderDetails { get; set; }
    public virtual DbSet<Orders> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<OrderDetails>(entity =>
        {
            entity.HasKey(e => e.OrderDetailId);
            entity.HasIndex(e => e.OrderId);
            entity.Property(e => e.OrderDetailId).HasColumnName("OrderDetailID");
            entity.Property(e => e.OrderId).HasColumnName("OrderID");
            entity.Property(e => e.ProductId).HasColumnName("ProductID");
            entity.HasOne(d => d.Order)
                .WithMany(p => p.OrderDetails)
                .HasForeignKey(d => d.OrderId);
        });

        modelBuilder.Entity<Orders>(entity =>
        {
            entity.HasKey(e => e.OrderId);
            entity.Property(e => e.OrderId).HasColumnName("OrderID");
            entity.Property(e => e.CustomerId).HasColumnName("CustomerID");
            entity.Property(e => e.EmployeeId).HasColumnName("EmployeeID");
        });
    }
}

常用的API[编辑]

Microsoft.EntityFrameworkCore.EF.Functions有很多常用函数,如相似比较EF.Functions.Like(p.ProductName,"%che%");

查询例子[编辑]

例如,使用实体的模型:

public class Customer
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public virtual List<Invoice> Invoices { get; set; }
}

列出表中所有数据:

using (var context = new MyContext())
{
    var customers = context.Customers.ToList();
}

列出单个实体:

using (var context = new MyContext())
{
    var customers = context.Customers
        .Single(c => c.CustomerId == 1);
}

过滤出名字是指定值的实体:

using (var context = new MyContext())
{
    var customers = context.Customers
        .Where(c => c.FirstName == "Mark")
        .ToList();
}

tracking[编辑]

Tracking行为跟踪每个实体的变化,并可在调用SaveChanges()方法时把实体的变化写回到数据库的对应行。如下例:

using (var context = new MyContext())
{
    var customer = context.Customers
        .Where(c => c.CustomerId == 2)
        .FirstOrDefault();

     customer.Address = "43 rue St. Laurent";
     context.SaveChanges();
}

对于只读的查询,可以在生成结果时使用AsNoTracking()方法。这叫做“Disconnected Entity”。

可以在数据上下文级别修改默认的tracking行为:

using (var context = new MyContext())
{
    context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    var customers = context.Customers.ToList();
}

多表查询:Include和ThenInclude[编辑]

举例,有3个Model:

public class Customer
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public virtual List<Invoice> Invoices { get; set; }
}

public class Invoice
{
    public int InvoiceId { get; set; }
    public DateTime Date { get; set; }
    public int CustomerId { get; set; }
    [ForeignKey("CustomerId")]
    public Customer Customer { get; set; }
    public List<InvoiceItem> Items { get; set; }
}

public class InvoiceItem
{
    public int InvoiceItemId { get; set; }
    public int InvoiceId { get; set; }
    public string Code { get; set; }
    [ForeignKey("InvoiceId")]
    public virtual Invoice Invoice { get; set; }
}

下面是从客户关联出其所有的发表,使用Include方法:

using (var context = new MyContext())
{
    var customers = context.Customers
        .Include(c => c.Invoices)
        .ToList();
}

如果需要多级的表的关联,即drilldown,则使用ThenInclude()方法。例如,从客户不仅需要知道其所有发票,还需要知道发票明细:

using (var context = new MyContext())
{
    var customers = context.Customers
        .Include(i => i.Invoices)
            .ThenInclude(it => it.Items)
        .ToList();
}

加载关联数据,可使用Collection和Reference方法:

  • Collection针对关联属性是一个类的集合
  • Reference针对关联属性是一个单个的类

这种方式查询每次只能查询一个导航属性。特别的,仅追踪模式下支持这种查询,非追踪模式下不支持此种查询。

多表查询:Join和GroupJoin[编辑]

var query = context.Customers //表1
    .Join(
        context.Invoices, //表2
        customer => customer.CustomerId,//表1的键
        invoice => invoice.Customer.CustomerId,//表2的键
        (customer, invoice) => new  //表1的行与表2的行配对后,番号匿名类型的实例
        {
            InvoiceID = invoice.Id,
            CustomerName = customer.FirstName + "" + customer.LastName,
            InvoiceDate = invoice.Date
        }
    ).ToList();

foreach (var invoice in query)
{
    Console.WriteLine("InvoiceID: {0}, Customer Name: {1} " + "Date: {2} ",
        invoice.InvoiceID, invoice.CustomerName, invoice.InvoiceDate);
}

//另外一个例子:
var query = context.Invoices
    .GroupJoin(
        context.InvoiceItems,
        invoice => invoice,
        item => item.Invoice,
        (invoice, invoiceItems) =>
            new
            {
                InvoiceId = invoice.Id,
                Items = invoiceItems.Select(item => item.Code)
            }
        ).ToList();

foreach (var obj in query)
{
    Console.WriteLine("{0}:", obj.InvoiceId);
    foreach (var item in obj.Items)
    {
        Console.WriteLine("  {0}", item);
    }
}

事务[编辑]

using var t = _context.Database.BeginTransaction();
var product = _context.Products.Single(p => p.ProductID == 2);
product.Cost += 10;
_context.SaveChanges();
t.Commit();

常见自己的LINQ扩展方法[编辑]

public static class NewLinqExtensions
{

    public static int Midian(this IEnumerable<int> sequence)
    {
        var ordered = sequence.OrderBy(item => item);
        var midianNum = ordered.Count() / 2;
        return ordered.ElementAt(midianNum);
    }

    public static int Midian<T>(this IEnumerable<T> sequence, Func<T, int> selector)
    {
        return sequence.Select(selector).Midian();
    }
}

全局过滤器[编辑]

在数据上下文创建时直接过滤。例如:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>().HasQueryFilter(c => !c.IsDeleted);
    }
}

//临时关闭全局过滤器:
using (var context = new MyContext())
{
    var customers = context.Customers
        .IgnoreQueryFilters().ToList();
}

显式加载[编辑]

使用Include或ThenInclude是Eager loading。

显式加载(explicit loading)作为导航属性的相关数据(related data),可以通过DbContext.Entry()方法。例如:

using (var context = new MyContext())
{
    var customer = context.Customers
        .Single(c => c.CustomerId == 1);

    context.Entry(customer)
        .Collection(c => c.Invoices)
        .Load();
}

//也可以用一个LINQ查询表示导航属性的内容,再过滤其内容:
using (var context = new MyContext())
{
    var customer = context.Customers
        .Single(c => c.CustomerId == 1);

    context.Entry(customer)
        .Collection(c => c.Invoices)
        .Query()
        .Where(i => i.Date >= new DateTime(2018, 1, 1))
        .ToList();
}

惰性加载[编辑]

惰性加载(Lazy loading),是对导航属性的访问透明加载。首先安装Microsoft.EntityFrameworkCore.Proxies,在UseSqlServer前面调用UseLazyLoadingProxies():

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchoolContext>(options =>
                 options
                      .UseLazyLoadingProxies()
                      .UseSqlServer/*UseMySQL*/(Configuration.GetConnectionString("DefaultConnection")));
}

原生SQL查询[编辑]

例如:

 
using (var context = new MyContext())
{
    var customers = context.Customers
        .FromSql("Select * from dbo.Customers where FirstName = 'Andy'")
        .OrderByDescending(c => c.Invoices.Count)
        .ToList();

//也可用这种方式调用存储过程,并使用存储过程的参数:
    int customerId = 1;
    var customer = context.Customers
        .FromSql($"EXECUTE dbo.GetCustomer {customerId}")
        .ToList();

//ExecuteSqlCommand()方法也可以调用存储过程,返回被影响的行数:
    int affectedRows = context.Database.ExecuteSqlCommand("CreateCustomer @p0, @p1, @p2",
        parameters: new[] 
        {
            "Elizabeth",
            "Lincoln",
            "23 Tsawassen Blvd."
        });
}

注意事项:

  • FromSql()方法的SQL查询必须返回实体类型的所有属性。
  • 结果集的列名必须匹配属性映射到的列名。
  • SQL查询不能包含相关数据。所以不能随后使用Include运算符。
  • 提供的SQL被处理为子查询,不应该包含子查询中无效的任何字符或选项,如尾部的分号。
  • 非SELECT的SQL语句自动被识别为不可复合的。因此,存储过程的完整结果总是返回给客户、FromSql之后的任何LINQ运算符总是在内存中求值。

迁移[编辑]

迁移(Migration)是在概念模型改变后,希望数据库随之变化但保持数据。

创建最初的数据库,首先执行命令:

PM> Add-Migration InitialCreate

其中InitialCreate是你指定的一个名字。在项目的Migrations文件夹下将创建3个文件:

  • _InitialCreate.cs: 这是主要的迁移文件,包含了Up()方法和逆操作Down()方法。
  • _InitialCreate.Designer.cs: 迁移元数据文件。
  • MyContextModelSnapshot.cs: 这是模型快照,用于在下次迁移时确定有哪些改变。

随后把上述迁移应用到数据库,从而创建schema:

PM> Update-Database

在客户实体中增加一个电话号码属性,为使数据库同步,执行:

PM> Add-Migration AddPhoneNumber

则创建迁移文件。再执行命令:

PM> Update-Database

有时你增加了一个迁移文件,但在应用它之前希望放弃这次迁移文件。这可以执行命令:

PM> Remove-Migration

如果你已经应用了几次迁移,但希望逆转回之前的某个版本,可以执行命令:

PM> Update-Database LastGoodMigration