[Abp 源碼分析]七、倉儲與 Entity Framework Core

来源:https://www.cnblogs.com/myzony/archive/2018/07/24/9359249.html
-Advertisement-
Play Games

0.簡介 Abp 框架在其內部實現了倉儲模式,並且支持 EF Core 與 Dapper 來進行資料庫連接與管理,你可以很方便地通過註入通用倉儲來操作你的數據,而不需要你自己來為每一個實體定義單獨的倉儲的實現,通用倉儲包含了常用的 CRUD 介面和一些常用方法。 例如: 1.通用倉儲定義與實現 在 ...


0.簡介

Abp 框架在其內部實現了倉儲模式,並且支持 EF Core 與 Dapper 來進行資料庫連接與管理,你可以很方便地通過註入通用倉儲來操作你的數據,而不需要你自己來為每一個實體定義單獨的倉儲的實現,通用倉儲包含了常用的 CRUD 介面和一些常用方法。

例如:

public class TestAppService : ITransientDependency
{
    private readonly IRepository<TestTable> _rep;
    
    // 註入通用倉儲
    public TestAppService(IRepository<TestTable> rep)
    {
        _rep = rep;
    }
    
    public void TestMethod()
    {
        // 插入一條新數據
        _rep.Insert(new TestTable{ Name = "TestName" });
    }
}

1.通用倉儲定義與實現

在 Abp 內部,倉儲的基本定義存放在 Abp 項目的 Domain/Repositories 內部,包括以下幾個文件:

文件名稱 作用描述
AbpRepositoryBase.cs 倉儲基類
AutoRepositoryTypesAttribute.cs 自動構建倉儲,用於實體標記
IRepository.cs 倉儲基本介面定義
IRepositoryOfTEntity.cs 倉儲介面定義,預設主鍵為 int 類型
IRepositoryOfTEntityAndTPrimaryKey.cs 倉儲介面定義,主鍵與實體類型由用戶定義
ISupportsExplicitLoading.cs 顯式載入
RepositoryExtensions.cs 倉儲相關的擴展方法

1.1 通用倉儲定義

綜上所述,倉儲的基礎定義是由 IRepository 決定的,這個介面沒什麼其他用處,就如同 ITransientDependency 介面與 ISingletonDependency 一樣,只是做一個標識作用。

真正定義了倉儲介面的是在 IRepositoryOfTEntityAndTPrimaryKey<TEntity, TPrimaryKey> 內部,他的介面定義如下:

public interface IRepository<TEntity, TPrimaryKey> : IRepository where TEntity : class, IEntity<TPrimaryKey>
{
    // CRUD 方法
}

可以看到,他有兩個泛型參數,第一個是實體類型,第二個是實體的主鍵類型,並且約束了 TEntity 必須實現了 IEntity<TPrimaryKey> 介面,這是因為在倉儲介面內部的一些方法需要得到實體的主鍵才能夠操作,比如修改與查詢方法。

在 Abp 內部還有另外一個倉儲的定義,叫做 IRepository<TEntity> ,這個介面就是預設你的主鍵類型為 int類型,一般很少使用 IRepository<TEntity, TPrimaryKey> 更多的還是用的 IRepository<TEntity>

1.2 通用倉儲的實現

在 Abp 庫裡面,有一個預設的抽象基類實現了倉儲介面,這個基類內部主要註入了 IUnitOfWorkManager 用來控制事務,還有 IIocResolver 用來解析 Ioc 容器內部註冊的組件。

本身在這個抽象倉儲類裡面沒有什麼實質性的東西,它只是之前 IRepository<TEntity> 的簡單實現,在 EfCoreRepositoryBase 類當中則才是具體調用 EF Core API 的實現。

public class EfCoreRepositoryBase<TDbContext, TEntity, TPrimaryKey> : 
    AbpRepositoryBase<TEntity, TPrimaryKey>,
    ISupportsExplicitLoading<TEntity, TPrimaryKey>,
    IRepositoryWithDbContext
    
    where TEntity : class, IEntity<TPrimaryKey>
    where TDbContext : DbContext
{
    /// <summary>
    /// 獲得資料庫上下文
    /// </summary>
    public virtual TDbContext Context => _dbContextProvider.GetDbContext(MultiTenancySide);

    /// <summary>
    /// 具體的實體表
    /// </summary>
    public virtual DbSet<TEntity> Table => Context.Set<TEntity>();

    // 資料庫事務
    public virtual DbTransaction Transaction
    {
        get
        {
            return (DbTransaction) TransactionProvider?.GetActiveTransaction(new ActiveTransactionProviderArgs
            {
                {"ContextType", typeof(TDbContext) },
                {"MultiTenancySide", MultiTenancySide }
            });
        }
    }

    // 資料庫連接
    public virtual DbConnection Connection
    {
        get
        {
            var connection = Context.Database.GetDbConnection();

            if (connection.State != ConnectionState.Open)
            {
                connection.Open();
            }

            return connection;
        }
    }

    // 事務提供器,用於獲取已經激活的事務
    public IActiveTransactionProvider TransactionProvider { private get; set; }
    
    private readonly IDbContextProvider<TDbContext> _dbContextProvider;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="dbContextProvider"></param>
    public EfCoreRepositoryBase(IDbContextProvider<TDbContext> dbContextProvider)
    {
        _dbContextProvider = dbContextProvider;
    }
}

其實從上方就可以看出來,Abp 對於每一個倉儲都會重新打開一個資料庫鏈接,在 EfCoreRepositoryBase 裡面的 CRUD 方法實際上都是針對 DbContext 來進行的操作。

舉個例子:

// 插入數據
public override TEntity Insert(TEntity entity)
{
    return Table.Add(entity).Entity;
}

// 更新數據
public override TEntity Update(TEntity entity)
{
    AttachIfNot(entity);
    Context.Entry(entity).State = EntityState.Modified;
    return entity;
}

// 附加實體狀態
protected virtual void AttachIfNot(TEntity entity)
{
    var entry = Context.ChangeTracker.Entries().FirstOrDefault(ent => ent.Entity == entity);
    if (entry != null)
    {
        return;
    }

    Table.Attach(entity);
}

這裡需要註意的是 Update() 方法,之前遇到過一個問題,假如我傳入了一個實體,它的 ID 是不存在的,那麼我將這個實體傳入 Update() 方法之後執行 SaveChanges() 的時候,會拋出 DbUpdateConcurrencyException 異常。

正確的操作是先使用實體的 ID 去查詢資料庫是否存在該條記錄,存在再執行 Update() 操作。

這裡 AttachIfNot 作用是將實體附加到追蹤上下文當中,如果你之前是通過 Get() 方法獲取實體之後更改了某個實體,那麼在調用 Context.ChangeTracker.Entries() 方法的時候會獲取到已經發生變動的身體對象集合。

1.3 通用倉儲的註入

倉儲的註入操作發生在 AbpEntityFrameworkCoreModule 模塊執行 Initialize() 方法的時候,在 Initialize() 方法內部調用了 RegisterGenericRepositoriesAndMatchDbContexes() 方法,其定義如下:

private void RegisterGenericRepositoriesAndMatchDbContexes()
{
    // 查找所有資料庫上下文
    var dbContextTypes =
        _typeFinder.Find(type =>
        {
            var typeInfo = type.GetTypeInfo();
            return typeInfo.IsPublic &&
                    !typeInfo.IsAbstract &&
                    typeInfo.IsClass &&
                    typeof(AbpDbContext).IsAssignableFrom(type);
        });

    if (dbContextTypes.IsNullOrEmpty())
    {
        Logger.Warn("No class found derived from AbpDbContext.");
        return;
    }

    using (IScopedIocResolver scope = IocManager.CreateScope())
    {
        // 遍曆數據庫上下文
        foreach (var dbContextType in dbContextTypes)
        {
            Logger.Debug("Registering DbContext: " + dbContextType.AssemblyQualifiedName);

            // 為資料庫上下文每個實體註冊倉儲
            scope.Resolve<IEfGenericRepositoryRegistrar>().RegisterForDbContext(dbContextType, IocManager, EfCoreAutoRepositoryTypes.Default);

            // 為自定義的 DbContext 註冊倉儲
            IocManager.IocContainer.Register(
                Component.For<ISecondaryOrmRegistrar>()
                    .Named(Guid.NewGuid().ToString("N"))
                    .Instance(new EfCoreBasedSecondaryOrmRegistrar(dbContextType, scope.Resolve<IDbContextEntityFinder>()))
                    .LifestyleTransient()
            );
        }

        scope.Resolve<IDbContextTypeMatcher>().Populate(dbContextTypes);
    }
}

方法很簡單,註釋已經說的很清楚了,就是遍歷實體,通過 EfGenericRepositoryRegistrarEfCoreBasedSecondaryOrmRegistrar 來註冊倉儲。

來看一下具體的註冊操作:

private void RegisterForDbContext(
    Type dbContextType, 
    IIocManager iocManager,
    Type repositoryInterface,
    Type repositoryInterfaceWithPrimaryKey,
    Type repositoryImplementation,
    Type repositoryImplementationWithPrimaryKey)
{
    foreach (var entityTypeInfo in _dbContextEntityFinder.GetEntityTypeInfos(dbContextType))
    {
        // 獲取主鍵類型
        var primaryKeyType = EntityHelper.GetPrimaryKeyType(entityTypeInfo.EntityType);
        if (primaryKeyType == typeof(int))
        {
            // 建立倉儲的封閉類型
            var genericRepositoryType = repositoryInterface.MakeGenericType(entityTypeInfo.EntityType);
            if (!iocManager.IsRegistered(genericRepositoryType))
            {
                // 構建具體的倉儲實現類型
                var implType = repositoryImplementation.GetGenericArguments().Length == 1
                    ? repositoryImplementation.MakeGenericType(entityTypeInfo.EntityType)
                    : repositoryImplementation.MakeGenericType(entityTypeInfo.DeclaringType,
                                                               entityTypeInfo.EntityType);

                // 註入
                iocManager.IocContainer.Register(
                    Component
                    .For(genericRepositoryType)
                    .ImplementedBy(implType)
                    .Named(Guid.NewGuid().ToString("N"))
                    .LifestyleTransient()
                );
            }
        }

        // 如果主鍵類型為 int 之外的類型
        var genericRepositoryTypeWithPrimaryKey = repositoryInterfaceWithPrimaryKey.MakeGenericType(entityTypeInfo.EntityType,primaryKeyType);
        if (!iocManager.IsRegistered(genericRepositoryTypeWithPrimaryKey))
        {
            // 操作跟上面一樣
            var implType = repositoryImplementationWithPrimaryKey.GetGenericArguments().Length == 2
                ? repositoryImplementationWithPrimaryKey.MakeGenericType(entityTypeInfo.EntityType, primaryKeyType)
                : repositoryImplementationWithPrimaryKey.MakeGenericType(entityTypeInfo.DeclaringType, entityTypeInfo.EntityType, primaryKeyType);

            iocManager.IocContainer.Register(
                Component
                .For(genericRepositoryTypeWithPrimaryKey)
                .ImplementedBy(implType)
                .Named(Guid.NewGuid().ToString("N"))
                .LifestyleTransient()
            );
        }
    }
}

這裡 RegisterForDbContext() 方法傳入的這些開放類型其實是通過 EfCoreAutoRepositoryTypes.Default 屬性指定,其定義:

public static class EfCoreAutoRepositoryTypes
{
    public static AutoRepositoryTypesAttribute Default { get; }

    static EfCoreAutoRepositoryTypes()
    {
        Default = new AutoRepositoryTypesAttribute(
            typeof(IRepository<>),
            typeof(IRepository<,>),
            typeof(EfCoreRepositoryBase<,>),
            typeof(EfCoreRepositoryBase<,,>)
        );
    }
}

2.Entity Framework Core

2.1 工作單元

在之前的文章裡面說過,Abp 本身只實現了一個抽象工作單元基類 UnitOfWorkBase ,而具體的事務處理是存放在具體的持久化模塊裡面進行實現的,在 EF Core 這裡則是通過 EfCoreUnitOfWork 實現的。

首先看一下 EfCoreUnitOfWork 註入了哪些東西:

public class EfCoreUnitOfWork : UnitOfWorkBase, ITransientDependency
{
    protected IDictionary<string, DbContext> ActiveDbContexts { get; }
    protected IIocResolver IocResolver { get; }

    private readonly IDbContextResolver _dbContextResolver;
    private readonly IDbContextTypeMatcher _dbContextTypeMatcher;
    private readonly IEfCoreTransactionStrategy _transactionStrategy;

    /// <summary>
    /// 創建一個新的 EF UOW 對象
    /// </summary>
    public EfCoreUnitOfWork(
        IIocResolver iocResolver,
        IConnectionStringResolver connectionStringResolver,
        IUnitOfWorkFilterExecuter filterExecuter,
        IDbContextResolver dbContextResolver,
        IUnitOfWorkDefaultOptions defaultOptions,
        IDbContextTypeMatcher dbContextTypeMatcher,
        IEfCoreTransactionStrategy transactionStrategy)
        : base(
                connectionStringResolver,
                defaultOptions,
                filterExecuter)
    {
        IocResolver = iocResolver;
        _dbContextResolver = dbContextResolver;
        _dbContextTypeMatcher = dbContextTypeMatcher;
        _transactionStrategy = transactionStrategy;

        ActiveDbContexts = new Dictionary<string, DbContext>();
    }
}

emmm,他註入的基本上都是與 EfCore 有關的東西。

第一個字典是存放處在激活狀態的 DbContext 集合,第二個是 IIocResolver 用於解析組件所需要的解析器,第三個是資料庫上下文的解析器用於創建 DbContext 的,第四個是用於查找 DbContext 的 Matcher,最後一個就是用於 EF Core 事物處理的東東。

根據 UnitOfWork 的調用順序,首先看查看 BeginUow() 方法:

if (Options.IsTransactional == true)
{
    _transactionStrategy.InitOptions(Options);
}

沒什麼特殊操作,就拿著 UOW 對象的 Options 去初始化事物策略。

之後按照 UOW 的調用順序(PS:如果看的一頭霧水可以去看一下之前文章針對 UOW 的講解),會調用基類的 CompleteAsync() 方法,在其內部則是會調用 EF Core UOW 實現的 CompleteUowAsync() 方法,其定義如下:

protected override async Task CompleteUowAsync()
{
    // 保存所有 DbContext 的更改
    await SaveChangesAsync();
    // 提交事務
    CommitTransaction();
}

public override async Task SaveChangesAsync()
{
    foreach (var dbContext in GetAllActiveDbContexts())
    {
        await SaveChangesInDbContextAsync(dbContext);
    }
}

private void CommitTransaction()
{
    if (Options.IsTransactional == true)
    {
        _transactionStrategy.Commit();
    }
}

內部很簡單,兩句話,第一句話遍歷所有激活的 DbContext ,然後調用其 SaveChanges() 提交更改到資料庫當中。

之後呢,第二句話就是使用 DbContextdbContext.Database.CommitTransaction(); 方法來提交一個事務咯。

public void Commit()
{
    foreach (var activeTransaction in ActiveTransactions.Values)
    {
        activeTransaction.DbContextTransaction.Commit();

        foreach (var dbContext in activeTransaction.AttendedDbContexts)
        {
            if (dbContext.HasRelationalTransactionManager())
            {
                continue; //Relational databases use the shared transaction
            }

            dbContext.Database.CommitTransaction();
        }
    }
}

2.2 資料庫上下文提供器

這個玩意兒的定義如下:

public interface IDbContextProvider<out TDbContext>
    where TDbContext : DbContext
{
    TDbContext GetDbContext();

    TDbContext GetDbContext(MultiTenancySides? multiTenancySide );
}

很簡單的作用,獲取指定類型的資料庫上下文,他的標準實現是 UnitOfWorkDbContextProvider<TDbContext>,它依賴於 UOW ,使用 UOW 的 GetDbContext<TDbContext>() 方法來取得資料庫上下文。

整個關係如下:

2.3 多資料庫支持

在 Abp 內部針對多資料庫支持是通過覆寫 IConnectionStringResolver 來實現的,這個操作在之前的文章裡面已經講過,這裡僅講解它如何在 Abp 內部實現解析的。

IConnectionStringResolver 是在 EF 的 Uow 才會用到,也就是創建 DbContext 的時候:

public virtual TDbContext GetOrCreateDbContext<TDbContext>(MultiTenancySides? multiTenancySide = null)
    where TDbContext : DbContext
{
    var concreteDbContextType = _dbContextTypeMatcher.GetConcreteType(typeof(TDbContext));

    var connectionStringResolveArgs = new ConnectionStringResolveArgs(multiTenancySide);
    connectionStringResolveArgs["DbContextType"] = typeof(TDbContext);
    connectionStringResolveArgs["DbContextConcreteType"] = concreteDbContextType;
    // 這裡調用了 Resolver
    var connectionString = ResolveConnectionString(connectionStringResolveArgs);

    // 創建 DbContext
    dbContext = _transactionStrategy.CreateDbContext<TDbContext>(connectionString, _dbContextResolver);

    return (TDbContext)dbContext;
}

// 傳入了 ConnectionStringResolveArgs 裡面包含了實體類型信息哦
protected virtual string ResolveConnectionString(ConnectionStringResolveArgs args)
{
    return ConnectionStringResolver.GetNameOrConnectionString(args);
}

他這裡的預設實現叫做 DefaultConnectionStringResolver ,就是從 IAbpStartupConfiguration 裡面拿去用戶在啟動模塊配置的 DefaultNameOrConnectionString 欄位作為自己的預設資料庫連接字元串。

在之前的 文章 的思路也是通過傳入的 ConnectionStringResolveArgs 參數來判斷傳入的 Type,從而來根據不同的 DbContext 返回不同的連接串。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文源自:https://github.com/cnxy/Dapper-zh-cn 本博客作者與Github上作者(cnxy)實為同一個作者。由於筆者翻譯水平有限,文本中錯誤難免,歡迎指正! 本文翻譯自:StackExchange.Dapper 原版教程源自:Dapper Tutorial 中文教程 ...
  • class Program { //第一種(1-2)+(3-4)+(5-6)...+m public static void Test(int m) { int z = 0; if (m % 2 == 0) { z = -(m / 2); ... ...
  • 微軟的eshop項目寫的很牛,學起來也比較吃力,最近公司剛好有一本書,說的就是.NET微服務,記下來。 因為微軟對性能的要求,docker裡面要有記憶體要求 解釋下這句代碼的意思: 然後你就成功了.... ...
  • 問題: 這幾天在裝.NET 的開發環境,在裝好VS2013和Oracle 11g之後,做了一個測試項目,運行調試沒問題 但是涉及到資料庫相關操作,如新建數據集、連接資料庫等在調試的時候則會出現如下錯誤: 目前百度之後現存的解決方案: 1. oracle odp.net 32位/64位版本的問題 "解 ...
  • https://www.cnblogs.com/cmt/p/4692920.html ...
  • 因個人需求,需要將html格式轉換成PDF並加上水印圖片。於是乎第一次接觸這種需求的小菜鳥博主我,在某度搜索引擎上不斷的查閱關鍵字資料、踩坑,終於有了一個相應的解決方案。以下是解決步驟,記錄下來方便以後的回顧,以及各位大神們的品鑒。 1、在 NuGet 搜索 itextsharp 關鍵字 下載以下截 ...
  • 對.Net Core的學習和實踐,已經進行了一年多的世間,截止目前,微軟已經發佈.Net Core2.1,關於.NetCore的應用部署的文章比比皆是。今天藉此,回顧下.net core環境的部署過程。 首先,我這邊採用的是CentOS7+上的版本,.net core2.1。在動手前,我們先做這樣的 ...
  • 在很早之前就介紹過圖表插件Highcharts的使用了,在2014年的隨筆《基於MVC4+EasyUI的Web開發框架經驗總結(4)--使用圖表控制項Highcharts》,這裡基本上都介紹的比較完整,基本的設置也沒有太大的差異,本篇介紹的是基於Bootstrap開發框架的界面處理,以及對圖表插件Hi... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...