☰
Orchard Core 数据存储原理:YesSql 文档数据库、索引与会话机制完全指南
2026/10/7 2:21:53 网站建设 项目流程
  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载

Orchard Core 并不使用 Entity Framework 等传统 ORM,而是基于 YesSql——一个运行在关系数据库之上的 .NET 文档数据库接口——来持久化大部分应用数据(内容项、用户、设置、工作流等)。本文以官方文档 How YesSql works 为主线,结合仓库源码深入讲解文档表结构、索引(MapIndex / ReduceIndex)的定义与迁移、ISession 会话工作单元,以及数据库提供者与 YesSqlOptions 的配置方式,帮助你理解"内容类型加字段/部件无需数据库迁移"这一 Orchard Core 核心特性的底层原理,并掌握自定义索引与高效查询的完整实战路径。

为什么 Orchard Core 不选 ORM 而是文档数据库

传统 ORM 将对象模型映射到关系表,对象结构一变就需要改表结构、写迁移。Orchard Core 反其道而行:数据以JSON 文档形式整体落库,对象结构的变化只影响 JSON 的形态,表结构完全不用动。

这一设计带来两个直接收益:

  • 灵活:给内容类型添加字段(Field)或部件(Part)时,不需要任何数据库迁移,新字段只是 JSON 里多了一个键值;
  • 兼容:底层仍然是标准的 SQL Server、SQLite、MySQL 或 PostgreSQL,可以沿用既有的数据库运维与备份方案。

而代价也显而易见:无法用 SQL 高效查询 JSON 内部的内容。这正是索引(Index)存在的意义——把需要查询的属性"投影"到常规的关系表里。

Documents:JSON 文档如何落库

当一个对象被保存时,YesSql 会把它序列化为 JSON,并作为一行写入文档表(document table)。默认集合使用Document表;命名集合(named collection)则使用各自独立的文档表。每个租户还可以使用表前缀(table prefix),使多租户共用同一个数据库时表名互不冲突。

文档表的行结构如下:

IdTypeContentVersion
42OrchardCore.ContentManagement.ContentItem, OrchardCore.ContentManagement.Abstractions{ "ContentItemId": "4tavbc...", "DisplayText": "My blog post", ... }3

其中:

  • Id:文档自增主键;
  • Type:存储的 CLR 类型全名(程序集限定名),用于反序列化时还原对象类型;
  • Content:对象序列化后的 JSON 内容;
  • Version:文档版本号,配合并发控制与乐观锁使用。

ContentItem是 Orchard Core 中最重要的文档类型。它的 JSON 形态可以查阅 ContentItem.cs 中的定义;而文档与类型信息之间的序列化约定由 DefaultContentJsonSerializer 负责。

Indexes:让 JSON 可被高效查询

索引是一个普通的 C# 类,其中只包含你希望用来查询的属性。YesSql 为索引数据维护常规 SQL 表:

  • Map 索引:每一行索引记录对应一个文档,映射(Map)可以为单个文档发射一行或多行记录;
  • Reduce 索引:每一行索引记录聚合一组文档,类似于 SQL 的GROUP BY,用于计数、分组等场景。

Map 索引行包含一个DocumentId列,指向文档表;Reduce 索引则使用单独的表来关联文档与聚合行。当文档被创建、更新或删除时,其索引行会在同一个事务内重新计算。

一条重要的规则是:只有进入索引的属性才能被查询,其余数据只存在于 JSON 文档中。因此在设计索引时,应当把"会出现在查询条件或排序里的字段"全部纳入索引。

MapIndex 与 ReduceIndex 的区别

维度MapIndexReduceIndex
行与文档关系一行映射一个文档(可一对多发射)一行聚合多个文档
典型用途属性查询、过滤、排序计数、分组统计
示例ContentItemIndex映射每个内容项的ContentType、Published、Owner等如按状态统计文档数量

例如,ContentItemIndex把每个内容项映射成一行索引,记录其ContentType、Published、Latest、Owner、Author、DisplayText等可查询字段;AliasPartIndex则只映射那些带有AliasPart的内容项及其别名。

定义一个索引

以商品(Product)为例,先定义继承自MapIndex的索引类:

using YesSql.Indexes; public class ProductIndex : MapIndex { public string Sku { get; set; } public decimal Price { get; set; } }

再通过IndexProvider<T>描述"如何把某类文档映射为索引行":

public class ProductIndexProvider : IndexProvider<ContentItem> { public override void Describe(DescribeContext<ContentItem> context) { context.For<ProductIndex>() .When(contentItem => contentItem.Has<ProductPart>()) .Map(contentItem => { var part = contentItem.As<ProductPart>(); return new ProductIndex { Sku = part.Sku, Price = part.Price, }; }); } }

这里When(...)是可选过滤条件,只有满足条件(本例为"内容项包含ProductPart")的文档才会被投影;Map(...)返回null则表示不生成索引行(可用于清理软删除记录的索引)。

注册索引提供者

在模块的Startup.ConfigureServices()中注册:

services.AddIndexProvider<ProductIndexProvider>();

AddIndexProvider<T>定义在 IndexServiceCollectionExtensions.cs,实现是将提供者注册为单例的IIndexProvider:

services.TryAddEnumerable(ServiceDescriptor.Singleton<IIndexProvider, TIndexProvider>());

同文件还提供了AddScopedIndexProvider<T>,将实现IScopedIndexProvider的提供者注册为Scoped生命周期,适用于需要依赖ISession等 scoped 服务的场景(详见下文 AliasPartIndex 案例)。

创建索引表(数据迁移)

索引表通过数据迁移创建。在 Migrations 数据迁移 中调用SchemaBuilder.CreateMapIndexTableAsync<T>():

public async Task<int> CreateAsync() { await SchemaBuilder.CreateMapIndexTableAsync<ProductIndex>(table => table .Column<string>("Sku", column => column.WithLength(64)) .Column<decimal>("Price") ); return 1; }

CreateMapIndexTableAsync会自动为索引类生成一个包含Id与DocumentId列的常规 SQL 表,表名默认取自索引类名;WithLength(64)用于指定字符串列长度(并通常配合唯一约束使用)。

真实案例:ContentItemIndex 与 AliasPartIndex

仓库中有两个典型的 MapIndex 实现可供参考:

1.ContentItemIndex(Records/ContentItemIndex.cs)

它映射每个内容项的ContentItemId、ContentItemVersionId、Published、Latest、ContentType、Owner、Author、DisplayText及三个时间戳。其ContentItemIndexProvider的Map方法还会对超长字符串做截断保护——类中定义了MaxContentTypeSize、MaxOwnerSize等常量(均为 255),映射时超过长度的值会被裁切,避免超出列宽:

if (contentItemIndex.ContentType?.Length > ContentItemIndex.MaxContentTypeSize) { contentItemIndex.ContentType = contentItem.ContentType[..ContentItemIndex.MaxContentTypeSize]; }

2.AliasPartIndex(Indexes/AliasPartIndex.cs)

它映射每个带AliasPart的内容项的Alias、ContentItemId、Latest、Published,并做了两处有意思的处理:

  • 实现IScopedIndexProvider(而非直接继承IndexProvider<T>),并在UpdatedAsync内容处理钩子中做懒加载校验,确保被移除出类型定义的部件不再被索引;
  • Map中对"既未发布也未保留最新版本"的软删除项返回null,从而清除其索引记录:
    if (!contentItem.Published && !contentItem.Latest) { return null; }

对应的建表迁移在 Migrations.cs:CreateAsync中不仅建表,还通过AlterIndexTableAsync创建复合索引IDX_AliasPartIndex_DocumentId(覆盖DocumentId、Alias、ContentItemId、Published、Latest),并且UpdateFrom1Async等后续迁移用AddColumn<bool>演进表结构——这正说明索引表是按需演进的关系表,而文档表始终无需迁移。

The session:查询与写入的工作单元

YesSql 的文档读写都通过ISession进行。它是注册在依赖注入容器中的Scoped 工作单元:SaveAsync与Delete只会在内存中缓冲,真正的 SQL 命令要等调用SaveChangesAsync()时才在单个事务内批量执行。Orchard Core 会在租户 shell scope 结束时(通常是请求结束时)自动提交从该 scope 解析出的 session。

通过索引查询文档

控制器中注入ISession后,可以用强类型查询直接过滤索引表:

public sealed class MyController : Controller { private readonly ISession _session; public MyController(ISession session) { _session = session; } public async Task<IActionResult> Cheap() { // Query documents through an index. var cheapProducts = await _session .Query<ContentItem, ProductIndex>(index => index.Price < 10) .ListAsync(); return View(cheapProducts); } }

该查询先在索引表上用常规 SQL 过滤出符合条件的DocumentId,再回文档表加载对应 JSON 并反序列化为ContentItem。ISession还会缓存文档:同一请求内重复加载同一文档,返回的是同一个实例,保证状态一致并减少数据库往返。

何时用 ISession、何时用 IContentManager

对于内容项(ContentItem)这类由 Orchard Core 管理的文档,应优先使用更高层的IContentManager,或IOrchardHelper的扩展方法(如QueryContentItemsAsync)——它们封装了加载、版本管理(Published / Latest)与内容处理管道(handlers)。只有在查询你自己定义的索引时,才需要下沉到ISession直接操作。

Configuration:数据库提供者与 YesSql 选项

按租户选择数据库提供者

数据库提供者、连接字符串、表前缀和 Schema 都是在租户设置(Setup)阶段选定的。根据 Data 模块文档,Orchard Core 内置四种提供者:

数据库Provider 值连接字符串表前缀与 Schema
SQLiteSqlite不使用不使用
SQL ServerSqlConnection必填支持
MySQLMySql必填支持
PostgreSQLPostgres必填支持

SQLite 是默认提供者,每个租户的数据库文件存放在该租户的 shell 数据目录中(文件名由DatabaseName设置控制,默认OrchardCore.db)。SQL Server、MySQL、PostgreSQL 需要先手动建库,并给配置的账号授予建表/改表权限——Orchard Core 只负责校验连接并创建自己的表,不会替你创建数据库。

YesSqlOptions

YesSqlOptions定义在 YesSqlOptions.cs,Orchard Core 将配置节OrchardCore:YesSql绑定到它:

设置默认值说明
CommandsPageSize500YesSql 命令页的最大命令数,更大的集合会被拆分到多个页执行
QueryGatingEnabledtrue合并并发环境中相同的查询工作,使其只执行一次并共享结果
EnableThreadSafetyChecksfalse开启后帮助诊断 YesSql 会话被并发使用的场景
IsolationLevelReadCommitted传给数据库提供者的默认事务隔离级别

配置示例(可通过任意受支持的租户配置源下发,如根应用的appsettings.json):

{ "OrchardCore": { "YesSql": { "CommandsPageSize": 1000, "QueryGatingEnabled": true, "EnableThreadSafetyChecks": false, "IsolationLevel": "ReadCommitted" } } }

此外,YesSqlOptions还暴露了IdGenerator、IdentifierAccessorFactory、VersionAccessorFactory、ContentSerializer等属性,用于注入无法通过配置绑定创建的服务实现,需要以代码方式配置。

原始 SQL 访问与查询模块

如需对同一数据库执行原始 SQL,可以使用IDbConnectionAccessor(定义于 IDbConnectionAccessor.cs,其CreateConnection()返回租户对应的DbConnection),详见 Data 模块文档;也可以在管理界面中使用 SQL 查询模块 直接编写和执行 SQL。

更进一步

  • 阅读 Migrations 数据迁移,掌握SchemaBuilder建表/改表的完整 API(CreateMapIndexTableAsync、AlterIndexTableAsync、AddColumn等);
  • 阅读 Data 模块文档,了解表命名约定、SQLite 连接池开关(UseConnectionPooling)以及租户数据库的完整配置方式;
  • 阅读 SQL 查询模块,了解如何在不写 C# 代码的情况下直接查询文档与索引;
  • 在仓库源码中继续追踪:ContentItemIndexProvider(Records/ContentItemIndex.cs)展示了标准映射写法,AliasPartIndexProvider(Indexes/AliasPartIndex.cs)展示了带清理逻辑与懒加载的 Scoped 提供者写法,Migrations.cs 则完整演示了索引表的创建与后续演进。

理解"文档表存 JSON、索引表存查询列、会话按事务提交"这三层模型,是掌握 Orchard Core 数据层的钥匙:它能解释为什么内容建模如此轻量,也能指导你为自己的业务数据设计高效、可查询的索引。

  • CMS
  • 后端
  • Web框架

【免费下载链接】OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.

项目地址:https://gitcode.com/gh_mirrors/or/OrchardCore
点击查看免费下载

相关推荐

上一篇:Windows系统优化新选择:Windows Cleaner让你的电脑重获新生
下一篇:终极指南:如何在Windows上免费安装ViGEmBus虚拟手柄驱动解决游戏兼容性问题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询