文章实体类Article、评论实体类Comment。一篇文章对应多条评论。
实体类:
public class Article
{
public long Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Comment> Comments { get; set; }
= new List<Comment>();
}
public class Comment
{
public long Id { get; set; }
public Article Article { get; set; }
public string Message { get; set; }
}
关系配置: EF Core中实体之间关系的配置的套路: HasXXX(…).WithXXX(…); 有XXX、反之带有XXX。 XXX可选值One、Many。
一对多:HasOne(…).WithMany(…);
一对一:HasOne(…).WithOne (…);
多对多:HasMany (…).WithMany(…);
class ArticleConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("T_Articles");
builder.Property(a => a.Content).IsRequired().IsUnicode();
builder.Property(a => a.Title).IsRequired().IsUnicode().HasMaxLength(255);
}
}
class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comments");
builder.HasOne<Article>(c=>c.Article).WithMany(a => a.Comments).IsRequired();
builder.Property(c=>c.Message).IsRequired().IsUnicode();
}
}
class TestDbContext : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connStr = "Server=.;Database=demo3;Trusted_Connection=True";
optionsBuilder.UseSqlServer(connStr);
optionsBuilder.LogTo(Console.WriteLine);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
Article a1 = new Article();
a1.Title = "微软发布.NET 20的首个预览";
a1.Content = "微软昨日在一篇官网博客文章中宣布了 .NET 20 首个预览版本的到来。";
Comment c1 = new Comment() { Message = "支持" };
Comment c2 = new Comment() { Message = "微软太牛了" };
Comment c3 = new Comment() { Message = "火钳刘明" };
a1.Comments.Add(c1);
a1.Comments.Add(c2);
a1.Comments.Add(c3);
using TestDbContext ctx = new TestDbContext();
ctx.Articles.Add(a1);
await ctx.SaveChangesAsync();
不需要显式为Comment对象的Article属性赋值(当前赋值也不会出错),也不需要显式地把新创建的Comment类型的对象添加到DbContext中。EF Core会自动识别。
Article a = ctx.Articles.Include(a=>a.Comments).Single(a=>a.Id==1); //生成的是left join
Console.WriteLine(a.Title);
foreach(Comment c in a.Comments)
{
Console.WriteLine(c.Id+":"+c.Message);
}
// 通过Comments查询Article也是相同的写法,生成的是inner join
Include定义在Microsoft.EntityFrameworkCore
命名空间中。
var a1 = dbContext.Articles.Select(x => new {x.Id, x.Title}).First();
1、在实体类中显式声明一个外键属性。
2、关系配置中通过HasForeignKey(c=>c.ArticleId)
指定这个属性为外键。
class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comments");
builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments)
.IsRequired().HasForeignKey(c => c.ArticleId);
builder.Property(c => c.Message).IsRequired().IsUnicode();
}
}
3、除非必要,否则不用声明,因为会引入重复(冗余)。
由一个属性可以访问到另外一种类型的实体叫做导航属性
单向导航:不设置反向的属性,然后配置的时候WithMany()不设置参数即可。
//请假配置类
class LeaveConfig : IEntityTypeConfiguration<Leave>
{
public void Configure(EntityTypeBuilder<Leave> builder)
{
builder.ToTable("T_Leaves");
//申请人
builder.HasOne<User>(l => l.Requester).WithMany();
//审批人
builder.HasOne<User>(l => l.Approver).WithMany();
builder.Property(l => l.Remarks).HasMaxLength(1000).IsUnicode();
}
}
using TestDbContext ctx = new TestDbContext();
User u = await ctx.Users.SingleAsync(u => u.Name == "张三");
foreach (var l in ctx.Leaves.Where(l => l.Requester == u))
{
Console.WriteLine(l.Remarks);
}
如果表属于被很多表引用的基础表,则用单向导航属性,否则可以自由决定是否用双向导航属性。
class OrgUnit
{
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; }
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}
builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsRequired().IsUnicode().HasMaxLength(100);
builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children);
//订单实体
class Order
{
public long Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public Delivery Delivery { get; set;}
}
//快递单实体
class Delivery
{
public long Id { get; set; }
public string CompanyName { get; set; }
public String Number { get; set; }
public Order Order { get; set; }
public long OrderId { get; set; }
}
OrderConfig:
builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d=>d.OrderId);
class Student
{
public long Id { get; set; }
public string Name { get; set; }
public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}
class Teacher
{
public long Id { get; set; }
public string Name { get; set; }
public List<Student> Students { get; set; } = new List<Student>();
}
StudentConfig:
builder.ToTable("T_Students");
builder.HasMany<Teacher>(s => s.Teachers).WithMany(t=>t.Students).UsingEntity(j=>j.ToTable("T_Students_Teachers"));
//执行迁移后,会自动生成T_Students_Teachers中间表
查询评论中含有“微软”的所有的文章:
ctx.Articles.Where(a=>a.Comments.Any(c=>c.Message.Contains("微软")));
//变换成另一种写法。
ctx.Comments.Where(c => c.Message.Contains("微软"))
.Select(c => c.Article).Distinct();
同样效果的代码可能有多种写法,有时候要关注底层的SQL,看哪种方式最好。
查询“所有由蜗牛快递负责的订单信息”:
ctx.Orders.Where(o=>o.Delivery.CompanyName== "蜗牛快递");
普通集合的版本(IEnumerable)是在内存中过滤(客户端评估),而IQueryable版本则是把查询操作翻译成SQL语句(服务器端评估)
IQueryable<Book> books = ctx.Books;
books.Where(b => b.Price > 1.1)
IEnumerable<Book> books = ctx.Books;
books.Where(b => b.Price > 1.1)
1、IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它 没有立即执行,只是“可以被执行”而已。
2、对于IQueryable接口调用非终结方法的时候不会执行查询,而 调用终结方法的时候则会立即执行查询。
3、终结方法:遍历、ToArray()
、ToList()
、Min()
、Max()
、Count()
等;
4、非终结方法:GroupBy()
、OrderBy()
、Include()
、Skip()
、Take()
等。
5、简单判断:一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法,否则就是终结方法。
IQueryable是一个待查询的逻辑,因此它是可以被重复使用的
IQueryable<Book> books = ctx.Books.Where(b => b.Price <= 8);
Console.WriteLine(books.Count());
Console.WriteLine(books.Max(b=>b.Price));
var books2 = books.Where(b=>b.PubTime.Year>2000); //执行条件:Price <= 8 && > PubTime.Year>2000
1、DataReader:分批从数据库服务器读取数据。内存占用小、 DB连接占用时间长; 2、DataTable:把所有数据都一次性从数据库服务器都加载到客户端内存中。内存占用大,节省DB连接。
IQueryable内部是在调用DataReader 如果需要一次性加载数据到内存:用IQueryable的
ToArray()
、ToArrayAsync()
、ToList()
、ToListAsync()
等方法。
using TestDbContext ctx = new TestDbContext();
foreach (Articles a in ctx.Articles)
{
//DataReader分批读取
Console.WriteLine(a.Id + ":" + a.Title);
foreach (Comment c in ctx.Comments)
{
Console.WriteLine(c.Id + ":" + c.Message);
}
}
using TestDbContext ctx = new TestDbContext();
foreach (Articles a in ctx.Articles.ToArray())
{
//一次性查询出来数据
Console.WriteLine(a.Id + ":" + a.Title);
foreach (Comment c in ctx.Comments.ToArray())
{
Console.WriteLine(c.Id + ":" + c.Message);
}
}
Tips:很多数据库的 ADO.NET Core Provider
是不支持多个DataReader同时执行的。这时候就需要使用终结方法查询。
SaveChanges()
、SaveChangesAsync()
、AddAsync()
、AddRangeAsync()
、AllAsync()
、AnyAsync
、AverageAsync
、ContainsAsync
、CountAsync
、FirstAsync
、FirstOrDefaultAsync
、ForEachAsync
、LongCountAsync
、MaxAsync
、MinAsync
、SingleAsync
、SingleOrDefaultAsync
、SumAsync
等
1、ToListAsync()
、ToArrayAsync()
。结果集不要太大。
2、await foreach (Book b in ctx.Books.AsAsyncEnumerable())
使用
dbCtx.Database.ExecuteSqlInterpolated ()
dbCtx.Database.ExecuteSqlInterpolatedAsync()
方法来执行原生的非查询SQL语句
ctx.Database.ExecuteSqlInterpolatedAsync(@$"insert into T_Books(Title,PubTime,Price,AuthorName)
select Title, PubTime, Price,{aName} from T_Books
where Price > {price}");
字符串内插 如果赋值给string变量,就是字符串拼接;字符串内插如果赋值给FormattableString
变量,编译器就会构造FormattableString
对象。ExecuteSqlInterpolatedAsync()
的参数是FormattableString
类型。因此ExecuteSqlInterpolatedAsync
会进行参数化SQL的处理,故不会造成sql注入。
如果要执行的原生SQL是一个查询语句,并且查询的结果也能对应一个实体,就可以调用对应实体的DbSet的
FromSqlInterpolated()
方法来执行一个查询SQL语句,同样使用字符串内插来传递参数。
IQueryable<Book> books = ctx.Books.FromSqlInterpolated(@$"select * from T_Books
where DatePart(year,PubTime)>{year}
order by newid()");
//FromSqlInterpolated()方法的返回值是IQueryable类型的,因此我们可以在实际执行IQueryable之前,对IQueryable进行进一步的处理。
foreach(Book b in books.Skip(3).Take(6))
{
}
局限性:
方式1、dbCxt.Database.GetDbConnection()
获得ADO.NET Core的数据库连接对象
DbConnection conn = ctx.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
{
conn.Open();
}
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = @"xxx";
var p1 = cmd.CreateParameter();
p1.ParameterName = "@year";
p1.Value = year;
cmd.Parameters.Add(p1);
using (var reader = cmd.ExecuteReader())
}
方式2、用Dapper等框架执行原生复杂查询SQL
快照更改跟踪:首次跟踪一个实体的时候,EF Core 会创建这个实体的快照。执行
SaveChanges()
等方法时,EF Core将会把存储的快照中的值与实体的当前值进行比较。
实体的状态:
SaveChanges()
的操作:
查看实体的状态:
使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntry。EntityEntry类的State属性代表实体的状态,通过DebugView.LongView属性可以看到实体的变化信息。
DbContext会根据跟踪的实体的状态,在SaveChanges()
的时候,根据实体状态的不同,生成Update、Delete、Insert等SQL语句,来把内存中实体的变化更新到数据库中。
如果通过DbContext查询出来的对象只是用来展示,不会发生状态改变,则可以使用AsNoTracking()
来 “禁用跟踪”。
Tips:如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking()
,就能降低内存占用。
常规更新需要先查询、再更新,两条SQL。利用修改实体状态可以直接更新数据库
Book b1 = new Book {Id=10};//跟踪通过Id定位
b1.Title = "zhangsan";
var entry1 = ctx.Entry(b1);
entry1.Property("Title").IsModified = true;
Console.WriteLine(entry1.DebugView.LongView);
ctx.SaveChanges();
常规删除需要先查询、再删除,两条SQL。利用修改实体状态可以直接删除
Book b1 = new Book { Id = 28 };
ctx.Entry(b1).State = EntityState.Deleted;
ctx.SaveChanges();
Tips:修改实体状态带来的性能提升微乎其微,对代码可读性、可维护性不强,代码可读性、可维护性不强。
全局查询筛选器:EF Core 会自动将这个查询筛选器应用于涉及这个实体类型的所有 LINQ 查询。
使用场景:软删除、多租户
builder.HasQueryFilter(b=>b.IsDeleted==false);
忽略全局查询筛选器:
ctx.Books.IgnoreQueryFilters().Where(b => b.Title.Contains("o")).ToArray()
表达式树(Expression Tree):树形数据结构表示代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
//Expression<TDelegate>类型
//从Lambda表达式来生成表达式树:
Expression<Func<Book, bool>> e1 = b =>b.Price > 5;
//普通委托:Func<Book, bool> e = b => b.Price > 5;
Expression对象储存了运算逻辑,它把运算逻辑保存成抽象语法树(AST),可以在运行时动态获取运算逻辑。而普通委托则没有。
1、ExpressionTreeVisualizer VS插件
2、nuget:Install-Package ExpressionTreeToString
Expression<Func<Book, bool>> e = b => b.AuthorName.Contains("牛炸了")||b.Price>30;
Console.WriteLine(e.ToString("Object notation", "C#"));
生成和如下硬编码的C#代码一样的表达式树:
Expression<Func<Book, bool>> e = b =>b.Price > 5;
ParameterExpression paramB = Expression.Parameter(typeof(Book),"b");
MemberExpression exprLeft = Expression.MakeMemberAccess(paramB, typeof(Book).GetProperty("Price"));
ConstantExpression exprRight = Expression.Constant(5.0,typeof(double));
BinaryExpression exprBody = Expression.MakeBinary(ExpressionType.GreaterThan, exprLeft, exprRight);
Expression<Func<Book, bool>> expr1 = Expression.Lambda<Func<Book, bool>>(exprBody, paramB);
Add
:加法;AndAlso
:短路与运算;ArrayAccess
:数组元素访问;Call
:方法访问;Condition
:三元条件运算符;
Constant
:常量表达式;Convert
:类型转换;
GreaterThan
:大于运算符;
GreaterThanOrEqual
:大于或等于运算符;
MakeBinary
:创建二元运算;NotEqual
:不等于运算;
OrElse
:短路或运算;Parameter
:表达式的参数;
Tips:一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译器确定要操作的类名、属性等,所以才需要编写动态构建表达式树的代码。否则为了提高代码的可读性和可维护性,要尽量避免动态构建表达式树。而是用IQueryable的延迟执行特性来动态构造。
nuget安装:System.Linq.Dynamic.Core
1、System.Linq.Dynamic.Core 2、使用字符串格式的语法来进行数据操作
var query = db.Customers
.Where("City == @0 and Orders.Count >= @1", "London", 10)
.OrderBy("CompanyName")
.Select("new(CompanyName as Name, Phone)");