多租户基础设施

rust-ef 提供多租户数据隔离的能力(而非写死实现),核心机制是**全局查询过滤器** + **Scoped 生命周期**。

1. 线程安全契约

`DbContext` **非线程安全**——单个实例禁止跨线程共享。这是设计决策(对齐 EFCore),不是限制。

正确用法:Owned 解析(推荐)

每个请求/操作通过 `get_owned()` 获得专属 `DbContext` 实例,直接 `&mut self` 访问,无需锁:

let provider = ServiceCollection::new()
    .add_dbcontext(|o| o.use_sqlite("app.db"))
    .build()?;

// 推荐:owned 解析,直接 &mut self 访问
// rust-dix 0.6+: get_owned() 返回 Result<T, RdiError>
let mut ctx: DbContext = provider.get_owned()?;

共享解析(同一 Scope 内复用)

当多个消费者在同一 Scope 内需要共享同一实例时,使用 `scope.get()` 获取 `Arc<DbContext>`(仅 `&self` 访问):

use rust_dix::scope::ScopeFactory;  // create_scope() 在 ScopeFactory trait 上

let scope = provider.create_scope();
let ctx: Arc<DbContext> = scope.get()?;
// 同一 scope 内多次 get 返回同一实例

反模式:Arc<Mutex<DbContext>>

// ❌ 错误:共享会导致跟踪污染
let ctx = Arc::new(Mutex::new(ctx));
// Thread A 的 save_changes() 会提交 Thread B 挂起的变更

从根 ServiceProvider 直接解析

从根 `ServiceProvider` 直接 `get::<DbContext>()` 会在 root scope 缓存中复用同一实例(rust-dix 0.6+ 行为)。使用 `get_owned::<DbContext>()` 则每次返回新实例。

2. 多租户查询过滤器

通过 `ModelBuilder.has_query_filter` 注册租户隔离谓词,框架自动应用到多种 DML 操作。

注册过滤器

let mut ctx = DbContext::from_options(&options)?;
// from_options() 已自动调用 discover_entities(),无需手动注册

// 注册租户隔离过滤器(tenant_id = 1)
ctx.model().has_query_filter::<Blog>(
    linq!(filter |b: Blog| b.tenant_id == 1)
);

ctx.set::<Blog>();
ctx.ensure_created().await?;

> **注意**:过滤器必须在 `set::<T>()` 之前注册。`DbSet` 创建时从 `ModelBuilder` 读取过滤器并缓存。

覆盖范围

| 操作 | 是否自动过滤 | 说明 | |------|:---:|------| | SELECT | 是 | `query().to_list()` 自动追加 WHERE | | UPDATE | 是 | `save_changes()` 的 UPDATE 语句 AND 过滤器 | | DELETE | 是 | `save_changes()` 的 DELETE 语句 AND 过滤器 | | Navigation (Include) | 是 | 二级查询 SELECT 自动追加 WHERE | | INSERT | 否 | 用户在 `add()` 前手动设置 `tenant_id` |

INSERT 不自动过滤的原因

INSERT 时框架无法知道当前租户 ID(过滤器表达式中的值是注册时绑定的,而非每次操作动态传入)。用户需在 `add()` 前显式设置:

let blog = Blog {
    tenant_id: current_tenant_id,  // 显式设置
    title: "My Blog".into(),
    ..
};
ctx.add::<Blog>(blog);
ctx.save_changes().await?;

这体现了"框架提供能力,不写死实现"的原则——用户可以自由选择租户 ID 的来源(JWT claim、请求头、配置等)。

Navigation 自动过滤示例

// Blog 有 HasMany<Post>,Post 有 tenant_id 过滤器
// 加载 Blog 时 Include Posts,只返回同租户的 Posts
let blogs = linq!(ctx.set::<Blog>(); include b.posts)
    .to_list()
    .await?;
// blogs[0].posts 只含 tenant_id 匹配的 Posts

Navigation 过滤器由 `ModelBuilder.filters_by_table()` 收集所有实体的过滤器,按表名索引,通过 `Arc<HashMap<String, BoolExpr>>` 共享给 `QueryBuilder`,最终传递给 `NavigationLoader`。

3. 跨租户管理查询

管理后台需要查看所有租户数据时,使用 `query_ignore_filters()` 绕过查询过滤器:

// 返回所有租户的数据
let all_blogs = ctx.set::<Blog>()
    .query_ignore_filters()
    .to_list()
    .await?;

> **语义**:`query_ignore_filters()` 只绕过**当前实体**的查询过滤器。Navigation 加载的**关联实体**过滤器仍生效(对应 EFCore `IgnoreQueryFilters` 语义)。

4. 完整示例

use rust_ef::prelude::*;
use rust_ef::linq;

#[derive(Debug, Clone, EntityType)]
#[table("blogs")]
struct Blog {
    #[primary_key]
    #[auto_increment]
    id: i32,
    #[required]
    tenant_id: i32,
    #[required]
    title: String,
    #[navigation]
    posts: HasMany<Post>,
}

#[derive(Debug, Clone, EntityType)]
#[table("posts")]
struct Post {
    #[primary_key]
    #[auto_increment]
    id: i32,
    #[required]
    tenant_id: i32,
    #[required]
    title: String,
    #[foreign_key(Blog)]
    blog_id: i32,
}

async fn run() -> EFResult<()> {
    let mut ctx = DbContext::from_options(&options)?;

    // 注册租户过滤器(必须在 set::<T>() 之前)
    ctx.model()
        .has_query_filter::<Blog>(linq!(filter |b: Blog| b.tenant_id == 1))
        .has_query_filter::<Post>(linq!(filter |p: Post| p.tenant_id == 1));

    ctx.set::<Blog>();
    ctx.set::<Post>();
    ctx.ensure_created().await?;

    // INSERT:手动设置 tenant_id
    ctx.add::<Blog>(Blog {
        id: 0,
        tenant_id: 1,
        title: "Tenant 1 Blog".into(),
        posts: HasMany::new(),
    });
    ctx.save_changes().await?;

    // SELECT:自动过滤 tenant_id=1
    let blogs = ctx.set::<Blog>().query().to_list().await?;
    // Navigation:Posts 自动过滤 tenant_id=1
    let blogs_with_posts = linq!(ctx.set::<Blog>(); include b.posts).to_list().await?;

    // 管理查询:绕过过滤器
    let all = ctx.set::<Blog>().query_ignore_filters().to_list().await?;

    Ok(())
}

5. 反模式警示

| 反模式 | 后果 | 正确做法 | |--------|------|----------| | `Arc<Mutex<DbContext>>` 共享 | 跟踪污染,跨请求提交 | 用 owned 解析或 DI Scope 隔离 | | 过滤器在 `set::<T>()` 后注册 | DbSet 未注入过滤器 | 先注册过滤器再 `set::<T>()` | | 依赖 INSERT 自动设置 tenant_id | 框架无法知道运行时租户 | `add()` 前手动设置 | | 用 `query_ignore_filters()` 做权限控制 | 绕过安全边界 | 权限校验放 Service 层 |

相关文档

- [全局查询过滤器(基础用法)](../06-advanced-query/global-query-filters.md) - [DI 注册与 Scoped 生命周期](../10-di-interceptors/di-registration.md)