ABP 資料訪問 - IRepository 倉儲

五行缺碼發表於2020-10-29

ABP系列,這個系列來的比較晚,很多大佬其實已經分析過,為什麼現在我又來一輪呢?
1.想自己來完整的學習一輪ABP
2.公司目前正在使用ABP,準備遷移Core
基於以上的目的,開始這個系列 ?

ABP IRepository

先上 IRepository 類圖結構

只是描述了類的關聯關係,很多成員並不準確 ?

基於這個類圖,我們再來分析下ABP的倉儲訪問;

1.IRepository 整體結構

按照我的理解,可以簡單分為三部分;

1.整體介面以及抽象父類定義
2.自定義DbContext,Repository,實體
3.自動註冊實體倉儲

1.整體介面以及抽象父類定義

這部分內容整體包含在IRepository,IRepository<TEntity,TprimaryKey>,AbpRepositoryBase中,也就是圖中為包含在虛線框的內容;
IRepository:倉儲的介面,介面中未定義方方法
IRepository<TEntity, TPrimaryKey> :定義倉儲物件的相關查詢方法,GetAll(),Get()等方法
AbpRepositoryBase<TEntity, TPrimaryKey> :抽象類,封裝了一些公共方法但是並未有具體實現,實現留在了具體的呼叫層,例如 EF,EfCore,Dapper等

介面實現
EfCoreRepositoryBase<TDbContext, TEntity, TPrimaryKey>
實現AbpRepositoryBase<TEntity, TPrimaryKey>
1.EFCore的內部核心查詢全部就依賴於 DbContext,DbSet來運算元據;
2.EFCore的DbContext引用來源Microsoft.EntityFrameworkCore.DbContext,而Ef的DbContext依賴引用System.Data.Entity.DbContext,Core的底層依賴就全部替換了

AbpDbContext :ABP預設的EFCore的DBContext封裝,包含一些公共方法,要在ABP框架下使用DbContext,需要繼承 AbpDbContext

2.自定義DbContext,Repository,實體

實現DBContext

 public class SampleAppDbContext : AbpZeroDbContext<Tenant, Role, User, SampleAppDbContext>, IAbpPersistedGrantDbContext
    {
        public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }
        public DbSet<Advertisement> Advertisements { get; set; }
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Comment> Comments { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<ProductTranslation> ProductTranslations { get; set; }
        public DbSet<Author> Authors { get; set; }
        public DbSet<Store> Stores { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderTranslation> OrderTranslations { get; set; }
        public DbSet<UserTestEntity> UserTestEntities { get; set; }
        public DbSet<Country> Countries { get; set; }
        public SampleAppDbContext(DbContextOptions<SampleAppDbContext> options) 
            : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ConfigurePersistedGrantEntity();
            modelBuilder.Entity<Blog>().OwnsOne(x => x.More);
            modelBuilder.Entity<Blog>().OwnsMany(x => x.Promotions, b => 
            {
                b.WithOwner().HasForeignKey(bp => bp.BlogId);
                b.Property<int>("Id");
                b.HasKey("Id");
                b.HasOne<Blog>()
                 .WithOne()
                 .HasForeignKey<BlogPromotion>(bp => bp.AdvertisementId)
                 .IsRequired();
            });
            modelBuilder.Entity<Advertisement>().OwnsMany(a => a.Feedbacks, b =>
            {
                b.WithOwner().HasForeignKey(af => af.AdvertisementId);
               b.Property<int>("Id");
                b.HasKey("Id");
                b.HasOne<Comment>()
                 .WithOne()
                 .HasForeignKey<AdvertisementFeedback>(af => af.CommentId);
            });
            modelBuilder.Entity<Book>().ToTable("Books");
            modelBuilder.Entity<Book>().Property(e => e.Id).ValueGeneratedNever();
            modelBuilder.Entity<Store>().Property(e => e.Id).HasColumnName("StoreId");
        }
    }
}

DbContext中需要定義實體的DBSet,因為資料操作都是基於DbSet來完成

個性化倉儲
第一步,設定自定義倉儲介面

  public interface IPostRepository : IRepository<Post, Guid>
    {
    }

這裡繼承IRepository<Entity,PrimaryKey>,說明實體主鍵並非Int型別,所以需要重新實現

第二步,繼承 EfCoreRepositoryBase,實現自定義倉儲方法

 public class PostRepository : EfCoreRepositoryBase<BloggingDbContext, Post, Guid>, 
IPostRepository
    {
        public PostRepository(IDbContextProvider<BloggingDbContext> dbContextProvider)
            : base(dbContextProvider)
        {
        }
        public override int Count()
        {
            throw new Exception("can not get count of posts");
        }
    }

第三步,註冊自定義倉儲,註冊程式碼寫在自定義模組中
注意:自定義模組的註冊必須依賴 AbpEntityFrameworkCoreModule 模組先註冊 ❓ 這裡留著後面來解釋,為什麼一定要依賴

//Custom repository

            Configuration.ReplaceService<IRepository<Post, Guid>>(() =>
            {
                IocManager.IocContainer.Register(
                    Component.For<IRepository<Post, Guid>, IPostRepository, 
PostRepository>()
                        .ImplementedBy<PostRepository>()
                        .LifestyleTransient()
                );
            });

3.自動註冊實體倉儲

首先來看下,我們定義好DbContext後,如果使用自己的倉儲服務呢?
在類裡面定義屬性倉儲

private readonly IRepository<EntityDynamicParameter> _entityDynamicParameterRepository;

大家有沒有考慮過,為什麼我們可以直接使用實體的倉儲類,在哪裡例項化的呢? 這是ABP自動完成的,會反射獲取所有的實體服務,並自動為其註冊倉儲服務,我們一起來分析下自動註冊的內容

AbpEntityFrameworkCoreModule.cs

public override void Initialize()
        {            
IocManager.RegisterAssemblyByConvention(typeof(AbpEntityFrameworkCoreModule).GetAssembly());
            IocManager.IocContainer.Register(
                Component.For(typeof(IDbContextProvider<>))
           .ImplementedBy(typeof(UnitOfWorkDbContextProvider<>))
                    .LifestyleTransient()
                );
            RegisterGenericRepositoriesAndMatchDbContexes();
        }

呼叫 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);
                    IocManager.IocContainer.Register(
                    Component.For<ISecondaryOrmRegistrar>()
                            .Named(Guid.NewGuid().ToString("N"))
                            .Instance(new EfCoreBasedSecondaryOrmRegistrar(dbContextType, scope.Resolve<IDbContextEntityFinder>()))
                            .LifestyleTransient()
                    );
                }
                scope.Resolve<IDbContextTypeMatcher>().Populate(dbContextTypes);
            }
        }

1.首先載入所有的AbpDbContext
2.對AbpDbContext迴圈進行註冊

這裡的註冊依賴介面
scope.Resolve().RegisterForDbContext(dbContextType, IocManager, EfCoreAutoRepositoryTypes.Default);
我們來看下這個具體實現邏輯,依賴介面 IEfGenericRepositoryRegistrar
EfGenericRepositoryRegistrar.cs

public void RegisterForDbContext(
            Type dbContextType, 
            IIocManager iocManager, 
            AutoRepositoryTypesAttribute defaultAutoRepositoryTypesAttribute)
        {
            var autoRepositoryAttr = dbContextType.GetTypeInfo().GetSingleAttributeOrNull<AutoRepositoryTypesAttribute>() ?? defaultAutoRepositoryTypesAttribute;
            RegisterForDbContext(
                dbContextType,
                iocManager,
                autoRepositoryAttr.RepositoryInterface,
                autoRepositoryAttr.RepositoryInterfaceWithPrimaryKey,
                autoRepositoryAttr.RepositoryImplementation,
                autoRepositoryAttr.RepositoryImplementationWithPrimaryKey
            );
            if (autoRepositoryAttr.WithDefaultRepositoryInterfaces)
            {
                RegisterForDbContext(
                    dbContextType,
                    iocManager,
                    defaultAutoRepositoryTypesAttribute.RepositoryInterface,
                    defaultAutoRepositoryTypesAttribute.RepositoryInterfaceWithPrimaryKey,
                    autoRepositoryAttr.RepositoryImplementation,
                    autoRepositoryAttr.RepositoryImplementationWithPrimaryKey
                );
            }
        }
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()
                        );
                    }
                }
                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()
                    );
                }
            }
        }

來分析下具體的實現邏輯
foreach (var entityTypeInfo in _dbContextEntityFinder.GetEntityTypeInfos(dbContextType))
_dbContextEntityFinder.GetEntityTypeInfos(dbContextType) 這裡獲取的就是DbContext定義的實體DbSet,從而獲取到每個實體,用來做後續的倉儲注入;例如:獲取到了 PersonEntity
 var primaryKeyType = EntityHelper.GetPrimaryKeyType(entityTypeInfo.EntityType);
獲取實體主鍵
if (primaryKeyType == typeof(int))
判斷主鍵是否為int,如果是int,則繼承 IRepository,否則繼承IRepository<Entity,PrimaryKey>用來重寫主鍵
那是通過什麼類來實現的IRepository呢?

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

這是預設的實體繼承的倉儲類,EfCoreRepositoryBase 類

好了,實體的預設倉儲就介紹完畢了。。。 ? 不對啊,這裡可以滿足我們的DbContext裡面所有的實體,但是萬一有了自定義倉儲呢?怎麼註冊自己的倉儲呢?

哈哈,其實還是有個方法的,而且還不只一個。。。
1.DbContext打標記,用來替換預設的AutoRepositoryTypesAttribute

[AutoRepositoryTypes(
            typeof(IMyModuleRepository<>),
            typeof(IMyModuleRepository<,>),
            typeof(MyModuleRepositoryBase<>),
            typeof(MyModuleRepositoryBase<,>)
            )]
            public class SupportDbContext : AbpDbContext

2.第二種就是替換已經註冊的實體倉儲服務
回到上面問題,AbpEntityFrameworkCoreModule 模組先註冊 ? 其實上面寫到了,在我們自定義的模組註冊時,可以重新註冊倉儲服務

//Custom repository

            Configuration.ReplaceService<IRepository<Post, Guid>>(() =>
            {
                IocManager.IocContainer.Register(
                    Component.For<IRepository<Post, Guid>, IPostRepository, 
PostRepository>()
                        .ImplementedBy<PostRepository>()
                        .LifestyleTransient()
                );
            });

就是要必須在 AbpEntityFrameworkCoreModule 註冊之後,否則就會被覆蓋哦,這裡也就呼應了上面的問題了

倉儲三要素:

  1. 倉儲的生命週期:倉儲都是臨時性的,需要的時候建立,用完銷燬。
  1. 資料庫的連線和管理倉儲的方法中,資料庫的連線和管理都是由ABP框架自動處理的。當方法被呼叫的時候,ABP自動開啟資料庫的連線同時開啟事務,當方法結束後,ABP會將實體資料儲存,然後斷開連線。當在倉儲方法中呼叫倉儲方法的時候,此時只會建立一個資料庫連線,他們共同享用資料庫連線和事務,由最上層的那個倉儲方法進行管理。
  2. 倉儲的最佳實踐在ABP框架初始化的時候已經為每一個實體類都預設的實現了相應的倉儲,這些倉儲裡的方法基本可以滿足日常的開發需求,所以不要自己手動建立倉儲

相關文章