问题描述
我正在考虑拥有单独的数据库(每个公司一个)与一个多租户数据库(所有公司).条件:
I'm weighing up having separate DBs (one per company) vs one multi-tenanted DB (with all companies). Criteria:
- 用户只能属于一个公司,而不能访问其他公司的文件.
- 系统管理员需要维护所有公司的数据库.
- 公司/租户数-从数百到数万
- 对于所有公司/租户,只有一个带有身份验证的入口点(它将解析租户并将其寻址到正确的数据库).
问题#1 .在RavenDB中设计多租户数据库是否有任何良好实践"?
Question #1. Are there any "good practices" for designing a multi-tenanted database in RavenDB?
对于MongoDB,有类似的帖子. RavenDB会一样吗?更多记录会影响索引,但会是否有可能使某些租户遭受其他租户对索引的积极使用?
There is a similar post for MongoDB. Would it be the same for RavenDB?More records will affect indexes, but would it potentially make some tenants suffer from active usage of an index by other tenants?
如果我要为 RavenDB 设计一个多租户数据库,那么我将实现视为
If I were to design a multi-tenanted DB for RavenDB, then I see the implementation as
- 每个公司/租户都有一个标签,因此一个公司的所有用户都有权使用该公司标签,并且所有顶级文档都具有该标签(请参阅 Auth Bundle上的KB )
- 具有一个租户ID"标签作为每个文档ID的前缀(由于官方推荐以使用顺序标识符,我对在服务器上生成ID感到满意)
- have a Tag per Company/Tenant, so all users of one company have permission to the company tag and all top-level documents have the tag (see KB on Auth Bundle)
- have a Tenant ID tag as a prefix for each Document ID (due to the official recommendation to use sequential identifiers and I'm happy with generating IDs on the server)
问题#2.1 .正在标记使用授权包的最佳方法解决用户的权限并阻止访问其他租户的文档?
Question #2.1. Is tagging the best way to utilise the Authorization Bundle for resolving users' permissions and prevent accessing documents of other tenants?
问题#2.2 .在顶级文档的ID前缀中具有租户ID"有多重要?我想,这里的主要考虑因素是通过标签解决权限后出现的性能问题,或者我丢失了某些东西?
Question #2.2. How important is to have the Tenant ID in the ID prefix of top-level documents?I guess, the main consideration here is performance once permissions gets resolved via tags or I'm missing something?
推荐答案
我试图通过编辑他的帖子来让@AyendeRahien参与技术实现的讨论是不成功的:),所以下面我将解决上面的问题:
My attempt to engage @AyendeRahien in a discussion of the technical implementation by editing his post was unsuccessful :), so below I'll address my concerns from the above:
1.多租户数据库与多个数据库
以下是 Ayende的想法关于多租户.
我认为问题可以归结为
- 预期的租户数量 每个租户的
- 数据库大小.
简单来说,如果有几个具有大量记录的租户,将租户信息添加到索引中将不必要地增加索引大小,并且处理租户ID会带来一些您宁愿避免的开销,所以继续然后是两个DB.
Simply, in a case of a couple of tenants with a huge number of records, adding the tenant information into the indexes will unnecessary increase the index size and handling the tenant ID will bring some overhead you'd rather avoid, so go for two DBs then.
2.多租户数据库的设计
第1步.在要支持多租户的所有持久性文档中添加TenantId
属性.
Step #1. Add TenantId
property to all persistent documents you want to support multi-tenancy.
/// <summary>
/// Interface for top-level entities, which belong to a tenant
/// </summary>
public interface ITenantedEntity
{
/// <summary>
/// ID of a tenant
/// </summary>
string TenantId { get; set; }
}
/// <summary>
/// Contact information [Tenanted document]
/// </summary>
public class Contact : ITenantedEntity
{
public string Id { get; set; }
public string TenantId { get; set; }
public string Name { get; set; }
}
第2步.为Raven的会话(IDocumentSession
或IAsyncDocumentSession
)来处理多租户实体.
Step #2. Implement facade for the Raven's session (IDocumentSession
or IAsyncDocumentSession
) to take care of multi-tenanted entities.
下面的示例代码:
/// <summary>
/// Facade for the Raven's IAsyncDocumentSession interface to take care of multi-tenanted entities
/// </summary>
public class RavenTenantedSession : IAsyncDocumentSession
{
private readonly IAsyncDocumentSession _dbSession;
private readonly string _currentTenantId;
public IAsyncAdvancedSessionOperations Advanced => _dbSession.Advanced;
public RavenTenantedSession(IAsyncDocumentSession dbSession, ICurrentTenantIdResolver tenantResolver)
{
_dbSession = dbSession;
_currentTenantId = tenantResolver.GetCurrentTenantId();
}
public void Delete<T>(T entity)
{
if (entity is ITenantedEntity tenantedEntity && tenantedEntity.TenantId != _currentTenantId)
throw new ArgumentException("Attempt to delete a record for another tenant");
_dbSession.Delete(entity);
}
public void Delete(string id)
{
throw new NotImplementedException("Deleting by ID hasn't been implemented");
}
#region SaveChanges & StoreAsync---------------------------------------
public Task SaveChangesAsync(CancellationToken token = new CancellationToken()) => _dbSession.SaveChangesAsync(token);
public Task StoreAsync(object entity, CancellationToken token = new CancellationToken())
{
SetTenantIdOnEntity(entity);
return _dbSession.StoreAsync(entity, token);
}
public Task StoreAsync(object entity, string changeVector, string id, CancellationToken token = new CancellationToken())
{
SetTenantIdOnEntity(entity);
return _dbSession.StoreAsync(entity, changeVector, id, token);
}
public Task StoreAsync(object entity, string id, CancellationToken token = new CancellationToken())
{
SetTenantIdOnEntity(entity);
return _dbSession.StoreAsync(entity, id, token);
}
private void SetTenantIdOnEntity(object entity)
{
var tenantedEntity = entity as ITenantedEntity;
if (tenantedEntity != null)
tenantedEntity.TenantId = _currentTenantId;
}
#endregion SaveChanges & StoreAsync------------------------------------
public IAsyncLoaderWithInclude<object> Include(string path)
{
throw new NotImplementedException();
}
public IAsyncLoaderWithInclude<T> Include<T>(Expression<Func<T, string>> path)
{
throw new NotImplementedException();
}
public IAsyncLoaderWithInclude<T> Include<T, TInclude>(Expression<Func<T, string>> path)
{
throw new NotImplementedException();
}
public IAsyncLoaderWithInclude<T> Include<T>(Expression<Func<T, IEnumerable<string>>> path)
{
throw new NotImplementedException();
}
public IAsyncLoaderWithInclude<T> Include<T, TInclude>(Expression<Func<T, IEnumerable<string>>> path)
{
throw new NotImplementedException();
}
#region LoadAsync -----------------------------------------------------
public async Task<T> LoadAsync<T>(string id, CancellationToken token = new CancellationToken())
{
T entity = await _dbSession.LoadAsync<T>(id, token);
if (entity == null
|| entity is ITenantedEntity tenantedEntity && tenantedEntity.TenantId == _currentTenantId)
return entity;
throw new ArgumentException("Incorrect ID");
}
public async Task<Dictionary<string, T>> LoadAsync<T>(IEnumerable<string> ids, CancellationToken token = new CancellationToken())
{
Dictionary<string, T> entities = await _dbSession.LoadAsync<T>(ids, token);
if (typeof(T).GetInterfaces().Contains(typeof(ITenantedEntity)))
return entities.Where(e => (e.Value as ITenantedEntity)?.TenantId == _currentTenantId).ToDictionary(i => i.Key, i => i.Value);
return null;
}
#endregion LoadAsync --------------------------------------------------
#region Query ---------------------------------------------------------
public IRavenQueryable<T> Query<T>(string indexName = null, string collectionName = null, bool isMapReduce = false)
{
var query = _dbSession.Query<T>(indexName, collectionName, isMapReduce);
if (typeof(T).GetInterfaces().Contains(typeof(ITenantedEntity)))
return query.Where(r => (r as ITenantedEntity).TenantId == _currentTenantId);
return query;
}
public IRavenQueryable<T> Query<T, TIndexCreator>() where TIndexCreator : AbstractIndexCreationTask, new()
{
var query = _dbSession.Query<T, TIndexCreator>();
var lastArgType = typeof(TIndexCreator).BaseType?.GenericTypeArguments?.LastOrDefault();
if (lastArgType != null && lastArgType.GetInterfaces().Contains(typeof(ITenantedEntity)))
return query.Where(r => (r as ITenantedEntity).TenantId == _currentTenantId);
return query;
}
#endregion Query ------------------------------------------------------
public void Dispose() => _dbSession.Dispose();
}
如果您同时需要Include()
,则上面的代码可能需要一些帮助.
The code above may need some love if you need Include()
as well.
我的最终解决方案不使用我之前建议的RavenDb v3.x的监听器(有关原因,请参见我的评论)或事件 RavenDb v4(因为很难在那里修改查询).
My final solution doesn't use listeners for RavenDb v3.x as I suggested earlier (see my comment on why) or events for RavenDb v4 (because it's hard to modify the query in there).
当然,如果您编写补丁,您必须手动处理多租户的JavaScript函数.
Of course, if you write patches of JavaScript functions you'd have have to handle multi-tenancy manually.
这篇关于多租户数据库.文件ID和授权的策略的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!