OUTER JOIN未在EF Core中返回預期結果

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

在我的ASP.NET MVC Core應用程序中, POST操作方法Test沒有返回預期的結果。 Web應用程序是使用此官方ASP.NET Core站點創建的,並稍作修改。真正的應用程序可以從這裡下載,並使用最新版本的VS2015 。該應用程序正在使用EF Core 。如果下載項目,則需要執行以下步驟來測試上述意外結果:

注意 :這些步驟的順序很重要。這是一個非常小的測試項目。 Step2將創建一個名為ASPCore_Blogs的小型SQL Server Db。因此,請確保SQL Server正在運行:

  1. 下載項目後,請確保在.vs打開項目之前從項目目錄中刪除.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數據庫,並在Edit模式下打開Posts表,手動將PostYear列值分別更改為:1998,1999,1998,2001(注意:1998年有意重複)
  10. 現在,轉到同一SQL Server數據庫中的Blogs表,然後輸入一個額外的博客Blog5@test.com
  11. 現在,運行Web應用程序並再次單擊“ Test鏈接(位於主頁左側)。您將看到Get action方法Test正在使用左外連接來顯示所有5個博客,但右側列( TitleContent )值是第5行中的空白,正如預期的那樣,因為左外連接不滿足連接第5篇博客的BlogId條件。到現在為止還挺好。
  12. 現在,在Test.cshtml視圖的Year下拉列表中,選擇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操作中的左外連接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; }
}

BlogsController

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())中刪除了新的Post Test() 。但同樣的錯誤仍然存在。

熱門答案

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


Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因