名称: 内容关系 描述: 用于设计内容项之间的引用关系、内容选取器字段、多对多关系或双向链接。涵盖关系类型、参考完整性、急切/延迟加载,以及无头CMS的关系API。 允许工具: 读取, 全局搜索, 查找, 任务, 技能
内容关系
在无头CMS架构中设计和实现内容项之间关系的指南。
何时使用此技能
- 向内容类型添加内容选取器字段
- 设计作者-文章关系
- 实现相关内容功能
- 构建内容层次结构(父/子页面)
- 管理双向关系
- 处理删除时的参考完整性
关系类型
一对多(父引用)
// 文章有一个作者
public class Article
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
// 作者的外键
public Guid AuthorId { get; set; }
public Author? Author { get; set; }
}
public class Author
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
// 导航属性(反向)
public List<Article> Articles { get; set; } = new();
}
多对多(连接表)
// 文章有多个类别,类别有多个文章
public class Article
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public List<ArticleCategory> ArticleCategories { get; set; } = new();
}
public class Category
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public List<ArticleCategory> ArticleCategories { get; set; } = new();
}
public class ArticleCategory
{
public Guid ArticleId { get; set; }
public Article Article { get; set; } = null!;
public Guid CategoryId { get; set; }
public Category Category { get; set; } = null!;
// 可选:关系元数据
public int Order { get; set; }
public bool IsPrimary { get; set; }
}
// EF Core 配置
modelBuilder.Entity<ArticleCategory>()
.HasKey(ac => new { ac.ArticleId, ac.CategoryId });
自引用(层次结构)
// 页面层次结构
public class Page
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public Guid? ParentId { get; set; }
public Page? Parent { get; set; }
public List<Page> Children { get; set; } = new();
// 为高效查询计算路径
public string Path { get; set; } = string.Empty;
public int Depth { get; set; }
}
多项式引用(任意内容类型)
// 引用任何内容项
public class ContentReference
{
public Guid ReferencingItemId { get; set; }
public string ReferencingItemType { get; set; } = string.Empty;
public Guid ReferencedItemId { get; set; }
public string ReferencedItemType { get; set; } = string.Empty;
public string RelationshipType { get; set; } = string.Empty; // “相关”、“特色”、“另请参阅”
public int Order { get; set; }
}
// 用法:文章引用产品、页面或其他文章
内容选取器字段模式
通用内容选取器
public class ContentPickerField
{
// 此选取器允许的内容类型
public List<string> AllowedContentTypes { get; set; } = new();
// 选定的内容项ID
public List<Guid> ContentItemIds { get; set; } = new();
// 最小/最大选择项
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
// 显示选项
public bool ShowContentType { get; set; } = true;
public string DisplayTemplate { get; set; } = "{Title}";
}
// 存储在JSON列中
public class ArticleExtensions
{
public ContentPickerField? RelatedArticles { get; set; }
public ContentPickerField? FeaturedProducts { get; set; }
}
用于API的解析引用
public class ContentPickerFieldDto
{
public List<Guid> ContentItemIds { get; set; } = new();
// 可选包含解析项
public List<ContentItemSummary>? Items { get; set; }
}
public class ContentItemSummary
{
public Guid Id { get; set; }
public string ContentType { get; set; } = string.Empty;
public string DisplayText { get; set; } = string.Empty;
public string? Url { get; set; }
public string? ThumbnailUrl { get; set; }
}
关系加载策略
急切加载
// 用初始查询加载关系
public async Task<Article?> GetArticleWithRelationsAsync(Guid id)
{
return await _context.Articles
.Include(a => a.Author)
.Include(a => a.ArticleCategories)
.ThenInclude(ac => ac.Category)
.FirstOrDefaultAsync(a => a.Id == id);
}
显式加载
// 按需加载关系
public async Task LoadAuthorAsync(Article article)
{
await _context.Entry(article)
.Reference(a => a.Author)
.LoadAsync();
}
public async Task LoadCategoriesAsync(Article article)
{
await _context.Entry(article)
.Collection(a => a.ArticleCategories)
.Query()
.Include(ac => ac.Category)
.LoadAsync();
}
用于API的投影
// 仅加载响应所需的内容
public async Task<ArticleDto?> GetArticleDtoAsync(Guid id)
{
return await _context.Articles
.Where(a => a.Id == id)
.Select(a => new ArticleDto
{
Id = a.Id,
Title = a.Title,
AuthorName = a.Author!.Name,
Categories = a.ArticleCategories
.Select(ac => ac.Category.Name)
.ToList()
})
.FirstOrDefaultAsync();
}
双向关系
维护双向关系
public class RelatedContent
{
public Guid SourceId { get; set; }
public Guid TargetId { get; set; }
public string RelationType { get; set; } = string.Empty;
public bool IsBidirectional { get; set; }
}
public class ContentRelationshipService
{
public async Task AddRelationshipAsync(
Guid sourceId,
Guid targetId,
string relationType,
bool bidirectional = true)
{
// 添加正向关系
await _repository.AddAsync(new RelatedContent
{
SourceId = sourceId,
TargetId = targetId,
RelationType = relationType,
IsBidirectional = bidirectional
});
// 如果双向,添加反向关系
if (bidirectional)
{
await _repository.AddAsync(new RelatedContent
{
SourceId = targetId,
TargetId = sourceId,
RelationType = GetReverseType(relationType),
IsBidirectional = true
});
}
}
private string GetReverseType(string type) => type switch
{
"parent-of" => "child-of",
"child-of" => "parent-of",
"references" => "referenced-by",
_ => type // 对称关系如“related-to”
};
}
参考完整性
删除行为
public enum ReferenceDeleteBehavior
{
Restrict, // 如果被引用,阻止删除
Cascade, // 删除引用项
SetNull, // 清除引用
NoAction // 留下孤儿(在应用中处理)
}
// EF Core 配置
modelBuilder.Entity<Article>()
.HasOne(a => a.Author)
.WithMany(a => a.Articles)
.HasForeignKey(a => a.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
孤儿检测
public class OrphanDetectionService
{
public async Task<List<ContentReference>> FindOrphanReferencesAsync()
{
// 查找目标已不存在的引用
return await _context.ContentReferences
.Where(r => !_context.ContentItems
.Any(c => c.Id == r.ReferencedItemId))
.ToListAsync();
}
public async Task<List<ContentItem>> FindUnreferencedContentAsync(
string contentType)
{
// 查找未被任何内容引用的内容
var referencedIds = await _context.ContentReferences
.Where(r => r.ReferencedItemType == contentType)
.Select(r => r.ReferencedItemId)
.Distinct()
.ToListAsync();
return await _context.ContentItems
.Where(c => c.ContentType == contentType)
.Where(c => !referencedIds.Contains(c.Id))
.ToListAsync();
}
}
关系API设计
REST模式
# 在单个请求中包含相关项
GET /api/articles/{id}?include=author,categories
# 嵌套资源
GET /api/articles/{id}/author
GET /api/articles/{id}/categories
GET /api/authors/{id}/articles
# 关系管理
POST /api/articles/{id}/relationships/categories
DELETE /api/articles/{id}/relationships/categories/{categoryId}
PUT /api/articles/{id}/relationships/author
包含项的响应
{
"data": {
"id": "article-123",
"type": "Article",
"attributes": {
"title": "My Article"
},
"relationships": {
"author": {
"data": { "id": "author-456", "type": "Author" }
},
"categories": {
"data": [
{ "id": "cat-1", "type": "Category" },
{ "id": "cat-2", "type": "Category" }
]
}
}
},
"included": [
{
"id": "author-456",
"type": "Author",
"attributes": { "name": "Jane Doe" }
},
{
"id": "cat-1",
"type": "Category",
"attributes": { "name": "Technology" }
}
]
}
GraphQL关系
type Article {
id: ID!
title: String!
author: Author!
categories: [Category!]!
relatedArticles(first: Int): [Article!]!
}
type Query {
article(id: ID!): Article
// 反向查找
articlesByAuthor(authorId: ID!): [Article!]!
articlesByCategory(categoryId: ID!): [Article!]!
}
相关技能
内容类型建模- 定义关系字段动态模式设计- 在JSON中存储引用无头API设计- 关系API端点