侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

详解ABP框架中的数据过滤器与数据传输对象的使用

技术  /  管理员 发布于 7年前   150

数据过滤器(Data filters)
在数据库开发中,我们一般会运用软删除(soft-delete)模式,即不直接从数据库删除数据,而是标记这笔数据为已删除。因此,如果实体被软删除了,那么它就应该不会在应用程序中被检索到。要达到这种效果,我们需要在每次检索实体的查询语句上添加SQL的Where条件IsDeleted = false。这是个乏味的工作,但它是个容易被忘掉的事情。因此,我们应该要有个自动的机制来处理这些问题。

ABP提供数据过滤器(Data filters),它使用自动化的,基于规则的过滤查询。ABP已经有一些预定义的过滤器,当然也可以自行创建你专属的过滤器。

注意:只针对EntityFramework:ABP数据过滤器仅实现在EntityFramework。还无法在其它ORM工具中使用。见其它ORM章节于本文末端。

预定义过滤器
1.软删除接口(ISoftDelete)

软删除过滤器(Soft-delete filter )会过滤从数据库查询出来的实体且是自动套用(从结果集中提取出来)。如果实体需要被软删除,它需要实现ISoftDelete接口,该接口仅定义了一个IsDeleted属性。例:

 public class Person : Entity, ISoftDelete { public virtual string Name { get; set; } public virtual bool IsDeleted { get; set; } }

Person实体实际上并没有从数据库中被删除,当删除此实体时,IsDeleted属性值会被设定为true。当你使用IRepository.Delete方法时,ABP会自动完成这些工作(你可以手动设定IsDeleted为true,但是Delete方法更加自然且是较建议的方式)。

当实现了ISoftDelete之后,当你已经从数据库中取得了People列表,已被删除的People实体并不会被检索到。在这里有一个示例类,该类使用了person仓储来取得所有的People实体: 

 public class MyService { private readonly IRepository<Person> _personRepository; public MyService(IRepository<Person> personRepository) {  _personRepository = personRepository; } public List<Person> GetPeople() {  return _personRepository.GetAllList(); } }

GetPeople方法仅取得Person实体,该实体其IsDeleted = false(非删除状态)。所有的仓储方法以及导航属性都能够正常运作。我们可以添加一些其它的Where条件,Join...等等。它将会自动地添加IsDeleted=false条件到生成的SQL查询语句中。

注意:何时启用?ISoftDelete过滤器总是启用,除非你直接禁用它。

提醒:如果你实现了IDeletionAudited接口(该接口继承自ISoftDelete),删除创建时间和被删除的用户Id,这些都会由ABP进行自动的处理。

2.多租接口(IMustHaveTenant)

如果你创建一个多租户的应用程序(储存所有租户的数据于单一一个数据库中),你肯定不会希望某个租户看到其它租户的资料。你可以实现IMustHaveTenant接口于此案例中,示例如下:

 public class Product : IMustHaveTenant { public virtual int TenantId { get; set; } public virtual string Name { get; set; } }

 IMustHaveTenant定义了TenantId来区别不同的租户实体。ABP使用IAbpSession来取得当前TenantId并且自动地替当前租户进行过滤查询的处理。

注意:何时启用?IMustHaveTenant默认是启用的。如果当前使用并没有登入到系统或是当前用户是一个管理级使用者(管理级使用者即为一个最高权限的使用者,它可以管理所有租户和租户的资料),ABP会自动地禁用IMustHaveTenant过滤器。因此,所有的租户的数据都可以被应用程序所检索。注意,这与安全性无关,你应该要对敏感数据进行验证授权处理。

3.多租接口(IMayHaveTenant)

 如果一个实体类由多个租户(tenant)以及管理级使用者(host)所共享(这意味着该实体对象或许由租户(tenant)或是管理级使用者(host)所掌控),你可以使用IMayHaveTenantfilter。IMayHaveTenant接口定义了TenantId但是它是可空类(nullable)。

 public class Product : IMayHaveTenant { public virtual int? TenantId { get; set; } public virtual string Name { get; set; } }

当为null值,则代表这是一个管理级使用者(host)所掌控的实体,若为非null值,则代表这个实体是由租户(tenant)所掌控,而其Id值即为TenantId。ABP使用IAbpSession接口来取得当前TenantId。IMayHaveTenant过滤器并不如IMustHaveTenant普遍常用。但是当作为管理级使用者(host)和租户(tenant)所需要的通用结构使用时,你或许会需要用到它。

何时启用?IMayHaveTenant接口总是启用的,除非你直接禁用它。

禁用过滤器
可以在工作单元(unit of work)中调用DisableFilter方法来禁用某个过滤器,如下所示:

var people1 = _personRepository.GetAllList();using(_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) { var people2 = _personRepository.GetAllList(); }var people3 = _personRepository.GetAllList();

DisableFilter方法取得一或多个过滤器名称,且类型皆为string。AbpDataFilters.SoftDelete是一个常数字符串其包含了ABP标准的软删除过滤器。

people2亦可取得已标记为删除的People实体,而people1和people3将会是唯一的非已标记为删除的People实体。若配合使用using语法,你可以禁用其控制范围内(Scope)的过滤器。如果你不使用 using 语法 ,此过滤器会被一直禁用,直到工作单元(unit of work)结束或者再度启用它。(意思是:如果你使用"using"关键字声明,过滤器是启用状态;当前工作单元(unit of work)结束后,过滤器是禁止状态。如果不使用"using"关键字声明,默认过滤器是禁用状态,此时可以手动启用过滤器。)

你可以注入IUnitOfWorkManager并且在上述示例中使用。同样的,你可以使用CurrentUnitOfWork属性作为一个在应用服务中的简便方式(它是从ApplicationService类继承而来的)。

注意:关于using语法:假如过滤器在你调用DisableFilter方法并配合using语法之前已是启用,则过滤器会被禁用,并且会自动地在using语法结束后在度启用。但是若过滤器在using语法之前就已经被禁用了,DisableFilter方法实际上并不做任何式,并且过滤器会维持禁用状态即便是using语法的结束后。

启用过滤器
你可以在工作单元(unit of work)中使用EnableFilter方法启用过滤器,如同DisableFilter方法一般(两者互为正反两面)。EnableFilter亦会返回disposable来自动地重新禁用过滤器。

设定过滤器参数
 过滤器可以被参数化(parametric)。IMustHaveTenant过滤器是这类过滤器的一个范本,因为当前租户(tenant)的Id是在执行时期决定的。对于这些过滤器,如果真有需要,我们可以改变过滤器的值。举例如下:

CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42);
另一个示例如下:设定IMayHaveTenant过滤器的tenantId值:

CurrentUnitOfWork.SetfilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42);

自定义过滤器
欲创建定制的过滤器并且整合到ABP中,首先我们需要定义一个接口,该接口将会由使用这个过滤器的实体所实现。假设我们想要自动化地依PersonId进行过滤,示例如下:

 public interface IHasPerson { int PersonId { get; set; } }

然后我们就可以实现这个接口在我们的实体上了,示例如下:

 public class Phone : Entity, IHasPerson { [ForeignKey("PersonId")] public virtual Person Person { get; set; } public virtual int PersonId { get; set; } public virtual string Number { get; set; } }

因为ABP使用EntityFramework.DynamicFilters这个过滤器,我们使用它的规则(rule)来定义过滤器。在我们的DbContext类中,我们重写了OnModelCreating并且定义了过滤器如下示例所示:

 protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, 0 ); }

PersonFilter过滤器在这里是一个唯一的过滤器名称。再来就是过滤器接口的参数定义和personId过滤器参数(不一定需要,假如过滤器是属于不可参数化(parametric)型),最后一个参数为personId的默认值。

最后一个步骤,我们需要注册这个过滤器到ABP工作单元(unit of work)系统中,设定的位置在我们模块里的PreInitialize方法。

Configuration.UnitOfWork.RegisterFilter("PersonFilter", false);

第一个参数是我们刚刚所定义的唯一名称,第二个参数指示这个过滤器预设是启用还是禁用。在声明完这些可参数化(parametric)的过滤器后,我们可以在执行期间指定它的值来操作这个过滤器。

 using(CurrentUnitOfWork.EnableFilter("PersonFilter")) { CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42); var phone = _phoneRepository.GetAllList(); // ... }

我们可以从有些数据源中取得personId而不需要写死在程序代码中。上述示例是为了要能够程序化过滤器。过滤器可拥有0到更多的参数。假如是无参数的过滤器,它就不需要设定过滤器的值。同样地,假如它预设是启用,就不需要手动启用(当然的,我们也可以禁用它)。

EntityFramework.DynamicFilters的文件:若需要更多关于动态数据过滤器的相关信息,可以见其在git上的文件https://github.com/jcachat/EntityFramework.DynamicFilters

我们可以为安全性创建一个定制化的过滤器,主/被动实体,多租户...诸如此类的。

其它对象关系映射工具
ABP数据过滤器仅实现在Entity Framework上。对于其它ORM工具则尚不可用。但是, 实际上,你可以模仿这个模式到其它使用仓储来取得数据的案例下。这此案例中, 你可以创建一个定制的仓储并且覆写GetAll方法,如果有需要的话,可以一起修改其它资料检索方法。


数据传输对象(DTOs)
数据传输对象(Data Transfer Objects)用于应用层和展现层的数据传输。

展现层传入数据传输对象(DTO)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回DTO给展现层。这样展现层和领域层被完全分离开了。在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体)。

数据传输对象的作用
为每个应用服务方法创建DTO看起来是一项乏味耗时的工作。但如果你正确使用它们,这将会解救你的项目。为啥呢?

1.抽象领域层 (Abstraction of domain layer)

在展现层中数据传输对象对领域对象进行了有效的抽象。这样你的层(layers)将被恰当的隔离开来。甚至当你想要完全替换展现层时,你还可以继续使用已经存在的应用层和领域层。反之,你可以重写领域层,修改数据库结构,实体和ORM框架,但并不需要对展现层做任何修改,只要你的应用层没有发生改变。

2.数据隐藏 (Data hiding)

想象一下,你有一个User实体拥有属性Id, Name, EmailAddress和Password。如果UserAppService的GetAllUsers()方法的返回值类型为List。这样任何人都可以查看所有人的密码,即使你没有将它打印在屏幕上。这不仅仅是安全问题,这还跟数据隐藏有关。应用服务应只返回展现层所需要的,不多不少刚刚好。

3.序列化 & 惰性加载 (Serialization & lazy load problems)

当你将数据(对象)返回给展现层时,数据有可能会被序列化。举个例子,在一个返回Json的MVC的Action中,你的对象需要被序列化成JSON并发送给客户端。直接返回实体给展现层将有可能会出现麻烦。

在真实的项目中,实体会引用其他实体。User实体会引用Role实体。所以,当你序列化User时,Role也将被序列化。而且Role还拥有一个List并且Permission还引用了PermissionGroup等等….你能想象这些对象都将被序列化吗?这有很有可能使整个数据库数据意外的被序列化。那么该如何解决呢?将属性标记为不可序列化?不行,因为你不知道属性何时该被序列化何时不该序列化。所以在这种情况下,返回一个可安全序列化,特别定制的数据传输对象是不错的选择哦。

几乎所有的ORM框架都支持惰性加载。只有当你需要加载实体时它才会被加载。比如User类型引用Role类型。当你从数据库获取User时,Role属性并没有被填充。当你第一次读取Role属性时,才会从数据库中加载Role。所以,当你返回这样一个实体给展现层时,很容易引起副作用(从数据库中加载)。如果序列化工具读取实体,它将会递归地读取所有属性,这样你的整个数据库都将会被读取。

在展现层中使用实体还会有更多的问题。最佳的方案就是展现层不应该引用任何包含领域层的程序集。

 DTO 约定 & 验证
ABP对数据传输对象提供了强大的支持。它提供了一些相关的(Conventional)类型 & 接口并对DTO命名和使用约定提供了建议。当你像这里一样使用DTO,ABP将会自动化一些任务使你更加轻松。

一个例子 (Example)

让我们来看一个完整的例子。我们相要编写一个应用服务方法根据name来搜索people并返回people列表。Person实体代码如下:

public class Person : Entity{ public virtual string Name { get; set; } public virtual string EmailAddress { get; set; } public virtual string Password { get; set; }}

首先,我们定义一个应用服务接口:

public interface IPersonAppService : IApplicationService{ SearchPeopleOutput SearchPeople(SearchPeopleInput input);}

ABP建议命名input/ouput对象类似于MethodNameInput/MethodNameOutput,对于每个应用服务方法都需要将Input和Output进行分开定义。甚至你的方法只接收或者返回一个值,也最好创建相应的DTO类型。这样,你的代码才会更具有扩展性,你可以添加更多的属性而不需要更改方法的签名,这并不会破坏现有的客户端应用。

当然,方法返回值有可能是void,之后你添加一个返回值并不会破坏现有的应用。如果你的方法不需要任何参数,那么你不需要定义一个Input Dto。但是创建一个Input Dto可能是个更好的方案,因为该方法在将来有可能会需要一个参数。当然是否创建这取决于你。 Input和Output DTO类型定义如下:

 public class SearchPeopleInput : IInputDto{ [StringLength(40, MinimumLength = 1)] public string SearchedName { get; set; }}public class SearchPeopleOutput : IOutputDto{ public List<PersonDto> People { get; set; }}public class PersonDto : EntityDto{ public string Name { get; set; } public string EmailAddress { get; set; }}

验证:作为约定,Input DTO实现IInputDto 接口,Output DTO实现IOutputDto接口。当你声明IInputDto参数时, 在方法执行前ABP将会自动对其进行有效性验证。这类似于ASP.NET MVC验证机制,但是请注意应用服务并不是一个控制器(Controller)。ABP对其进行拦截并检查输入。查看DTO 验证(DTO Validation)文档获取更多信息。 EntityDto是一个简单具有与实体相同的Id属性的简单类型。如果你的实体Id不为int型你可以使用它泛型版本。EntityDto也实现了IDto接口。你可以看到PersonDto并不包含Password属性,因为展现层并不需要它。

跟进一步之前我们先实现IPersonAppService:

public class PersonAppService : IPersonAppService{ private readonly IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; } public SearchPeopleOutput SearchPeople(SearchPeopleInput input) { //获取实体 var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName)); //转换成DTO var peopleDtoList = peopleEntityList  .Select(person => new PersonDto    {     Id = person.Id,     Name = person.Name,     EmailAddress = person.EmailAddress    }).ToList(); return new SearchPeopleOutput { People = peopleDtoList }; }}

我们从数据库获取实体,将实体转换成DTO并返回output。注意我们没有手动检测Input的数据有效性。ABP会自动验证它。ABP甚至会检查Input是否为null,如果为null则会抛出异常。这避免了我们在每个方法中都手动检查数据有效性。

但是你很可能不喜欢手动将Person实体转换成PersonDto。这真的是个乏味的工作。Peson实体包含大量属性时更是如此。

DTO和实体间的自动映射
还好这里有些工具可以让映射(转换)变得十分简单。AutoMapper就是其中之一。你可以通过nuget把它添加到你的项目中。让我们使用AutoMapper来重写SearchPeople方法:

public SearchPeopleOutput SearchPeople(SearchPeopleInput input){ var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName)); return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(peopleEntityList) };}

这就是全部代码。你可以在实体和DTO中添加更多的属性,但是转换代码依然保持不变。在这之前你只需要做一件事:映射

Mapper.CreateMap<Person, PersonDto>();

AutoMapper创建了映射的代码。这样,动态映射就不会成为性能问题。真是快速又方便。AutoMapper根据Person实体创建了PersonDto,并根据命名约定来给PersonDto的属性赋值。命名约定是可配置的并且很灵活。你也可以自定义映射和使用更多特性,查看AutoMapper的文档获取更多信息。

使用特性(attributes)和扩展方法来映射 (Mapping using attributes and extension methods)

ABP提供了几种attributes和扩展方法来定义映射。使用它你需要通过nuget将Abp.AutoMapper添加到你的项目中。使用AutoMap特性(attribute)可以有两种方式进行映射,一种是使用AutoMapFrom和AutoMapTo。另一种是使用MapTo扩展方法。定义映射的例子如下:

[AutoMap(typeof(MyClass2))] //定义映射(这样有两种方式进行映射)public class MyClass1{ public string TestProp { get; set; }}public class MyClass2{ public string TestProp { get; set; }}

接着你可以通过MapTo扩展方法来进行映射:

var obj1 = new MyClass1 { TestProp = "Test value" };var obj2 = obj1.MapTo<MyClass2>(); //创建了新的MyClass2对象,并将obj1.TestProp的值赋值给新的MyClass2对象的TestProp属性。

上面的代码根据MyClass1创建了新的MyClass2对象。你也可以映射已存在的对象,如下所示:

var obj1 = new MyClass1 { TestProp = "Test value" };var obj2 = new MyClass2();obj1.MapTo(obj2); //根据obj1设置obj2的属性

 辅助接口和类型
ABP还提供了一些辅助接口,定义了常用的标准化属性。

ILimitedResultRequest定义了MaxResultCount属性。所以你可以在你的Input DTO上实现该接口来限制结果集数量。

IPagedResultRequest扩展了ILimitedResultRequest,它添加了SkipCount属性。所以我们在SearchPeopleInput实现该接口用来分页:

public class SearchPeopleInput : IInputDto, IPagedResultRequest{ [StringLength(40, MinimumLength = 1)] public string SearchedName { get; set; } public int MaxResultCount { get; set; } public int SkipCount { get; set; }}

对于分页请求,你可以将实现IHasTotalCount的Output DTO作为返回结果。标准化属性帮助我们创建可复用的代码和规范。可在Abp.Application.Services.Dto命名空间下查看其他的接口和类型。


  • 上一条:
    解析ABP框架中的事务处理和工作单元
    下一条:
    解析ABP框架领域层中的实体类与仓储类
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 2024.07.09日OpenAI将终止对中国等国家和地区API服务(0个评论)
    • 2024/6/9最新免费公益节点SSR/V2ray/Shadowrocket/Clash节点分享|科学上网|免费梯子(0个评论)
    • 国外服务器实现api.openai.com反代nginx配置(0个评论)
    • 2024/4/28最新免费公益节点SSR/V2ray/Shadowrocket/Clash节点分享|科学上网|免费梯子(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-07
    • 2017-08
    • 2017-09
    • 2018-01
    • 2018-07
    • 2018-08
    • 2018-09
    • 2018-12
    • 2019-01
    • 2019-02
    • 2019-03
    • 2019-04
    • 2019-05
    • 2019-06
    • 2019-07
    • 2019-08
    • 2019-09
    • 2019-10
    • 2019-11
    • 2019-12
    • 2020-01
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-09
    • 2020-10
    • 2020-11
    • 2021-04
    • 2021-05
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-12
    • 2022-01
    • 2022-02
    • 2022-03
    • 2022-04
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-08
    • 2023-09
    • 2023-10
    • 2023-12
    • 2024-02
    • 2024-04
    • 2024-05
    • 2024-06
    • 2025-02
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客