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