03-EF Core筆記之查詢數據
- 2019 年 10 月 6 日
- 筆記
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();