数据过滤器
介绍
软删除模式是常用的模式,这种模式并没有从数据库中删除实体而是打上'deleted'的标记。所以,如果实体被软删除,那么它不应该被意外的提取到应用中。为了保证这种事情不会发生,当我们选择实体时,应该添加入‘IsDeleted=false’这样的SQL where条件。这是一个乏味、易忘但很重要的工作。所以,应该由自动的方式来完成。
ABP提供了数据过滤器,可以用来基于一些规则自动过滤查询。这有一些预定义的过滤器,也可以创建自己的过滤器。
预定义过滤器
ISoftDelete
Sofe-delete过滤器用来当查询数据库时自动过滤(从结果中提取)已删除的实体。如果一个实体为软删除模式的,它必须实现ISoftDelete接口,这个接口只有IsDeleted属性。示例:
public class Person : Entity, ISoftDelete
{
public virtual string Name { get; set; } public virtual bool IsDeleted { get; set; }
}
Person实体不会真正从数据库中删除,而是当需要删除它时,将IsDeleted属性设置为true。当使用IRepository.Delete方法(你可以手动设置IsDeleted为true,但是Delete方法是更自然、更好的方式)时,ABP自动完成这个操作。
实现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方法仅获取Isdeleted=false(没有删除)的person实体。所有的仓储方法和导航属性都可以正常工作。我们可以添加一些其他的Where条件。它会自动添加IsDeleted=false条件到生成的查询语句中。
何时使用?
ISoftDelete过滤器除非显示的禁用它,否则它总是启用的。
边注:如果你实现了IDeletionAudited(它扩展了ISoftDelete)接口,ABP会自动设置删除时间和删除用户id。
IMustHaveTenant
如果你创建了多租户的应用,并把所有的租户数据都存放在一个数据库中,你肯定不想一个租户可以看到其他租户的数据。在这种情况下,你可以实现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过滤器。IMayHaveTenant接口定义了TenantId,但它是nullable。
public class Role : Entity, IMayHaveTenant
{
public int? TenantId { get; set; } public string RoleName { get; set; }
}
null值意味着是一个租主实体,非null值以为了这个实体数据租户且租户id为TenantId。ABP默认使用IAbpSession获取当前租户的Id。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将包含删除的people,但people1和people3只有未删除的people。使用using语句,你可以在一个范围内禁用过滤器。如果你不使用using语句,过滤器将禁用到当前工作单元的结尾或者你重新显示的启用它。
你可以注入IUnitOfWorkManager并如在本例中那样使用。你也可以使用CurrentUnitOfWork属性作为捷径,如果你的类集成了一些特殊的基类(如ApplicationService、AbpController、AbpApiController...)。
关于using语句
当你使用using语句调用DisableFilter方法时,如果这时过滤器可用的,那么调用方法之后过滤器是禁用的,using语句之后会自动重新启用过滤器。但是,如果在using语句之前过滤器已经是禁用的,那么DisableFilter方法实际上什么也没做,using语句之后过滤器仍然保持禁用。
关于多租户
你可以禁用租户过滤器来查询所有租户的数据。但是记住,这种方法只对单数据库方式有效。如果每一个租户都有单独的数据库,禁用过滤器将不会查询所有租户的所有数据,因为他们在不同的数据库中,甚至在不同的服务器上。参见多租户文档了解更多信息。
全局禁用过滤器
如果需要,可以全局禁用预定义的过滤器。例如,全局禁用软删除过滤器,在你模块的PreInitialize方法中添加下面的代码:
Configuration.UnitOfWork.OverrideFilter(AbpDataFilters.SoftDelete, false);
启用过滤器
你可以在工作单元里使用EnableFilter方法来启用过滤器,和DisableFilter相似(功能相反)。EnableFilter方法返回disposable用来在using语句中使用,如果需要,可以自动重新禁用过滤器。
设置过滤器参数
过滤器可以是参数化的。IMustHaveTenant过滤器就是这种类型的一个示例,因为当前租户id是在运行时决定的。对于这种过滤器,如果需要的话我们可以改变过滤器的值,例如:
CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", );
另一个示例:设置IMayHaveTenant过滤器的tenantId值:
urrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, );
SetFilterParameter方法也返回一个IDisposable接口。所以,我们可以使用using语句自动重建之前的值,在using语句之后。
SetTenantId方法
当你使用SetFilterParameter方法改变MayHaveTenant和MustHaveTenant过滤器的值时,这有一个更好的方式更改租户过滤器:SetTenantId()。SetTenantId为这两个过滤器更改参数值,并且对单数据库和每个租户一个数据库两种类型都有效。所以,建议使用SetTenantId改变租户过滤器的参数值。参见多租户文档了解更多信息。
ORM集成
预定义过滤器对NHibernate、EntityFramework 6.x和Entity Framework Core同样有效。目前,你只能为Entity Framework 6.x定义自定义过滤器。
Entity Framework
对Entity Framework集成,使用EntityFramework.DynamicFilters类库实现自动数据过滤。
为了为Entity Framework创建一个自定义过滤器并集成到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; }
}
我们使用它的规则定义过滤器。在我们的DbContext类,我们重写OnModelCreating并定义过滤器,如下:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, );
}
“PersonFilter”是过滤器唯一的名称。第二个参数定义了过滤器接口和personId过滤器参数(如果过滤器没有参数可以不需要),最后的参数是personId的默认值。
最后一件事情,我们必须在模块的PreInitialize方法中注册这个过滤器到ABP的工作单元系统中。
Configuration.UnitOfWork.RegisterFilter("PersonFilter", false);
第一个参数是和我们之前定义的同样的名字。第二个参数标示这个过滤器默认是启用还是禁用。声明这么一个参数化过滤器之后,我们可以在运行时提供给他参数值并使用它。
using (CurrentUnitOfWork.EnableFilter("PersonFilter"))
{
using(CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", ))
{
var phones = _phoneRepository.GetAllList();
//...
}
}
我们可以从一些源获取到personId而不是静态编码。上面的例子是参数化过滤器的。过滤器可以没有或有更多的参数。如果没有参数就不需要设置过滤器参数值。如果默认是启用的就不需要手动启用它(当然,我们可以禁用它)。
EntityFramework.DynaamicFilters文档
更过关于动态数据过滤器的信息,参见github页上的文档:https://github.com/jcachat/EntityFramework.DynamicFilters
我们可以为安全、激活/失活实体等等创建自定义过滤器。
其他ORMs
对于Entity Framework Core和NHibernate,数据过滤是在仓储级别实现的。这就意味着,它只有当你通过仓储查询时才会过滤。如果你直接使用DbContext(对于 EF Core)或通过自定义SQL,你需要自己处理过滤。
Configuration.UnitOfWork.OverrideFilter(AbpDataFilters.SoftDelete, false); 返回主目录