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