- 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.
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),使多租户共用同一个数据库时表名互不冲突。
文档表的行结构如下:
| Id | Type | Content | Version |
|---|---|---|---|
| 42 | OrchardCore.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 的区别
| 维度 | MapIndex | ReduceIndex |
|---|---|---|
| 行与文档关系 | 一行映射一个文档(可一对多发射) | 一行聚合多个文档 |
| 典型用途 | 属性查询、过滤、排序 | 计数、分组统计 |
| 示例 | 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 |
|---|---|---|---|
| SQLite | Sqlite | 不使用 | 不使用 |
| SQL Server | SqlConnection | 必填 | 支持 |
| MySQL | MySql | 必填 | 支持 |
| PostgreSQL | Postgres | 必填 | 支持 |
SQLite 是默认提供者,每个租户的数据库文件存放在该租户的 shell 数据目录中(文件名由DatabaseName设置控制,默认OrchardCore.db)。SQL Server、MySQL、PostgreSQL 需要先手动建库,并给配置的账号授予建表/改表权限——Orchard Core 只负责校验连接并创建自己的表,不会替你创建数据库。
YesSqlOptions
YesSqlOptions定义在 YesSqlOptions.cs,Orchard Core 将配置节OrchardCore:YesSql绑定到它:
| 设置 | 默认值 | 说明 |
|---|---|---|
CommandsPageSize | 500 | YesSql 命令页的最大命令数,更大的集合会被拆分到多个页执行 |
QueryGatingEnabled | true | 合并并发环境中相同的查询工作,使其只执行一次并共享结果 |
EnableThreadSafetyChecks | false | 开启后帮助诊断 YesSql 会话被并发使用的场景 |
IsolationLevel | ReadCommitted | 传给数据库提供者的默认事务隔离级别 |
配置示例(可通过任意受支持的租户配置源下发,如根应用的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.
相关推荐
Orchard Core 数据访问实战:YesSql 文档存储、索引与会话,以及 GraphQL 数据暴露
Orchard Core 数据访问实战:YesSql 文档存储、索引与会话,以及 GraphQL 数据暴露 Orchard Core 的持久化数据层并不使用 E
CMS后端Web框架Orchard Core Data 模块深度指南:YesSql 数据库配置、表命名预设与原生 SQL 查询
Orchard Core Data 模块深度指南:YesSql 数据库配置、表命名预设与原生 SQL 查询 Orchard Core 的 Data 模块( Or
CMS后端Web框架Orchard Core 内容定义存储(Content Definition Store)完全指南:从文件存储到数据库的迁移实战
Orchard Core 内容定义存储(Content Definition Store)完全指南:从文件存储到数据库的迁移实战 Content Definit
CMS后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考