在开发中,我们常会遇到一个问题,就是归属查询问题,比如只能查看我自己的,往往这个时候还附带了一个规则,比如有人是在这个规则之外的!
1.只能查看创建者自己创建的资料
2.只能查看我店铺的相关内容,不能查看别人店铺的
3.只能查看我部门的相关信息等
可能你会说,直接查询的时候加条件即可!道理就是这么个道理,哪怕是多租户其实也是这样实现的!
为啥不直接用多租户呢
多租户在我的项目中,用得非常非常少,更多的用的是灵活的协作模式,比如多店铺模式,多商户号模式等!
1.目前的多租户限制比较多,如果引入了,变成店铺+多租户,多了一层关系,别和我说扩展,我感觉扩展还是不如重写来得直接!
2.很多查询,其实并不是严格的多租户模式,比如有些组织,或者个人拥有多个店铺太正常了
3.范围限定,你不能多租户套多租户吧,但是实际需求中往往很多限定的,比如只能查看他自己创建的东西,外面又套了一层店铺
在PasteDocument项目中,有这么一个需求,我只能查看我组织的信息,为啥我没用多租户做呢,当时也是考虑使用多租户的,后面放弃了,我宁愿每个表都加一个字段CompanyId
1.Company本身,我有很多自定义的规则
2.多租户用的是Guid,实际中给我的感觉数据库如果没有必要还是不要使用Guid作为主键的好,其实int/long完全够用,当然Guid也有他的优点所在,比如别人猜不到你的日增长,至于说基于id猜数据的,我只能说是你的权限有问题,而不是数据本身!
3.不够灵活,因为我一个用户率属于多个组织太正常了
4.后续扩展考虑
综上,你看需求不就来了么,好了,直接上代码!
居然有多租户这个模式,那直接按照他的思路来改即可,对吧!
下载ABP的源码后,我们在AbpDbContext中看到如下代码,主要是针对多租户的
protected virtual Expression<Func<TEntity, bool>>? CreateFilterExpression<TEntity>()
where TEntity : class
{
Expression<Func<TEntity, bool>>? expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");
}
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;
expression = expression == null ? multiTenantFilter : QueryFilterExpressionHelper.CombineExpressions(expression, multiTenantFilter);
}
return expression;
}
注意看上面的2个被引用的参数
IsMultiTenantFilterEnabled:是否需要过滤多租户
CurrentTenantId:当前的多租户是多少
然后上面的函数意思是基于2个变量生成一个表达式!
看看谁调用他了
protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (mutableEntityType.BaseType == null && ShouldFilterEntity<TEntity>(mutableEntityType))
{
var filterExpression = CreateFilterExpression<TEntity>();
if (filterExpression != null)
{
var abc = modelBuilder.Entity<TEntity>();
modelBuilder.Entity<TEntity>().HasAbpQueryFilter(filterExpression);
}
}
}
感觉可以略过,意思就是判断Entity是否继承于设定的IMultiTenant
protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
{
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
return false;
}
我们再看看ConfigureGlobalFilters被谁调用了
protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (mutableEntityType.IsOwned())
{
return;
}
if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity)))
{
return;
}
modelBuilder.Entity<TEntity>().ConfigureByConvention();
ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
}
这个判断,啥意思?防止自己调用自己?那估计是有递归查之类的,别管,继续上一级
private static readonly MethodInfo ConfigureBasePropertiesMethodInfo
= typeof(AbpDbContext<TDbContext>)
.GetMethod(
nameof(ConfigureBaseProperties),
BindingFlags.Instance | BindingFlags.NonPublic
)!;
这里用了一个,,,,
反射吧!
typeof xxx GetMethod
好家伙,直接避免直接接触啊!
看看谁调用他了
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
TrySetDatabaseProvider(modelBuilder);
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
ConfigureBasePropertiesMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType });
ConfigureValueConverterMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType });
ConfigureValueGeneratedMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType });
}
}
看到上面的,有感觉没有?
protected override void OnModelCreating(ModelBuilder modelBuilder)
看上面代码,意思就是在DbContext的时候,创建对应的过滤规则,这里有一个关键点!!!
就是上面说的2个变量IsMultiTenantFilterEnabled和CurrentTenantId
然后是表达式
Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, “TenantId”) == CurrentTenantId;
翻译过来,大致是这样的
where IsMultiTenantFilterEnabled==false || TenantId ='xxxxxxxxxx'
也就是说,无论IsMultiTenantFilterEnabled是true/false,这个表达式都是存在的
我之前的理解应该是,有启用的时候才拼接这个表达式,没有则没有这个表达式!
这个是理解误区之一!!
其实这个不是我的误区,是那几个AI的,为啥呢?
我问了AI这个功能如何实现,直接给我全部往OnModelCreating中写
然后我问了一句,不对劲啊,On Model Creating的字面意思就是模块创建的时候,难不成一直创建???
然后AI就进入死循环无法自拔了!!!
这里有一个重要点
1.DbContext是瞬时的,也就是每次都New一个
2.OnModelCreating只有第一次会执行,不是每次new DbContext都会执行!
3.也就是关键的2个变量IsMultiTenantFilterEnabled/CurrentTenantId其实是和DbContext一样的,瞬时的,也就是当前!!!
结合上面的知识和误区一,误区二
我们可以把当前需求注入到DbContext中,你看官方AbpDbContext也是注入了
protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;
protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;
protected virtual bool IsSoftDeleteFilterEnabled => DataFilter?.IsEnabled<ISoftDelete>() ?? false;
public ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
那我就把ICurrentUser注入,然后其他的按照多租户的流程走一遍即可!
以下用我的测试项目来做例子,我做一个过滤UserId的
这个呢直接参考IMultiTenant
/// <summary>
///
/// </summary>
public interface IUserIdEntity
{
/// <summary>
///
/// </summary>
int UserId { get; set; }
}
然后改造我的SqliteDbContext
using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using PasteTest.usermodels;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Users;
namespace PasteTest
{
/// <summary>
///
/// </summary>
[ConnectionStringName(PasteTestDbProperties.SqliteConnectionStringName)]
public class SqliteDbContext : AbpDbContext<SqliteDbContext>, IPasteTestDbContext
{
/* Add DbSet for each Aggregate Root here. Example:
* public DbSet<Question> Questions { get; set; }
*/
private readonly ICurrentUser _currentUser;
private bool LimitUserId { get; set; } = false;
private int CurrentUserId { get; set; } = 0;
/// <summary>
///
/// </summary>
/// <param name="options"></param>
/// <param name="currentUser"></param>
public SqliteDbContext(DbContextOptions<SqliteDbContext> options, ICurrentUser currentUser)
: base(options)
{
_currentUser = currentUser;
if (_currentUser != null)
{
//这里是核心,主要判断当前的开关,当前登录用户的
var limit = _currentUser.FindClaim("LimitUser")?.Value.To<bool>();
if (limit.HasValue && limit.Value)
{
var userid = _currentUser.FindClaim("UserId")?.Value.To<int>();
LimitUserId = true;
CurrentUserId = userid.Value;
}
}
}
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePasteTest();
foreach (var entityType in builder.Model.GetEntityTypes())
{
if (typeof(IUserIdEntity).IsAssignableFrom(entityType.ClrType))
{
MyPropertiesMethodInfo.MakeGenericMethod(entityType.ClrType).Invoke(this, new object[] { builder, entityType });
}
}
}
#region 以下作用为了在查询的时候添加过滤,和当前用户有关
//注意SqliteDbContext要进行实际替换
private static MethodInfo MyPropertiesMethodInfo = typeof(SqliteDbContext).GetMethod(nameof(ConfigureMyProperties), BindingFlags.Instance | BindingFlags.NonPublic);
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="modelBuilder"></param>
/// <param name="mutableEntityType"></param>
protected void ConfigureMyProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) where TEntity : class
{
if (mutableEntityType.IsOwned())
{
return;
}
if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity)))
{
return;
}
MyConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType);
}
/// <summary>
///
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="modelBuilder"></param>
/// <param name="mutableEntityType"></param>
protected void MyConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) where TEntity : class
{
if (mutableEntityType.BaseType == null && typeof(IUserIdEntity).IsAssignableFrom(typeof(TEntity)))
{
var filterExpression = MyCreateFilterExpression<TEntity>();
if (filterExpression != null)
{
modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
}
}
}
/// <summary>
/// 判断是否要创建过滤表达式
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <returns></returns>
protected Expression<Func<TEntity, bool>> MyCreateFilterExpression<TEntity>()
{
Expression<Func<TEntity, bool>>? expression = null;// expression = base.CreateFilterExpression<TEntity>();
if (typeof(IUserIdEntity).IsAssignableFrom(typeof(TEntity)))
{
//下面内容注意按照实际情况替换
Expression<Func<TEntity, bool>> multiTenantFilter = e => !LimitUserId || EF.Property<int>(e, "UserId") == CurrentUserId;
expression = expression == null ? multiTenantFilter : NeedCombineExpressions(expression, multiTenantFilter);
}
//多个在这里追加
return expression;
}
/// <summary>
/// 表达式拼接
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression1"></param>
/// <param name="expression2"></param>
/// <returns></returns>
private static Expression<Func<T, bool>> NeedCombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
var parameter = Expression.Parameter(typeof(T));
var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);
var left = leftVisitor.Visit(expression1.Body);
var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
var right = rightVisitor.Visit(expression2.Body);
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left!, right!), parameter);
}
/// <summary>
///
/// </summary>
private class ReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;
public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}
public override Expression? Visit(Expression? node)
{
return node == _oldValue ? _newValue : base.Visit(node);
}
}
#endregion
/// <summary>
/// 用户信息
/// </summary>
public DbSet<UserInfo> UserInfo { get; set; }
/// <summary>
/// 权限信息
/// </summary>
public DbSet<RoleInfo> RoleInfo { get; set; }
/// <summary>
/// 角色信息
/// </summary>
public DbSet<GradeInfo> GradeInfo { get; set; }
/// <summary>
/// 角色权限
/// </summary>
public DbSet<GradeRole> GradeRole { get; set; }
/// <summary>
/// 用户绑定角色 解绑的直接删除数据
/// </summary>
public DbSet<UserGrade> UserGrade { get; set; }
/// <summary>
/// 测试表
/// </summary>
public DbSet<MyLinkTable> MyLinkTable { get; set; }
}
}
注意看构造函数的这部分代码
if (_currentUser != null)
{
//这里是核心,主要判断当前的开关,当前登录用户的
var limit = _currentUser.FindClaim("LimitUser")?.Value.To<bool>();
if (limit.HasValue && limit.Value)
{
var userid = _currentUser.FindClaim("UserId")?.Value.To<int>();
LimitUserId = true;
CurrentUserId = userid.Value;
}
}
这里主要就是判断当前用户是否需要过滤!
然后就是
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePasteTest();
foreach (var entityType in builder.Model.GetEntityTypes())
{
if (typeof(IUserIdEntity).IsAssignableFrom(entityType.ClrType))
{
MyPropertiesMethodInfo.MakeGenericMethod(entityType.ClrType).Invoke(this, new object[] { builder, entityType });
}
}
}
看看哪些Entity实现了IUserIdEntity
按照上面的,我用一个测试表来测试下
/// <summary>
/// 测试表
/// </summary>
public class MyLinkTable : Entity<int>, IUserIdEntity
{
/// <summary>
/// 名称
/// </summary>
[MaxLength(16)]
public string Name { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int Age { get; set; }
/// <summary>
/// 归属
/// </summary>
public int UserId { get; set; }
}
上面的代码感觉是不是少了啥,看到LimitUser,是不是少了一个写入的地方!!!
这个是我自定义的一个信息,所以我们需要在合适的地方写入!!!
假设有这么一个函数
/// <summary>
/// 获取
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet]
[TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "data", "edit" })]
public async Task<PagedResultDto<MyLinkTableListDto>> Page([FromQuery] InputQueryMyLinkTable input)
{
var _query = from a in _dbContext.MyLinkTable
select new MyLinkTableListDto
{
Name = a.Name,
Age = a.Age,
UserId = a.UserId,
Id = a.Id
};
_query = _query.WhereIf(!string.IsNullOrEmpty(input.word), x => x.Name.Contains(input.word));
var _pagedto = new PagedResultDto<MyLinkTableListDto>();
if (input.page == 1)
{
_pagedto.TotalCount = await _query.CountAsync();
}
var dataList = await _query
.OrderByDescending(x => x.Id)
.Page(input.page, input.size)
.AsNoTracking()
.ToListAsync();
if (dataList == null || dataList.Count == 0)
{
return null;// throw new PasteCodeException("没有查询到数据", 204);
}
_pagedto.Items = dataList;
return _pagedto;
}
查询不是在函数体么,所以我们在他之前实现 LimitUser 即可,也就是过滤器中!
在我上面的案例中,我只要在这个过滤器中按需写入即可
if (!token.CurrentIsRoot())
{
var claims = new[] { new Claim("LimitUser", "true"), new Claim("UserId", token.CurrentUserId().ToString()) };
var identity = new ClaimsIdentity(claims, "Current");//Simple
context.HttpContext.User = new ClaimsPrincipal(identity);
}
在这里,你就可以做很多花样了,因为在这里你可以获得当前用户的非常多东西,比如IP,Headers,Cookie等
我这里的判断是,只要不是超级用户,都需要做限定!!!
上面的代码可以分为2个部分,
对于1来说,我不管你2如何实现,我1只管按照条件打开还是关闭开关即可
对于2来说,我不管1如何实现的,我只要按照对应的值干活即可!
以上代码完成后,我们来运行看看
我先看看角色列表,这个角色表我是没有添加IUserIdEntity的,查询语句如下
SELECT "p"."Id", "p"."Code", "p"."Desc", "p"."IsEnable", "p"."Mark", "p"."Name"
FROM "PTGradeInfo" AS "p"
ORDER BY "p"."Id" DESC
LIMIT @__p_1 OFFSET @__p_0
再看看有添加IUserIdEntity的
SELECT "p"."Name", "p"."Age", "p"."UserId", "p"."Id"
FROM "PTMyLinkTable" AS "p"
WHERE @__ef_filter__p_0 OR ("p"."UserId" = @__ef_filter__CurrentUserId_1)
ORDER BY "p"."Id" DESC
LIMIT @__p_1 OFFSET @__p_0
看到区别没有?
WHERE @__ef_filter__p_0 OR ("p"."UserId" = @__ef_filter__CurrentUserId_1)
按照字面意思是添加了过滤了
我们加一些数据测试下!
数据的UserId字段我赋值的是当前用户
然后UserId=1的为超级用户
UserId=2的为非超级用户
我用超级用户查看如下
切换到非超级用户,查看如下
符合我的预期!!!
上面只是一个案例,实际中很多地方可以使用
比如上面说的 我的部门,我的店铺,我的创建等
你只要按照流程添加不同的接口,然后条件判断中添加对应的信息即可!!!