EFコアで期待通りの結果を返さないOUTER JOIN

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

質問

私のASP.NET MVCコアアプリケーションでは、 POSTアクションメソッドTestは期待された結果を返していません。 Webアプリケーションは、 この公式ASP.NET Coreサイトを使用し作成され、わずかに変更されました。実際のアプリはここからダウンロードでき、 VS2015最新バージョンをVS2015ます。アプリはEF Coreを使用しています。プロジェクトをダウンロードする場合、上記の予期しない結果をテストするには、次の手順を実行する必要があります。

:これらの手順の順序は重要です。これは非常に小さなテストプロジェクトです。ステップ2では、ASPCore_Blogsという小さなSQL Server Dbが作成されます。したがって、SQL Serverが実行されていることを確認してください。

  1. プロジェクトをダウンロードした後、 .vsプロジェクトを開く前に、プロジェクトディレクトリから.vsフォルダを削除してください(プロジェクトがハングアップする場合は、Windows OSのTask Managerを使用して強制終了し、これはVS2015の既知の問題です)。
  2. startup.csファイルを開き、Configuration()メソッドで、データベースインスタンス名をMyComputer\SQLServerInstanceから使用しているインスタンスに変更します。ルートディレクトリのappsettings.jsonファイルで同じ操作を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リンクをクリックすると、Blog1@test.com、Blog2@test.com、Blog3@test.com、Blog4@test.comの4つのBlog Createれます。

  7. 上記の4つのブログがすべて作成されたことを確認するには、 Blogs Indexリンクをクリックしてください
  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データベース内のBlogsテーブルに行き、余分なブログBlog5@test.comを入力してBlog5@test.com
  11. 今度は、Webアプリケーションを実行し、もう一度[ Test ]リンク(ホームページの左側にある)をクリックします。 Get actionメソッドTestが左外部結合を使用して5つのすべてのブログを表示していますが、左の外部結合が結合を満たさないため、右側の列( TitleContent )の値は5行目の空白になります。 BlogIdの5番目のブログの条件。ここまでは順調ですね。
  12. 今、 Test.cshtmlビューのYearドロップダウンで、1998年を選択し、 GOボタンをクリックします。 POSTアクションメソッドTestの最初のif条件によると、アプリケーションは3つのレコード(1998年に2つ、結合条件を満たす5番目のもの)のみを表示します:第1、第3、第5レコード。

しかし、それは起こっていることではありません。ドロップダウンから何年もの年を選択してこのアクションを繰り返し、 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外部ジョイン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年を選択して[実行]ボタンをクリックすると、テスト(...)ポストアクションメソッドのクエリが返されます。 しかし、それはランダムに任意の行を返す

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()を削除しました。しかし、同じエラーはまだそこにあります。

人気のある回答

linqクエリでは、 DefaultIfEmtpy呼び出しを行います:

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

DefaultIfEmtpyがヌルを返すのではなく、空のときにnew Post()インスタンスを返すオーバーロードを使用しました。しかし、あなたのロジックはそれがnullを返すことを期待していnull 。 snipperの最初の行を、代わりにnullを返すオーバーロードに置き換えnull

from c in bp.DefaultIfEmpty()


Related

ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ
ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ