如何加载多对多LINQ查询?
我有以下(相当标准的)表结构:
Post <-> PostTag <-> Tag
假设我有以下记录:
PostID Title 1, 'Foo' 2, 'Bar' 3, 'Baz' TagID Name 1, 'Foo' 2, 'Bar' PostID TagID 1 1 1 2 2 2
换句话说,第一个帖子有两个标签,第二个帖子有一个标签,第三个帖子没有任何标签。
我想在一个查询中加载所有帖子及其标签,但无法找到合适的运算符组合。我已经可以只加载带有标签的帖子,也可以加载多个标签时的重复帖子。
给定上面的数据库,我想在Post对象的collection属性中接收三个帖子及其标签(如果有)。有可能吗?
谢谢
解决方案
回答
我已经在另一篇文章中回答了这个问题:关于热切的加载。在情况下,可能类似于:
DataLoadOptions options = new DataLoadOptions(); options.LoadWith<Post>(p => p.PostTag); options.LoadWith<PostTag>(pt => pt.Tag);
尽管要小心,否则必须在将任何查询发送到数据库之前设置DataLoadOptions,否则会引发异常(不知道为什么在Linq2Sql中这样子可能会在以后的版本中修复)。
回答
很抱歉,不行,Eager Loading将为每个帖子的每个标签执行一个额外的查询。
经过以下代码测试:
var options = new DataLoadOptions(); options.LoadWith<Post>(p => p.PostTags); options.LoadWith<PostTag>(pt => pt.Tag); using (var db = new BlogDataContext()) { db.LoadOptions = options; return (from p in db.Posts where p.Status != PostStatus.Closed orderby p.PublishDateGmt descending select p); }
在示例数据库中,它将执行4个在生产中不可接受的查询。谁能建议其他解决方案?
谢谢
回答
有点奇怪,因为
DataLoadOptions o = new DataLoadOptions ( ); o.LoadWith<Listing> ( l => l.ListingStaffs ); o.LoadWith<ListingStaff> ( ls => ls.MerchantStaff ); ctx.LoadOptions = o; IQueryable<Listing> listings = (from a in ctx.Listings where a.IsActive == false select a); List<Listing> list = listings.ToList ( );
结果如下:
SELECT [t0].*, [t1].*, [t2].*, ( SELECT COUNT(*) FROM [dbo].[LStaff] AS [t3] INNER JOIN [dbo].[MStaff] AS [t4] ON [t4].[MStaffId] = [t3].[MStaffId] WHERE [t3].[ListingId] = [t0].[ListingId] ) AS [value] FROM [dbo].[Listing] AS [t0] LEFT OUTER JOIN ([dbo].[LStaff] AS [t1] INNER JOIN [dbo].[MStaff] AS [t2] ON [t2].[MStaffId] = [t1].[MStaffId]) ON [t1].[LId] = [t0].[LId] WHERE NOT ([t0].[IsActive] = 1) ORDER BY [t0].[LId], [t1].[LStaffId], [t2].[MStaffId]
(我已经缩短了名称,并在选择项上添加了*)。
如此看来,选择就可以了。
回答
抱歉。我们提供的解决方案有效,但是我发现在使用Take(N)分页时,它会中断。我正在使用的完整方法如下:
public IList<Post> GetPosts(int page, int records) { var options = new DataLoadOptions(); options.LoadWith<Post>(p => p.PostTags); options.LoadWith<PostTag>(pt => pt.Tag); using (var db = new BlogDataContext()) { db.LoadOptions = options; return (from p in db.Posts where p.Status != PostStatus.Closed orderby p.PublishDateGmt descending select p) .Skip(page * records) //.Take(records) .ToList(); } }
注释了Take()方法后,它会生成与我们发布的查询类似的查询,但是如果我再次添加Take(),它将生成1 + N x M个查询。
因此,我想我现在的问题是:Take()方法是否可以替代记录分页?
谢谢
回答
耶!有效。
如果有人遇到相同的问题,这就是我所做的:
public IList<Post> GetPosts(int page, int record) { var options = new DataLoadOptions(); options.LoadWith<Post>(p => p.PostTags); options.LoadWith<PostTag>(pt => pt.Tag); using (var db = new DatabaseDataContext(m_connectionString)) { var publishDateGmt = (from p in db.Posts where p.Status != PostStatus.Hidden orderby p.PublishDateGmt descending select p.PublishDateGmt) .Skip(page * record) .Take(record) .ToList() .Last(); db.LoadOptions = options; return (from p in db.Posts where p.Status != PostStatus.Closed && p.PublishDateGmt >= publishDateGmt orderby p.PublishDateGmt descending select p) .Skip(page * record) .ToList(); } }
这仅执行两个查询并为每个帖子加载所有标签。
想法是获取一些值以将查询限制在我们需要的最后一个帖子(在这种情况下,PublishDateGmt列就足够了),然后用该值而不是Take()限制第二个查询。
感谢帮助sirrocco。
回答
我知道这是一篇旧文章,但是我发现了一种仅执行一个查询的同时使用Take()的方法。诀窍是在嵌套查询中执行Take()。
var q = from p in db.Posts where db.Posts.Take(10).Contains(p) select p;
在上面的查询中使用DataLoadOptions将在一个查询中为我们提供前十个帖子,包括它们的关联标签。生成的SQL将是以下内容的简洁得多:
SELECT p.PostID, p.Title, pt.PostID, pt.TagID, t.TagID, t.Name FROM Posts p JOIN PostsTags pt ON p.PostID = pt.PostID JOIN Tags t ON pt.TagID = t.TagID WHERE p.PostID IN (SELECT TOP 10 PostID FROM Posts)