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();