OUTER JOIN未在EF Core中返回预期结果

时间:2016-09-14 01:40:41

标签: c# tsql linq-to-entities entity-framework-core outer-join

在我的ASP.NET MVC Core应用中,POST操作方法Test未返回预期结果。该网络应用程序是使用this official ASP.NET Core site创建的,并略有修改。真正的应用可以是downloaded from here,并使用最新版本的VS2015。该应用正在使用EF Core。 如果下载项目,则需要执行以下步骤来测试上述意外结果:

注意:这些步骤的顺序非常重要。这是一个非常小的测试项目。 Step2将创建一个名为ASPCore_Blogs的小型SQL Server Db。因此,请确保SQL Server正在运行:

  1. 下载项目后,请确保在VS2015中打开项目之前从项目目录中删除.vs文件夹(如果项目挂起,您可能必须使用Task Manager窗口强行关闭它然后重新打开它以使其工作。这是VS2015中的一个已知问题。
  2. 打开startup.cs文件,在Configuration()方法中,将数据库实例名称从MyComputer\SQLServerInstance更改为您正在使用的任何实例。在根目录的appsettings.json文件中执行相同操作。
  3. 在VS2015 PM窗口中,运行PM> update-database -context BloggingContext [确保SQL Server正在运行]
  4. 然后运行:PM> update-database -context ApplicationDbContext
  5. 运行网络应用。通过输入登录/密码信息进行注册。登录需要使用电子邮件(test@test.com)表格。在主页的左侧:

  6. 点击链接Blog Create创建4个博客:Blog1 @ test.com,Blog2 @ test.com,Blog3 @ test.com,Blog4 @ test.com

  7. 点击链接Blogs Index以验证上述所有4个博客均已创建
  8. 点击Test链接。此视图由GET操作方法Test调用。在相应的视图(Test.cshtml)上,您会在页面上看到Url列显示以上所有4个博客。 TitleContent列是空白。将Title列填写为:Title1,Title2,Title3,Title4。将Content列填写为:Content1,Content2,Content3,Content4
  9. 现在,转到名为ASPCore_BlogsNAxis的相应SQL Server数据库,并在Posts模式下打开Edit表,手动将PostYear列值分别更改为:1998,1999,1998,2001 (注:1998年有意重复)
  10. 现在,转到同一SQL Server数据库中的Blogs表并输入一个额外的博客Blog5@test.com
  11. 现在,运行网络应用并再次点击Test链接(位于主页左侧)。您会看到Get操作方法Test正在使用左外连接来显示所有5个博客,但右侧列(TitleContent)值是空白的第5行,正如预期的那样,因为左外连接不满足第5个博客BlogId上的连接条件。到目前为止一切顺利。
  12. 现在,在Year视图的Test.cshtml下拉列表中,选择年份为1998,然后点击GO按钮。根据{{​​1}}操作方法if的第一个POST条件,应用程序应仅显示三条记录(1998年为两条,第五条不满足连接条件):第一条,第三条,和第5记录。
  13. 但这不是发生的事情。通过从下拉列表中选择不同年份重复此操作并单击Test按钮,您将看到输出不符合预期。

    示例数据

    博客表格数据

    GO

    帖子表数据

    BlogId  Url
    1       test1.com
    2       test2.com
    3       test3.com
    4       test4.com
    5       test5.com
    

    PostId BlogId Content PostYear Title 1 1 Content1 1998 Title1 2 2 Content2 1999 Title2 3 3 Content3 1998 Title3 4 4 Content4 2001 Title4 行动GET方法中的LEFT外部JOIN应返回

    Test

    当你在下拉列表中选择1998年并单击Go按钮时,Test(...)Post action方法查询应该返回但它会随机返回任何行

    BlogId  Url PostId  Content PostYear    Title
    1   test1.com   1   Content1    1998    Title1
    2   test2.com   2   Content2    1999    Title2
    3   test3.com   3   Content3    1998    Title3
    4   test4.com   4   Content4    2001    Title4
    5   test5.com   NULL    NULL    NULL    NULL
    

    模型

    BlogId  Url        PostId  Content    PostYear  Title
      1     test1.com     1     Content1    1998    Title1
      3     test3com      3     Content2    1998    Title3
      5     test5.com     NULL  NULL        NULL    NULL
    

    BlogsController

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        { }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }
    
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public int PostYear { get; set; }
        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
    

    更新

    1. 添加了第2步,要求用户更改连接字符串
    2. public class BlogsController : Controller { private readonly BloggingContext _context; public BlogsController(BloggingContext context) { _context = context; } // GET: Blogs public async Task<IActionResult> Index() { return View(_context.Blogs.ToList()); } // GET: /Blogs/Test [HttpGet] public async Task<IActionResult> Test(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; ViewBag.YearsList = Enumerable.Range(1996, 29).Select(g => new SelectListItem { Value = g.ToString(), Text = g.ToString() }).ToList(); //return View(await _context.Blogs.Include(p => p.Posts).ToListAsync()); var qrVM = from b in _context.Blogs join p in _context.Posts on b.BlogId equals p.BlogId into bp from c in bp.DefaultIfEmpty() select new BlogsWithRelatedPostsViewModel { BlogID = b.BlogId, PostID = (c == null ? 0 : c.PostId), Url = b.Url, Title = (c == null ? string.Empty : c.Title), Content = (c == null ? string.Empty : c.Content) }; return View(await qrVM.ToListAsync()); } // POST: /Blogs/Test [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Test(List<BlogsWithRelatedPostsViewModel> list, string GO, int currentlySelectedIndex, string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; ViewBag.YearsList = Enumerable.Range(1996, 29).Select(g => new SelectListItem { Value = g.ToString(), Text = g.ToString() }).ToList(); if (!string.IsNullOrEmpty(GO)) { var qrVM = from b in _context.Blogs join p in _context.Posts on b.BlogId equals p.BlogId into bp from c in bp.DefaultIfEmpty() where c == null? true : c.PostYear.Equals(currentlySelectedIndex) select new BlogsWithRelatedPostsViewModel { BlogID = b.BlogId, PostID = (c == null ? 0 : c.PostId), Url = b.Url, Title = (c == null ? string.Empty : c.Title), Content = (c == null ? string.Empty : c.Content) }; return View(await qrVM.ToListAsync()); } else if (ModelState.IsValid) { foreach (var item in list) { var oPost = _context.Posts.Where(r => r.PostId.Equals(item.PostID)).FirstOrDefault(); if (oPost != null) { oPost.Title = item.Title; oPost.Content = item.Content; oPost.PostYear = currentlySelectedIndex; oPost.BlogId = item.BlogID; //according to new post below the blogId should exist for a newly created port - but just in case } else { if (item.PostID == 0) { Post oPostNew = new Post { BlogId = item.BlogID, Title = item.Title, Content = item.Content, PostYear = currentlySelectedIndex }; //need to use currentlySelectedIndex intead of item.FiscalYear in case of adding a record _context.Add(oPostNew); } } } await _context.SaveChangesAsync(); //return RedirectToLocal(returnUrl); return View(list); } // If we got this far, something failed, redisplay form return View(); } // GET: Blogs/Details/5 public async Task<IActionResult> Details(int? id) { if (id == null) { return NotFound(); } var blog = await _context.Blogs.SingleOrDefaultAsync(m => m.BlogId == id); if (blog == null) { return NotFound(); } return View(blog); } // GET: Blogs/Create [HttpGet] public IActionResult Create() { return View(); } // POST: Blogs/Create // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("BlogId,Url")] Blog blog) { if (ModelState.IsValid) { _context.Blogs.Add(blog); await _context.SaveChangesAsync(); return RedirectToAction("Index"); } return View(blog); } // GET: Blogs/Edit/5 public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var blog = await _context.Blogs.SingleOrDefaultAsync(m => m.BlogId == id); if (blog == null) { return NotFound(); } return View(blog); } // POST: Blogs/Edit/5 // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int id, [Bind("BlogId,Url")] Blog blog) { if (id != blog.BlogId) { return NotFound(); } if (ModelState.IsValid) { try { _context.Update(blog); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!BlogExists(blog.BlogId)) { return NotFound(); } else { throw; } } return RedirectToAction("Index"); } return View(blog); } // GET: Blogs/Delete/5 public async Task<IActionResult> Delete(int? id) { if (id == null) { return NotFound(); } var blog = await _context.Blogs.SingleOrDefaultAsync(m => m.BlogId == id); if (blog == null) { return NotFound(); } return View(blog); } // POST: Blogs/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(int id) { var blog = await _context.Blogs.SingleOrDefaultAsync(m => m.BlogId == id); _context.Blogs.Remove(blog); await _context.SaveChangesAsync(); return RedirectToAction("Index"); } private bool BlogExists(int id) { return _context.Blogs.Any(e => e.BlogId == id); } } 的GET / POST操作方法中从bp.DefaultIfEmpty(new Post())中删除了新的Post()。但是同样的错误仍然存​​在。

1 个答案:

答案 0 :(得分:0)

  • 在linq查询中,您执行DefaultIfEmtpy调用:

    from c in bp.DefaultIfEmpty(new Post())
    where c == null? true : c.PostYear.Equals(currentlySelectedIndex)
    

    你使用了重载DefaultIfEmtpy将在它为空时返回new Post()实例,而不是返回null。但是你的逻辑希望它返回null。使用返回null的重载替换snipper的第一行:

    from c in bp.DefaultIfEmpty()