03-EF Core筆記之查詢數據


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM