【问题标题】:Multi-tenanted DB. Strategy for Document ID and authorization多租户数据库。文档 ID 和授权策略
【发布时间】:2017-11-14 15:30:45
【问题描述】:

我正在权衡拥有单独的数据库(每家公司一个)与一个多租户数据库(所有公司)。标准:

  • 一个用户只能属于一个公司,不能访问其他公司的文档。
  • 系统管理员需要维护所有公司的数据库。
  • 公司/租户数量 - 从数百到数万
  • 所有公司/租户都有一个身份验证入口点(它将解析租户并将其寻址到正确的数据库)。

问题 #1。在 RavenDB 中设计多租户数据库有什么“好的做法”吗?

有一个类似的post for MongoDB。 RavenDB 也一样吗? 更多记录会影响indexes,但是否会导致部分租户受到其他租户主动使用索引的影响?


如果我要为 RavenDB 设计一个多租户数据库,那么我将实现视为

  • 每个公司/租户都有一个标签,因此一个公司的所有用户都有权使用该公司标签,并且所有顶级文档都有该标签(请参阅KB on Auth Bundle
  • 有一个租户 ID 标签作为每个文档 ID 的前缀(由于 official recommendation 使用顺序标识符,我很高兴在服务器上生成 ID)

问题 #2.1。标记是利用Authorization Bundle 解决用户权限并防止访问其他租户文档的最佳方式吗?

问题 #2.2。在顶级文档的 ID 前缀中包含租户 ID 有多重要? 我想,这里的主要考虑因素是一旦通过标签解决权限或我遗漏了什么?

【问题讨论】:

标签: c# .net ravendb multi-tenant nosql


【解决方案1】:

如果您打算拥有数百家公司,那么每个公司一个数据库就可以了。 如果您将拥有数万个,那么您希望将它们全部放在一个数据库中。

一个数据库可以消耗大量资源,拥有大量资源可能比单个更大的数据库昂贵得多。

我建议不要使用授权包,它需要我们进行O(N) 过滤。最好直接在查询中添加TenantId = XYZ,可能通过查询监听器。

不要太担心顺序标识符。它们会产生影响,但除非您每秒生成数万个,否则它们并不那么重要。


查看处理多租户的侦听器示例。

将当前租户 ID 添加到所有查询的查询侦听器(过滤掉其他租户的条目):

public class TenantedEntityQueryListener : IDocumentQueryListener
{
    private readonly ICurrentTenantIdResolver _resolver;

    public TenantedEntityQueryListener(ICurrentTenantIdResolver resolver) : base(resolver) 
    {
        _resolver = resolver;
    }

    public void BeforeQueryExecuted(IDocumentQueryCustomization customization)
    {
        var type = customization.GetType();
        var entityType = type.GetInterfaces()
                             .SingleOrDefault(i => i.IsClosedTypeOf(typeof(IDocumentQuery<>))
                                                || i.IsClosedTypeOf(typeof(IAsyncDocumentQuery<>)))
                             ?.GetGenericArguments()
                             .Single();
        if (entityType != null && entityType.IsAssignableTo<ITenantedEntity>())
        {
            // Add the "AND" to the the WHERE clause 
            // (the method has a check under the hood to prevent adding "AND" if the "WHERE" is empty)
            type.GetMethod("AndAlso").Invoke(customization, null);
            // Add "TenantId = 'Bla'" into the WHERE clause
            type.GetMethod( "WhereEquals", 
                            new[] { typeof(string), typeof(object) }
                          )
                .Invoke(customization,
                    new object[]
                    {
                        nameof(ITenantedEntity.TenantId),
                        _resolver.GetCurrentTenantId()
                    }
                );
        }
    }
}

将当前租户 ID 设置为所有租户实体的商店侦听器:

public class TenantedEntityStoreListener : IDocumentStoreListener
{
    private readonly ICurrentTenantIdResolver _resolver;

    public TenantedEntityStoreListener(ICurrentTenantIdResolver resolver) : base(resolver)
    {
        _resolver = resolver;
    }

    public bool BeforeStore(string key, object entityInstance, RavenJObject metadata, RavenJObject original)
    {
        var tenantedEntity = entityInstance as ITenantedEntity;
        if (tenantedEntity != null)
        {
            tenantedEntity.TenantId = _resolver.GetCurrentTenantId();
            return true;
        }

        return false;
    }

    public void AfterStore(string key, object entityInstance, RavenJObject metadata) {}
}

接口,由支持多租户的顶级实体实现:

public interface ITenantedEntity
{
    string TenantId { get; set; }
}

【讨论】:

  • 感谢您的及时回复!我添加了一个你提到的听众的例子。希望,有道理。顺便说一句,当您说“不使用授权包”时,您是指仅在单个文档上定义的权限还是检查用户/角色的操作权限?我喜欢开箱即用的角色和权限层次结构,并希望在我的应用中采用它。
  • 不幸的是,如果结果类型与文档类型不同,我上面对IDocumentQueryListener 的实现将不起作用。例如。 session.Query&lt;Entity&gt;().Select(e =&gt; e.Id).ToList() 不起作用,当 session.Query&lt;Entity&gt;().ToList().Select(e =&gt; e.Id) 起作用时(Entity 实现 ITenantedEntity)。这是因为customization 参数的类型为AsyncDocumentQuery&lt;string&gt; 而不是AsyncDocumentQuery&lt;Entity&gt;。 @AyendeRahien,您能否给我一个提示,说明如何在我的示例中挖掘 Query&lt;&gt; 的泛型类型(而不是 Select&lt;&gt; 类型)?
  • 上述解决方案不适用于 RavenDB v4。 IDocumentQueryListener 的替换是 OnBeforeQuery event,您可以在其中访问查询(通过 BeforeQueryExecuted),但它只是一个 JavaScript 字符串。没有用于操作这些字符串的 API。)
【解决方案2】:

更新(2021 年 9 月):4 年后我制作了:


原答案

我试图通过编辑他的帖子让@AyendeRahien 参与讨论技术实现的尝试没有成功:),所以下面我将从上面解决我的担忧:

1.多租户数据库与多个数据库

这里有一些关于多租户的Ayende's thoughts

在我看来问题归结为

  • 预计租户数量
  • 每个租户的数据库大小。

简单来说,在有大量记录的几个租户的情况下,将租户信息添加到索引中将不必要地增加索引大小,并且处理租户 ID 会带来一些您希望避免的开销,所以去吧那么对于两个数据库。

2。多租户数据库的设计

第 1 步。将TenantId 属性添加到要支持多租户的所有持久文档中。

/// <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 的 sessionIDocumentSessionIAsyncDocumentSession)实施 facade 以处理多租户实体。

示例代码如下:

/// <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(),上面的代码可能需要一些爱。

我的最终解决方案没有像我之前建议的那样将 listeners 用于 RavenDb v3.x(请参阅 my comment 了解原因)或 events 用于 RavenDb v4(因为很难修改其中的查询)。

当然,如果您编写 patches 的 JavaScript 函数,则必须手动处理多租户。

【讨论】:

    猜你喜欢
    • 2020-12-07
    • 1970-01-01
    • 2018-11-02
    • 2016-05-20
    • 2019-04-16
    • 1970-01-01
    • 1970-01-01
    • 2023-03-20
    • 2017-10-10
    相关资源
    最近更新 更多