在我的项目中,我有类型Comment
和CommentDto
:
public class Comment
{
public Guid CommentId { get; set; }
public string Content { get; set; }
public Guid PostId { get; set; }
public virtual Post Post { get; set; }
public Guid? ParentCommentId { get; set; }
public virtual Comment ParentComment { get; set; }
public virtual ICollection<Comment> InverseParentComment { get; set; }
}
class CommentDto
{
public Guid CommentId { get; set; }
public string Content { get; set; }
public Guid? ParentCommentId { get; set; }
public ICollection<CommentDto> InverseParentComment { get; set; }
}
Comment
将映射到CommentDto
. 这是配置:
cfg.CreateMap<Comment, CommentDto>();
我有以下递归 CTE,封装在表值函数中:
FUNCTION [dbo].[fn_PostCommentHierarchy] (@postId UNIQUEIDENTIFIER)
RETURNS TABLE
AS
RETURN
(
WITH cte AS
(
SELECT CommentId, Content, PostId, ParentCommentId
FROM dbo.Comment
WHERE ParentCommentId IS NULL and PostId = @postId
UNION ALL
SELECT child.CommentId, child.Content, child.PostId, child.ParentCommentId
FROM dbo.Comment child
INNER JOIN cte parent
ON parent.CommentId = child.ParentCommentId
WHERE parent.PostId = @postId
)
SELECT * FROM cte
);
此函数允许获取给定帖子的评论层次结构(它需要帖子的 ID)。
为了使其更具可读性,我以分层顺序(仅Content
属性)表示它:
- 你好世界!
- 你是程序员吗?
- 当然
- 什么?
- 你是程序员吗?
- 我也想去火星!
- 月球上见:)
fn_PostCommentHierarchy
使用 EF Core调用函数
List<Comment> commentHierarchy = await _context.Comment
.FromSqlInterpolated($"SELECT CommentId, Content, PostId, ParentCommentId FROM dbo.fn_PostCommentHierarchy('post-id-here')")
.ToListAsync();
EF Core 将以下 SQL 查询发送到 SQL-Server:
SELECT CommentId, Content, PostId, ParentCommentId FROM dbo.fn_PostCommentHierarchy('post-id-here')
上面的代码按预期工作(使用 JSON 格式来增加可读性):
[
{
"commentId": "be02742a-9170-4335-afe7-3c7c22684424",
"content": "Hello World!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": [
{
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"commentRates": [],
"inverseParentComment": [
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
},
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
}
]
}
]
},
{
"commentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"content": "I wanna go to Mars too!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": [
{
"commentId": "ab6d6b49-d772-48cd-9477-8d40f133c37a",
"content": "See you on the Moon :)",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"commentRates": [],
"inverseParentComment": []
}
]
},
{
"commentId": "ab6d6b49-d772-48cd-9477-8d40f133c37a",
"content": "See you on the Moon :)",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"parentComment": {
"commentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"content": "I wanna go to Mars too!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": []
},
"commentRates": [],
"inverseParentComment": []
},
{
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"parentComment": {
"commentId": "be02742a-9170-4335-afe7-3c7c22684424",
"content": "Hello World!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": []
},
"commentRates": [],
"inverseParentComment": [
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
},
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
}
]
},
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"parentComment": {
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"parentComment": {
"commentId": "be02742a-9170-4335-afe7-3c7c22684424",
"content": "Hello World!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": []
},
"commentRates": [],
"inverseParentComment": [
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
}
]
},
"commentRates": [],
"inverseParentComment": []
},
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"parentComment": {
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"parentComment": {
"commentId": "be02742a-9170-4335-afe7-3c7c22684424",
"content": "Hello World!",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": null,
"parentComment": null,
"commentRates": [],
"inverseParentComment": []
},
"commentRates": [],
"inverseParentComment": [
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"postId": "69f3ca3a-66fc-4142-873d-01e950d83adf",
"post": null,
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"commentRates": [],
"inverseParentComment": []
}
]
},
"commentRates": [],
"inverseParentComment": []
}]
注意:我做了大胆的评论,没有父母(根评论)。
映射Comment
成CommentDto
上面的代码适用于 entity-type Comment
,但我想将其映射到CommentDto
. 因此,让我们ProjectTo
用于此目的:
List<CommentDto> commentHierarchy = await _context.Comment
.FromSqlInterpolated($"SELECT CommentId, Content, PostId, ParentCommentId FROM dbo.fn_PostCommentHierarchy('post-id-here')")
.ProjectTo<CommentDto>(_mapper.ConfigurationProvider)
.ToListAsync();
注意:_mapper
是一个对象类型IMapper
。
我认为,结果应该与我使用之前得到的结果相似ProjectTo
。但它看起来像:
[
{
"commentId": "be02742a-9170-4335-afe7-3c7c22684424",
"content": "Hello World!",
"parentCommentId": null,
"inverseParentComment": [
{
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"inverseParentComment": null
}
]
},
{
"commentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"content": "I wanna go to Mars too!",
"parentCommentId": null,
"inverseParentComment": [
{
"commentId": "ab6d6b49-d772-48cd-9477-8d40f133c37a",
"content": "See you on the Moon :)",
"parentCommentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"inverseParentComment": null
}
]
},
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"inverseParentComment": []
},
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"inverseParentComment": []
},
{
"commentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"content": "Are you a programmer?",
"parentCommentId": "be02742a-9170-4335-afe7-3c7c22684424",
"inverseParentComment": [
{
"commentId": "0bb77a43-c7bb-482f-9bf8-55c4050974da",
"content": "Sure",
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"inverseParentComment": null
},
{
"commentId": "b8d61cfd-d274-4dae-a2be-72e08cfa9066",
"content": "What?",
"parentCommentId": "59656765-d1ed-4648-8696-7d576ab7419f",
"inverseParentComment": null
}
]
},
{
"commentId": "ab6d6b49-d772-48cd-9477-8d40f133c37a",
"content": "See you on the Moon :)",
"parentCommentId": "cfe126b3-4601-4432-8c87-445c1362a225",
"inverseParentComment": []
}
]
注意:我做了大胆的评论,没有父母(根评论)。
比较使用前后的结果ProjectTo
。为什么它们不同?
对于上面的代码,EF Core 将以下 SQL 查询发送到 SQL 服务器:
SELECT [c].[CommentId], [c].[Content], [c].[ParentCommentId], [c0].[CommentId], [c0].[Content], [c0].[ParentCommentId]
FROM (
SELECT CommentId, Content, PostId, ParentCommentId FROM dbo.fn_PostCommentHierarchy('69f3ca3a-66fc-4142-873d-01e950d83adf')
) AS [c]
LEFT JOIN [Comment] AS [c0] ON [c].[CommentId] = [c0].[ParentCommentId]
ORDER BY [c].[CommentId], [c0].[CommentId]
问题
为什么使用前的结果和使用ProjectTo
后的结果ProjectTo
不一样?如何解决这个问题?
更新 1
根据 Svyatoslav Danyliv 的说法:
递归 CTE 返回平面列表,然后您必须再次构建层次结构。
但是为什么在这种情况下我应该使用递归 CTE?
以下解决方案以相同的方式工作:
List<CommentDto> commentFlatList = await _context.Comment
.Where(c => c.PostId == Guid.Parse("post-id-here"))
.ProjectTo<CommentDto>(_mapper.ConfigurationProvider)
.ToListAsync();
Dictionary<Guid, CommentDto> commentDictionary = commentFlatList
.ToDictionary(c => c.CommentId);
foreach (var comment in commentFlatList)
{
if (comment.ParentCommentId == null)
{
continue;
}
if (commentDictionary.TryGetValue((Guid) comment.ParentCommentId, out CommentDto parent))
{
parent.Children.Add(comment);
}
}
List<CommentDto> commentHierarchy = commentFlatList.Where(c => c.ParentCommentId == null);
注意:我使用Dictionary
了代替Lookup
(参见这个例子),但它并没有改变这个想法。
更新 2
让我们看一下更新 1 中的代码:
List<CommentDto> commentFlatList = await _context.Comment
.Where(c => c.PostId == Guid.Parse("post-id-here"))
.ProjectTo<CommentDto>(_mapper.ConfigurationProvider)
.ToListAsync();
它将由 EF Core 翻译成以下内容:
exec sp_executesql N'SELECT [c].[CommentId], [c].[Content], [c].[ParentCommentId]
FROM [Comment] AS [c]
WHERE [c].[PostId] = @__request_PostId_0',N'@__request_PostId_0 uniqueidentifier',@__request_PostId_0='post-id-here'