【问题标题】:OUTER JOIN not returning expected results in EF Core外部联接未在 EF Core 中返回预期结果
【发布时间】:2017-01-21 17:16:42
【问题描述】:

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

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

  1. 下载项目后,在VS2015中打开项目之前,请确保删除项目目录中的.vs文件夹(如果项目挂起,您可能需要使用windows操作系统的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. 运行 Web 应用程序。通过输入登录名/密码信息进行注册。登录需要使用电子邮件 (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 DB并以Edit模式打开Posts表,手动将PostYear列值分别更改为:1998,1999,1998,2001(注意:1998重复故意的)
  10. 现在,转到同一 SQL Server DB 中的 Blogs 表并输入额外的博客 Blog5@test.com
  11. 现在,运行网络应用程序并再次单击Test 链接(在主页左侧)。您会看到 Get 操作方法 Test 使用左外连接来显示所有 5 个博客,但右侧列(TitleContent)值在第 5 行是空白,正如预期的那样,因为第 5 篇博客的左外连接不满足 BlogId 的连接条件。到目前为止一切顺利。
  12. 现在,在Test.cshtml 视图的Year 下拉列表中,选择年份为1998 并单击GO 按钮。根据POST 操作方法Test 的第一个if 条件,应用程序应该只显示三条记录(两条为1998 年,第五条不满足加入条件):第一条、第三条和第五条记录。

但事实并非如此。当您通过从下拉列表中选择不同年份并单击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

Test 中的 LEFT Outer JOIN 操作 GET 方法应该返回

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

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

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

模型

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; }
}

博客控制器

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);
    }
}

更新

  1. 添加了第 2 步要求用户更改连接字符串
  2. Test() 的 GET/POST 操作方法中的 bp.DefaultIfEmpty(new Post()) 中删除了 new Post()。但同样的错误仍然存​​在。

【问题讨论】:

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


    【解决方案1】:

    在 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()
    

    【讨论】:

    • 感谢您的帮助。 new Post() 是在我得到意外结果后添加的。但同样的意外行为仍然存在。无论如何,我已经删除了 new Post() 并在我的帖子中添加了 UPDATE 部分。
    猜你喜欢
    • 2017-09-07
    • 2015-11-01
    • 2018-04-22
    • 1970-01-01
    • 1970-01-01
    • 2011-08-09
    • 2010-09-22
    • 2021-05-17
    • 2018-02-19
    相关资源
    最近更新 更多