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