软删除模式通常用于不会真正从数据库删除一个实体而是仅仅将它标记为"已删除的"。这样,如果一个实体是软删除的,那么它不应该在应用中检索到。为了实现这个目的,我们应该在每一个select实体查询操作中添加一个SQL where 条件,如“IsDeleted=false”。这是乏味但是很重要的一项容易忘记的任务。因此,这项工作应该自动完成。
ABP提供了 数据过滤器 ,它们可以基于某些规则自动过滤查询。有很多预定义的过滤器,但你也可以创建自己的过滤器。
软删除过滤器用于当查询数据库时自动过滤(从结果中提取)已经删除的实体。如果实体应该是软删除的,那么它必须实现只定义了 IsDelete 属性的 ISoftDelete 接口,例如:
public class Person : Entity, ISoftDelete { public virtual string Name { get; set; } public virtual bool IsDeleted { get; set; } }
实际上, Person 实体并没有从数据库中删除,只是当要删除它时将它的 IsDelete 属性设置成了true。当使用 IRepository.Delete 方法时,ABP会自动处理(你可以手动设置IsDelete为true,但是Delete方法更自然且更受人欢迎)。
实现了ISoftDelete之后,当从数据库获取Person的列表时,已经软删除的person是不会检索到的。这里有一个使用了person仓储获得所有person的例子:
public class MyService { private readonly IRepository<Person> _personRepository; public MyService(IRepository<Person> personRepository) { _personRepository = personRepository; } public List<Person> GetPeople() { return _personRepository.GetAllList(); } }
GetPeople方法只会获得IsDeleted=false(没有删除)的Person实体。所有的仓储方法和导航属性都会正确工作。我们也可以添加一些其他的Where条件,连接等等。它会自动将IsDeleted=false添加到生成的Sql查询中。
ISoftDelete过滤器始终是开启的,除非你显式关闭了它。
额外注意:如果实现了 IDeletionAudited (它继承了ISoftDelete),那么ABP会自动设置删除时间和删除者的id。
如果你生成的是多租户应用(在一个数据库中存储所有租户的数据),那么你肯定不想一个租户意外地看到了其他租户的数据。这种情况你可以实现 IMustHaveTenant 。例如:
public class Product : Entity, IMustHaveTenant { public int TenantId { get; set; } public string Name { get; set; } }
IMustHaveTenant定义了 TenantId 来区分不同的租户实体。ABP使用了 IAbpSession 来获得当前的TenantId,而且自动过滤当前租户的查询。
IMustHaveTenant默认是开启的。如果当前的用户没有登录到系统或者当前的用户是一个租主用户(租主用户是可以管理租户和租户数据的更高级用户),ABP会自动关闭IMustHaveTenant过滤器。因此,所有租户的所有数据都可以被检索到。注意这是没有涉及到安全的情况,你应该总是要对敏感的数据进行授权。
如果一个实体类是租户和租主共享的(这意味着一个实体对象可能被一个租户或者租主拥有),那么你可以使用IMayHaveTenant过滤器。 IMayHaveTenant 接口定义了 ITenantId 但是它是 nullable 。
public class Role : Entity, IMayHaveTenant { public int? TenantId { get; set; } public string RoleName { get; set; } }
如果TenantId的值是 null ,就意味着这是一个 租主 实体;如果值不为null,就意味着该实体被一个 租户 拥有,该租户的Id就是该TenantId。ABP使用了IAbpSession来获得当前的TenantId。IMayHaveTenant过滤器不像IMustHaveTenant过滤器那样常用,但是,对于租户和租户公用的结构,你可能需要它。
IMayHaveTenant总是开启的,除非你显式关闭了它。
你可以通过调用 DisableFilter 方法来为每个工作单元关闭过滤器,如下所示:
var people1 = _personRepository.GetAllList(); using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) { var people2 = _personRepository.GetAllList(); } var people3 = _personRepository.GetAllList();
DisableFilter方法以字符串获得一个或更多的过滤器。AbpDataFilters.SoftDelete包含了ABP标准软删除过滤器的名称的常量字符串。
people2可以获得已经删除的person实体,然而people1和people3只会获得没有删除的实体。使用 using 语句,你可以在一个 区域(scope) 中关闭过滤器。如果你没有使用using语句,那么过滤器在当前工作单元结束前都是关闭的,除非你显式再次开启它。
你可以像上面的例子那样注入 IUnitOfWorkManager 使用。此外,你也可以在应用服务(它派生自ApplicationService类)中使用 CurrentUnitOfWork 属性作为快捷方式。
如果过滤器是开启的,当你在using语句中调用DisableFilter方法时,那么过滤器会关闭,然后,当using语句结束时,它会自动再次开启。但是如果在使用using语句之前过滤器已经关闭了,那么DisableFilter实际上什么都不会做,而且在using语句结束后仍然是关闭的。
你可以在工作单元中使用 EnableFilter 方法来开启一个过滤器,和DisableFilter很相似。EnableFilter也返回disable来自动再次关闭该过滤器。
过滤器是可以带参数的。IMustHaveTenant过滤器就是这些过滤器类型的一个例子,因为当前的租户Id要在运行时确定。对于这些过滤器,如果需要的话,我们可以改变过滤器的值。例如:
CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42);
另一个例子:为IMayHaveTenant过滤器设置tenantId值:
CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42);
SetFilterParameter方法也返回一个IDisposable。因此,我们可以在一个using语句中使用它,在using语句结束时自动 还原旧值 。
要创建一个自定义过滤器并集成到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 ,因此我们可以使用它的规则来定义该过滤器。在我们的 DbContext 类中,我们重写了 OnModelCreating ,而且定义过滤器如下所示:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, 0); }
这里,“PersonFilter”是过滤器唯一的名字。第二个参数定义了过滤器接口和personId过滤器参数(如果过滤器没有参数那么就不需要了)。最后一个参数是personId的默认值。
最后一件事,我们应该在模块的PreInitialize方法中将该过滤器注册到ABP的工作单元系统中。
Configuration.UnitOfWork.RegisterFilter("PersonFilter", false);
第一个参数是我们上面定义的过滤器的唯一的名字。第二个参数表明默认是开启的还是关闭的。声明这么一个参数化的过滤器之后,我们可以通过在运行时给它提供值来使用了。
using (CurrentUnitOfWork.EnableFilter("PersonFilter")) { CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42); var phones = _phoneRepository.GetAllList(); //... }
我们可以从一些源中获得personId而不是静态代码中。上面的例子是对于参数化的过滤器来说的。过滤器可以有零个或更多的参数。如果它没有参数,就不需要设置过滤器的值了。此外,如果它默认是开启的,它就不需要手动开启了(当然,我们还可以关闭它)。
关于动态数据过滤器的更多信息,请看 github上的文档 。
我们也可以为安全,激活/未激活的实体等创建自定义的过滤器。
ABP已经实现了EF和NH的数据过滤。对于其他的ORM还不可用。但是实际上,只要你使用 仓储 获得数据,你就可以为绝大多数情况模拟数据过滤。对于这种情况,如果需要的话,你可以创建自定义的仓储,然后重写 GetAll 和其他的数据检索方法。