EF Core使用Linq進行數據查詢。
基本查詢
微軟提供了一百多個示例來演示查詢,地址:https://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b
我們可以通過下面的代碼進行簡單的查詢:
//獲取全部數據
var blogs = context.Blogs.ToList();
//獲取單個實體
var blog = context.Blogs.Single(b => b.BlogId == 1);
//篩選
var blogs = context.Blogs
.Where(b => b.Url.Contains("dotnet"))
.ToList();
加載關聯數據
EF Core有三種常見模型來加載關聯數據:
- 預先加載:表示從數據庫中加載關聯數據,作為初始查詢的一部分
- 顯式加載:表示稍后從數據庫中顯式加載關聯數據
- 延遲加載:表示在訪問關聯數據時,再從數據庫中加載關聯數據
預先加載
使用Include
方法指定要包含在查詢結果中的關聯數據。例如:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.Include(blog => blog.Owner)
.ToList();
}
關聯數據可以是有層級的,可通過鏈式調用ThenInclude
,進一步包含更深級別的關聯數據。:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ThenInclude(author => author.Photo)
.Include(blog => blog.Owner)
.ThenInclude(owner => owner.Photo)
.ToList();
}
如果更改查詢,從而使其不再返回查詢以之為開頭的實體類型的實例,則會忽略 include 運算符。例如:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.Select(blog => new
{
Id = blog.BlogId,
Url = blog.Url
})
.ToList();
}
此時EF Core會忽略包含,並生成警告日志。
顯式加載
通過 DbContext.Entry(...) API 顯式加載導航屬性。例如:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}
延遲加載
使用延遲加載的最簡單方式是通過安裝 Microsoft.EntityFrameworkCore.Proxies 包,並通過調用 UseLazyLoadingProxies 來啟用該包。 例如:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString);
或者在ServiceConfigure中,調用services.AddDbContext方法時啟用:
services.AddDbContext<BloggingContext>(
b => b.UseLazyLoadingProxies()
.UseSqlServer(myConnectionString));
EF Core 延遲加載需要屬性必須具有是共有的,且具有virtual修飾符,只有這樣才可以被子類重寫。為何要這樣做,可以參考我之前的文章《Castle DynamicProxy基本用法(AOP)》。
下面的代碼演示了延遲加載的用法:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public virtual Blog Blog { get; set; }
}
此時EF Core會使用代理類進行延遲加載數據。
EF Core還提供了不使用代理的方式進行延遲加載,此方法需要向實體類中注入ILazyLoader
實例,並通過該實例實現get訪問:
public class Blog
{
private ICollection<Post> _posts;
public Blog()
{
}
private Blog(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
private ILazyLoader LazyLoader { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts
{
get => LazyLoader.Load(this, ref _posts);
set => _posts = value;
}
}
此種方法需要注入ILazyLoader,從而造成更多的包依賴。
使用EF Core延遲加載,可能會造成循環引用,此時無法使用Json.Net進行序列化,需要對此進行一些配置:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
}
客戶端 vs. 服務器
EF Core支持部分查詢在客戶端進行、部分查詢發送到服務器,此種情況下可能會造成性能問題。
當發生客戶端篩選數據的時候,EF Core會發出警告,也可以配置當發生客戶端篩選時拋出異常:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;")
.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning));
}
跟蹤和非跟蹤
默認情況下,EF Core跟蹤查詢返回的實體,如果我們不需要跟蹤查詢返回的實體,則可以通過AsNoTracking
方法禁用跟蹤。
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.AsNoTracking()
.ToList();
}
或者在DbContext級別禁用跟蹤:
using (var context = new BloggingContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var blogs = context.Blogs.ToList();
}
當使用投影查詢結果時,如果包含實體類型,則會對實體類型執行跟蹤,例如下面的查詢,將會對Blog和Post進行跟蹤:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Select(b =>
new
{
Blog = b,
Posts = b.Posts.Count()
});
}
另外,如果查詢結果中不包含任何實體類型,則不執行跟蹤。例如:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Select(b =>
new
{
Id = b.BlogId,
Url = b.Url
});
}
原始SQL查詢
當Linq無法滿足查詢需求,或因為使用Linq生成效率比較低的SQL查詢時,可以考慮使用原始SQL進行查詢。EF Core支持原始SQL語句和存儲過程。
原始SQL語句:
var blogs = context.Blogs
.FromSql("SELECT * FROM dbo.Blogs")
.ToList();
存儲過程:
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogs")
.ToList();
參數傳遞
當使用原始SQL進行查詢時,必須使用參數化查詢以抵御SQL注入攻擊。
好的一點是,EF Core在設計時就替我們考慮了如何防御SQL注入攻擊,因此當我們使用FromSql方法時,參數中如果有使用到拼接字符串的情況,則會自動為我們生成SQL查詢參數,例如:
var user = "johndoe";
var blogs = context.Blogs
.FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser {user}")
.ToList();
上面的SQL語句雖然看上去像是直接拼接的字符串,其實EF Core已經為我們生成了查詢參數。
當然了,我們也可以手工創建查詢參數:
var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogsForUser @user", user)
.ToList();
當數據庫的存儲過程使用了命名參數時,手工創建查詢參數將會派上用場:
var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogs @filterByUser=@user", user)
.ToList();
拼接Linq
當我們使用原始SQL查詢時,EF Core仍然支持我們使用linq編寫查詢語句。在執行查詢時,EF Core會檢查我們的sql語句是否支持拼接,如果支持的情況下,則會將linq過濾語句拼接為sql一並發送到數據庫進行查詢。
跟蹤
原始SQL中的跟蹤與Linq查詢的跟蹤方式一致。
關聯數據
原始SQL中查詢關聯數據的方式與Linq查詢的關聯方式一致。
全局篩選器
全局篩選器對於軟刪除和多租戶非常有用。定義方式如下:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");
// Configure entity filters
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}
我們可以在特定的查詢中禁用全局篩選器:
blogs = db.Blogs
.Include(b => b.Posts)
.IgnoreQueryFilters()
.ToList();