• ABP框架之——数据访问基础架构(下)


    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的一块垫脚石,我们一起精进。

    EF Core集成

    EF Core是微软的ORM,可以使用它与主流的数据库提供商合作,如SQL Server、Oracle、MySQL、PostgreSQL和Cosmos DB。当您使用ABP命令行界面(CLI)创建新的ABP解决方案时,它是默认的数据库提供程序。

    默认情况下,启动模板使用SQL Server。如果您更喜欢其他的数据库管理系统(DBMS),可以在创建新解决方案时指定-DBMS参数,如下所示:

    abp new DemoApp -dbms MySQL
    

    您可以参考ABP的文档,了解最新支持的数据库选项,以及如何切换到其他现成数据库提供程序。

    在接下来您将了解到:

    • 如何配置DBMS;
    • 如何定义DbContext类;
    • 如何注册到依赖注入(DI)系统;
    • 如何将实体映射到数据库表;
    • 如何使用Code First和为实体创建自定义存储库;
    • 如何为实体加载相关数据的不同方式。

    3.1 配置 DBMS

    我们使用AbpDbContextOptions在模块的ConfigureServices方法中配置DBMS。以下示例使用SQL Server作为DBMS进行配置:

    Configure<AbpDbContextOptions>(options =>
    {
        options.UseSqlServer();
    });
    

    当然,如果希望配置不同的DBMS,那么UseSqlServer()方法调用将有所不同。我们不需要设置连接字符串,因为它是从ConnectionString:Default配置自动获得的。你可以查看appsettings.json文件,以查看和更改连接字符串。

    配置了DBMS,但还没有定义DbContext对象,这是在EF Core中使用数据库所必需的,我接下来看看如何配置:

    3.2 定义 DbContext

    DbContext是EF Core中与数据库交互的主要对象。通常创建一个继承自DbContext的类来创建自己的DbContext。使用ABP框架,我们将继承AbpDbContext。

    下面是一个使用ABP框架的DbContext类定义示例:

    using Microsoft.EntityFrameworkCore;
    using Volo.Abp.EntityFrameworkCore;
    namespace FormsApp
    {
        public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
        {
            public DbSet<Form> Forms { get; set; }
            public FormsAppDbContext(DbContextOptions<FormsAppDbContext> options)
                : base(options)
            {
            }
        }
    }
    

    FormsAppDbContext继承自AbpDbContext<FormsAppDbContext>AbpDbContext是一个泛型类,将DbContext类型作为泛型参数。它还迫使我们创建一个构造函数。然后,我们就可以为实体添加DbSet属性。

    一旦定义了DbContext,我们就应该向DI系统注册它,以便在应用程序中使用它。

    3.3 向 DI 注册 DbContext

    AddAbpDbContext扩展方法用于向DI系统注册DbContext类。您可以在模块的ConfigureServices方法中使用此方法(它位于启动解决方案的EntityFrameworkCore项目中),如以下代码块所示:

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAbpDbContext<FormsAppDbContext> (options =>
        {
        	//启用默认通用存储库,DDD应始终通过聚合根访问子实体
            options.AddDefaultRepositories();
            
            //开启后,非聚合根实体也支持IRepository注入
        	//options.AddDefaultRepositories(includeAllEntities: true);
        });
    }
    

    AddDefaultRepositories()用于为与DbContext相关的实体启用默认通用存储库。默认情况下,它仅为聚合根实体启用通用存储库,因为在域驱动设计(DDD)中,子实体应始终通过聚合根进行访问。如果还想将存储库用于其他实体类型,可以将可选的includealentities参数设置为true

    options.AddDefaultRepositories(includeAllEntities: true);
    

    使用此选项,意味着您可以为应用程序的任何实体注入IRepository服务。

    注意:因为从事关系数据库的开发人员习惯于从所有数据库表中查询,如果要严格应用 DDD 原则,则应始终使用聚合根来访问子实体。

    我们已经了解了如何注册DbContext类,我们可以为DbContext类中的所有实体注入和使用IRepository接口。接下来,我们应该首先为实体配置EF Core映射。

    3.4 配置实体映射

    EF Core是一个对象到关系的映射器,它将实体映射到数据库表。我们可以通过以下两种方式配置这些映射的详细信息:

    • 在实体类上使用数据注释属性
    • 通过重写OnModelCreating方法在内部使用 Fluent API(推荐)

    使用数据注释属性会领域层依赖于EF Core,如果这对您来说不是问题,您可以遵循EF Core的文档使用这些属性。为了解脱依赖,同时也为了保持实体类的纯洁度,推荐使用Fluent API方法。

    要使用Fluent API方法,可以在DbContext类中重写OnModelCreating方法,如以下代码块所示:

    public class FormsAppDbContext : AbpDbContext<FormsAppDbContext>
    {
        ...
        //1.override覆盖后,依然会调用父类的base.OnModelCreating(),因为内置审计日志和数据过滤
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            
            2.Fluent API,这里可以继续封装(TODO)
            builder.Entity<Form>(b =>
            {
                b.ToTable("Forms");
                b.ConfigureByConvention(); //3.重要,默认配置预定义的Entity或AggregateRoot,无需再配置,剩下的配置就显得整洁而规范了。
                b.Property(x => x.Name)
                    .HasMaxLength(100)
                    .IsRequired();
                b.HasIndex(x => x.Name);
            });
            
            //4.一对多的配置
            builder.Entity<Question>(b =>
            {
                b.ToTable("FormQuestions");
                b.ConfigureByConvention();
                b.Property(x => x.Title)
                    .HasMaxLength(200)
                    .IsRequired();
                b.HasOne<Form>() //5.一个问题对应一个表单,一个表单有多个问题。
                    .WithMany(x => x.Questions)
                    .HasForeignKey(x => x.FormId)
                    .IsRequired();
            });
        }
    }
    

    重写OnModelCreating方法时,始终调用base.OnModelCreating(),因为该方法内执行默认配置(如审核日志和数据过滤器)。然后,使用builder对象执行配置。

    例如,我们可以为本章中定义的表单类配置映射,如下所示:

    builder.Entity<Form>(b => { 
        b.ToTable("Forms");     
        b.ConfigureByConvention();     
        b.Property(x => x.Name).HasMaxLength(100) .IsRequired();     
        b.HasIndex(x => x.Name); 
    });
    

    在这里调用b.ConfigureByConvention方法很重要。如果实体派生自ABP的预定义实体或AggregateRoot类,它将配置实体的基本属性。剩下的配置代码非常干净和标准,您可以从EF Core的文档中了解所有细节。

    下面是另一个配置实体之间关系的示例:

    builder.Entity<Question>(b => {     
        b.ToTable("FormQuestions");     
        b.ConfigureByConvention();     
        b.Property(x => x.Title).HasMaxLength(200).IsRequired();     
        b.HasOne<Form>().WithMany(x => x.Questions).HasForeignKey(x => x.FormId).IsRequired(); 
    });
    

    在这个例子中,我们定义了表单和问题实体之间的关系:一个表单可以有许多问题,而一个问题属于一个表单。

    EF的 Code First Migrations系统提供了一种高效的方法来增量更新数据库,使其与实体保持同步。

    Code First相比较传统迁移的好处:

    • 高效快速
    • 增量更新
    • 版本管理

    3.5 实现自定义存储库

    我们在“自定义存储库”部分创建了一个IFormRepository接口。现在,是时候使用EF Core实现这个存储库接口了。

    在解决方案的EF Core集成项目中实现存储库,如下所示:

    //1.集成自EfCoreRepository,传入三个泛型参数,继承了所有标准存储库的方法。
    public class FormRepository : EfCoreRepository<FormsAppDbContext, Form, Guid>,IFormRepository
    {
        public FormRepository(IDbContextProvider<FormsAppDbContext> dbContextProvider)
            : base(dbContextProvider){ }
            
        public async Task<List<Form>> GetListAsync(string name, bool includeDrafts = false)
        {
            var dbContext = await GetDbContextAsync();
            var query = dbContext.Forms.Where(f => f.Name.Contains(name));
            if (!includeDrafts)
            {
                query = query.Where(f => !f.IsDraft);
            }
            return await query.ToListAsync(); 
        }
    }
    

    该类派生自ABP的EfCoreRepository类。通过这种方式,我们继承了所有标准的存储库方法。EfCoreRepository类获得三个通用参数:DbContext类型、实体类型和实体类的PK类型。

    FormRepository还实现了IFormRepository,它定义了一个GetListAsync方法,DbContext实例在这个方法中可以使用EF Core API的所有功能。

    关于WhereIf的提示:

    条件过滤是一种广泛使用的模式,ABP提供了一种很好的WhereIf扩展方法,可以简化我们的代码。

    我们可以重写GetListAsync方法,如下代码块所示:

    var dbContext = await GetDbContextAsync(); 
    return await dbContext.Forms
    .Where(f => f.Name.Contains(name))
    .WhereIf(!includeDrafts, f => !f.IsDraft)
    .ToListAsync();
    

    因为我们有DbContext实例,所以可以使用它执行结构化查询语言(SQL)命令或存储过程。下面是执行“删除所有表单”命令:

    public async Task DeleteAllDraftsAsync() 
    {     
        var dbContext = await GetDbContextAsync();     
        //执行SQL查询
        await dbContext.Database.ExecuteSqlRawAsync("DELETE FROM Forms WHERE IsDraft = 1"); 
    }
    

    执行存储过程和函数,请参考EF的核心文档学习如何执行存储过程和函数。

    一旦实现了IFormRepository,就可以注入并使用它,而不是IRepository<Form,Guid>,如下所示:

    1)自定义存储库的调用

    public class FormService : ITransientDependency
    {
        private readonly IFormRepository _formRepository;//自定义仓储库
        public FormService(IFormRepository formRepository)
        {
            _formRepository = formRepository;
        }
    
        public async Task<List<Form>> GetFormsAsync(string name)
        {
            return await _formRepository.GetListAsync(name, includeDrafts: true);
        }
    }
    

    FormService类使用IFormRepository的自定义GetListAsync方法。即使为表单实现了自定义存储库类,仍然可以为该实体注入并使用默认的通用存储库(例如,IRepository<Form,Guid>),尤其是刚开始不熟悉,可以从通用存储库上手,等熟悉后就可以使用自定义存储库。

    2)自定义存储库的配置

    如果重写EfCoreRepository类中的基方法并,可能会出现一个潜在问题:使用通用存储库的服务将继续使用非重写方法。要防止这种情况,请在向DI注册DbContext时使用AddRepository方法,如下所示:

    context.Services.AddAbpDbContext<FormsAppDbContext>(options =>
    {
        options.AddDefaultRepositories();
        //实现仓储库后,建议进行注入
        options.AddRepository<Form, FormRepository>();
    });
    

    通过这种配置,AddRepository方法将通用存储库重定向到自定义存储库类。

    3.7 数据加载

    如果您的实体具有指向其他实体的导航属性或具有其他实体的集合,则在使用主实体时,您经常需要访问这些相关实体。例如,前面介绍的表单实体有一组问题实体,您可能需要在使用表单对象时访问这些问题集。

    访问相关实体有多种方式,包括:

    • 显式加载
    • 延迟加载
    • 即时加载

    1)显式加载

    存储库提供了EnsureRepropertyLoadedAsyncEnsureRecollectionLoadedAsync扩展方法,以显式加载导航属性或子集合。

    例如,我们可以显式加载表单的问题,如以下代码块所示:

    public async Task<IEnumerable<Question>> GetQuestionsAsync(Form form)
    {
    	//
        await _formRepository.EnsureCollectionLoadedAsync(form, f => f.Questions);
        return form.Questions;
    }
    

    如果不用EnsureCollectionLoadedAsyncQuestions可能是空的,如果已经加载过,不会重复加载,所以多次调用对性能没有影响。

    2)延迟加载

    延迟加载是EF Core的一项功能,它在您首次访问相关属性和集合时加载它们。默认情况下不启用延迟加载。如果要为DbContext启用它,请执行以下步骤:

    1. 在 EF Core 层中安装Microsoft.EntityFrameworkCore.Proxies
    2. 配置时使用 UseLazyLoadingProxies方法
    Configure<AbpDbContextOptions>(options =>
    {
        options.PreConfigure<FormsAppDbContext>(opts =>
        {
            opts.DbContextOptions.UseLazyLoadingProxies();
        });
        options.UseSqlServer();
    });
    
    • 确保导航属性和集合属性在实体中是virtual
    public class Form : BasicAggregateRoot<Guid>
    {
        ...
        public virtual ICollection<Question> Questions { get; set; }
        public virtual ICollection<FormManager> Owners { get; set; }
    }
    

    当您启用延迟加载时,您无需再使用显式加载。

    延迟加载是一个被讨论过的ORM概念。一些开发人员发现它很实用,而其他人则建议不要使用它。我之所以不使用它,是因为它有一些潜在的问题,比如:

    • 无法使用异步

    延迟加载不能使用异步编程,无法使用async/await模式访问属性。因此,它会阻止调用线程,这对于吞吐量和可伸缩性来说是一种糟糕的做法。

    • 1+N性能问题

    如果在使用foreach循环之前没有预先加载相关数据,则可能会出现1+N加载问题。1+N加载意味着通过单个数据库操作1次(比如,从数据库中查询实体列表),然后执行一个循环来访问这些实体的导航属性(或集合)。在这种情况下,它会延迟加载每个循环内的相关属性(N=第一次数据库操作中查询的实体数)。因此,进行1+N数据库调用,会显著降低应用程序性能。

    • 断言和代码优化问题

    因为您可能不容易看到相关数据何时从数据库加载。我建议采用一种更可控的方法,尽可能使用即时加载

    3)即时加载

    顾名思义,即时加载是在首先查询主实体时加载相关数据的一种方式。假设您已经创建了一个自定义存储库,以便在从数据库获取表单对象时加载相关问题,如下所示:

    • EF Core层,在自定义仓储库中使用EF Core API
    public async Task<Form> GetWithQuestions(Guid formId)
    {
        var dbContext = await GetDbContextAsync();
        return await dbContext.Forms
            .Include(f => f.Questions)
            .SingleAsync(f => f.Id == formId);
    }
    

    自定义存储库方法,可以使用完整的EF Core API。但是,如果您使用的是ABP的存储库,并且不想在应用程序层依赖EF Core,那么就不能使用EF CoreInclude 扩展方法(用于快速加载相关数据)。

    假如你不想在应用层依赖EF Core API该怎么办?

    在本例中,您有两个选项:

    1)IRepository.WithDetailsAsync

    IRepositoryWithDetailsSync方法通过包含给定的属性或集合来返回IQueryable实例,如下所示:

    public async Task EagerLoadDemoAsync(Guid formId)
    {
        var queryable = await _formRepository.WithDetailsAsync(f => f.Questions);
        var query = queryable.Where(f => f.Id == formId);
        var form = await _asyncExecuter.FirstOrDefaultAsync(query);
        foreach (var question in form.Questions)
        {
            //...
        }
    }
    

    WithDetailsAsync(f=>f.Questions)返回IQueryable<Form>,其中包含form.Questions,因此我们可以安全地循环表单。IAsyncQueryableExecuter在本章的“通用存储库”部分进行了介绍。如果需要,WithDetailsSync方法可以获取多个表达式以包含多个属性。如果需要嵌套包含(EF Core中的ThenClude扩展方法),则不能使用WithDetailsAsync

    2)聚合模式

    聚合模式将在第10章DDD——领域层中详细介绍。可以简单地理解:一个聚合被认为是一个单一的单元,它与所有子集合一起作为单个单元进行读取和保存。这意味着您在加载Form时总是加载相关Questions

    ABP很好地支持聚合模式,并允许您在全局点为实体配置即时加载。我们可以在模块类的ConfigureServices方法中编写以下配置(在解决方案的EntityFrameworkCore项目中):

    Configure<AbpEntityOptions>(options =>
    {
        options.Entity<Form>(orderOptions =>
        {
        	//全局点为实体配置预加载
            orderOptions.DefaultWithDetailsFunc = query => query
                .Include(f => f.Questions)
                .Include(f => f.Owners);
        });
    });
    

    建议包括所有子集合。如上所示配置DefaultWithDetailsFunc方法后,将发生以下情况

    • 默认情况下,返回单个实体(如GetAsync)的存储库方法将加载相关实体,除非通过在方法调用中将includeDetails参数指定为false来明确禁用该行为。
    • 返回多个实体(如GetListAsync)的存储库方法将允许相关实体的即时加载,而默认情况下它们不会即时加载。

    下面是一些例子,获取包含子集合的单一表单,如下所示:

    //获取一个包含子集合的表单
    var form = await _formRepository.GetAsync(formId);
    
    //获取一个没有子集合的表单
    var form = await _formRepository.GetAsync(formId, includeDetails: false);
    
    //获取没有子集合的表单列表
    var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"));
    
    //获取包含子集合的表单列表
    var forms = await _formRepository.GetListAsync(f => f.Name.StartsWith("A"), includeDetails: true);
    

    聚合模式在大多数情况下简化了应用程序代码,而在需要性能优化的情况下,您可以进行微调。请注意,如果真正实现聚合模式,则不会使用导航属性(指向其他聚合),我们将在第10章DDD——领域层中再次回到这个主题。

    了解UoW

    UoW是ABP用来启动、管理和处理数据库连接和事务的主要系统。UoW采用环境上下文模式(Ambient Context pattern)设计。这意味着,当我们创建一个新的UoW时,它会创建一个作用域上下文,该上下文中共享所有数据库操作=。UoW中完成的所有操作都会一起提交(成功时)或回滚(异常时)。

    配置UoW选项

    ASP.NET Core中,默认设置下,HTTP请求被视为一个UoW。ABP在请求开始时启动UoW,如果请求成功完成,则将更改保存到数据库中。如果请求因异常而失败,它将回滚。

    ABP根据HTTP请求类型确定数据库事务使用情况。HTTP GET请求不会创建数据库事务。UoW仍然可以工作,但在这种情况下不使用数据库事务。如果您没有对所有其他HTTP请求类型(POSTPUTDELETE和其他)进行配置,则它们将使用数据库事务

    HTTP请求 是否创建事务
    GET 不创建事务
    PUT 创建事务
    POST 创建事务

    最好不要在GET请求中更改数据库。如果在一个GET请求中进行了多个写操作,但请求以某种方式失败,那么数据库状态可能会处于不一致的状态,因为ABP不会为GET请求创建数据库事务。在这种情况下,可以使用AbpUnitOfWorkDefaultOptionsGET请求启用事务,也可以手动控制UoW。

    为GET启用请求事务的配置:

    在模块(在数据库集成项目中)的ConfigureServices方法中使用AbpUnitOfWorkDefaultOptions,如下所示:

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpUnitOfWorkDefaultOptions>(options =>
        {
            options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled;
            options.Timeout = 300000; // 5 minutes
            options.IsolationLevel = IsolationLevel.Serializable;
        });
    }
    

    TransactionBehavior的三个值:

    • Auto(默认):自动使用事务(为非GET HTTP请求启用事务)
    • Enabled:始终使用事务,即使对于HTTP GET请求
    • Disabled: 从不使用事务

    Auto是默认值,对于大多数应用推荐使用。IsolationLevel仅对关系数据库有效。如果未指定,ABP将使用基础提供程序的默认值。最后,Timeout选项允许将事务的默认超时值设置为毫秒,如果UoW操作未在给定的超时值内完成,将引发超时异常。

    以上,我们学习了如何在全局配置UOW默认选项,也可以为单个UoW手动配置这些值。

    手动控制UoW

    对于web应用,一般很少需要手动控制UoW。但是,对于后台作业或非web应用程序,您可能需要自己创建UoW作用域。

    使用特性

    创建UoW作用域的一种方法是在方法上使用[UnitOfWork]属性,如下所示:

    [UnitOfWork(isTransactional: true)] 
    public async Task DoItAsync()
    {     
        await _formRepository.InsertAsync(new Form() { ... });     
        await _formRepository.InsertAsync(new Form() { ... }); 
    }
    

    如果周围的UoW已经就位,那么UnitOfWork特性将被忽略。否则,ABP会在进入DoItAsync方法之前启动一个新的事务UoW,并在不引发异常的情况下提交事务。如果该方法引发异常,事务将回滚。

    使用注入服务

    如果要精细控制UoW,可以注入并使用IUnitOfWorkManager服务,如以下代码块所示:

    public async Task DoItAsync() 
    {     
        using (var uow = _unitOfWorkManager.Begin(requiresNew: true,isTransactional: true,         timeout: 15000))
        {
            await _formRepository.InsertAsync(new Form() { });         
            await _formRepository.InsertAsync(new Form() { });         
            await uow.CompleteAsync();     
        }
    }
    

    在本例中,我们将启动一个新的事务性UoW作用域,timeout参数的值为15秒。使用这种用法(requiresNew: true),ABP总是启动一个新的UoW,即使周围已经有一个UoW。如果一切正常,会调用uow.CompleteAsync()方法。如果要回滚当前事务,请使用uow.RollbackAsync()方法。

    如前所述,UoW使用环境作用域。您可以使用IUnitOfWorkManager.Current访问此范围内的任何位置的当前UoW。如果没有正在进行的UoW,则可以为null

    下面的代码段将SaveChangesAsync方法与IUnitOfWorkManager.Current属性一起使用:

    await _unitOfWorkManager.Current.SaveChangesAsync();
    

    我们将所有挂起的更改保存到数据库中。但是,如果这是事务性UoW,那么如果回滚UoW或在UoW范围内引发任何异常,这些更改也会回滚。

    小结 & 思考

    • 小结:ABP 框架可以与任何数据库系统一起工作,同时它提供了与EF Core和MongoDB的内置集成包。
    • 思考:假如你不想在应用层依赖EF Core API,或者用的是ABP仓储库该怎么办?
  • 相关阅读:
    Android 网络框架学习之Retrofit,androidretrofit
    Android 应用APP加入聊天功能
    Mybatis 数据库物理分页插件 PageHelper
    mvn常用命令
    执行第一maven用例出错:Unknown lifecycle phase "complile".
    Mybatis分页插件
    ViewHolder模式超简洁写法
    android网络框架Retrofit 同步异步
    严苛模式(StrictMode)
    使用PF_PACKET和SOCK_RAW发送自己定义type以太网数据包
  • 原文地址:https://www.cnblogs.com/jackyfei/p/16405053.html
Copyright © 2020-2023  润新知