none
ASP.NET MVC5で1対多の2つのモデルを同時に更新する方法についてわからなくて困っています。 RRS feed

  • 質問

  • ASP.NET MVC5で 1⇔多 の2つモデルを同時に更新する方法についてわからなく困っています。 どなたかご教授お願いしたく。 例として、 (Employee) 1⇔多 (EmployeeDepartment) 多⇔1 (Department) のモデルがあり、 ※1対多の表現が間違っているかもしれませんが。 EmployeeのView(modelはEmployeeViewModel)と、EmployeesControllerを用いて、 Employee、EmployeeDepartmentsのデータ更新をしようとしています。

    例では、Employeeは複数のDepartmentを兼務するということです。 ■ Model

    public class Employee {
        public int Id { get; set; }
        public string Name { get; set; }
         
        public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; }
    }
    
    public class EmployeeDepartment {
        public int Id { get; set; }
        public int? EmployeeId { get; set; }
        public int? DepartmentId { get; set; }
    
        public Employee Employee { get; set; }
        public Department Department { get; set; }
    }
    
    public class Department {
        public int Id { get; set; }
        public string Name { get; set; }
    	
        public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; }
    }

    ■ ViewModel (このデータ取得と、データ更新をしたい)

    public class EmployeeViewModel {
        public Employee Employee { get; set; }
        public IList<EmployeeDepartment> EmployeeDepartments { get; set; }
    }

    ■ 困り事 ① データ取得時に、Modelのコレクション employeesと、そのModel中のコレクション employeeDepartments から、

    employeesViewModelに変換したいが、わからなく困っています。

    // データ取得をするコントローラー
    public async Task<ActionResult> Gets() {
    	IEnumerable<Employee> employees = await unitOfWork.EmployeeRepository.GetEmployeesAsync();
    	IEnumerable<EmployeeDepartment> employeeDepartments = await unitOfWork.EmployeeDepartmentRepository.GetEmployeeDepartmentsAsync();
    	IList<EmployeeViewModel> employeesViewModel = new List<EmployeeViewModel>();
    	
    	foreach (Employee employee in employees){
    		employeesViewModel.Add(new EmployeeViewModel() {
    			Employee = employee,
    			EmployeeDepartments = null  // 変換方法について考え中
    		});
    	}
    	
    	var json = Json(employeesViewModel, JsonRequestBehavior.AllowGet);
    	json.MaxJsonLength = int.MaxValue;
    	return json;
    }

    ② Edit時に、Modelにあるコレクションについて、Controllerへのバインド方法がわからなく困っています。

    下記で、Employeeはバインドされるが、EmployeeDepartmentsがバインドできない状態です。

    public async Task<ActionResult> Edit([Bind(Include = "Employee,EmployeeDepartments")] EmployeeViewModel employeeViewModel) 

    View(Edit)のAjax.BeginFormの中でテストのため、comboboxではなく、下記コードのinput textで試してみましたが、

    うまくいきませんでした。

    <input type="text" name="EmployeeDepartments[0].Id" value="1" hidden="hidden" />
    <input type="text" name="EmployeeDepartments[0].EmployeeId" value="1" hidden="hidden" />
    <input type="text" name="EmployeeDepartments[0].DepartmentId" value="1" hidden="hidden" />

    • 編集済み yuchan01 2020年2月26日 12:03 誤記訂正
    2020年2月26日 8:47

回答

  • 話を進めていって方針変更になっては何ですので確認させてください。

    > 例として、(Employee) 1⇔多 (EmployeeDepartment) 多⇔1 (Department) のモデルがあり、

    Employee と Department を多対多で結びつけるためだけの役割しか EmployeeDepartment エンティティには定義されてないようです。もしそうであれば、EmployeeDepartment エンティティは不要ですのでそのあたりから考え直した方がよさそうです。

    Microsoft の MVC5 + EF6 のチュートリアルの一つのセクションの説明ですが、以下の記事を見てください。

    Many-to-Many Relationships
    https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application#many-to-many-relationships

    上のセクションの最初の図の Student ⇔ Enrollment ⇔ Course が質問者さんのケースとほぼ同じですが、Enrollment エンティティを使っているのは理由があって、「この学生はこのコースではこういうグレードとなっている」という情報を保持する Grade というプロパティが必要だからです。

    単純に多対多で関係づけたいだけなら、上のセクションの図の Instructor ⇔ Course ような 2 つのエンティティだけを定義すれば済みます。

    方針変更して  Instructor ⇔ Course ような 2 つのエンティティだけにするのか、今のままにするのかを考えて、結果を連絡してください。

    また、以下の点を見直してください。

    (1) EmployeeDepartment の EmployeeId と DepartmentId の型の null 許容は適切か?(そうは思えません)

    (2) ナビゲーションプロパティには virtual を付与した方が良い。

    • 回答としてマーク yuchan01 2020年3月3日 17:02
    2020年2月27日 1:31
  • EmployeeViewModel は、

    public class EmployeeViewModel 
    {
        public Employee Employee { get; set; }
        public IList<EmployeeDepartment> EmployeeDepartments { get; set; }
    }

    ではなくて、

    public class EmployeeViewModel 
    {
        public Employee Employee { get; set; }
        public IList<Department> Departments { get; set; }
    }

    にする必要があるのではないですか?

    ① のケースでは、最終的に JSON 文字列にシリアライズしたいようですが、どのデータをどのような形で JSON 文字列にしたいのか書いてください。そうしてもらわないと話が噛み合わないと思います。
    • 編集済み SurferOnWww 2020年2月27日 9:17 追記
    • 回答としてマーク yuchan01 2020年3月3日 17:02
    2020年2月27日 8:19
  • > virtualを使用したほうが良いかについては、一旦ここでの話からは外させてください。
    > 遅延実行プロパティについては今後、必要時に使い分けしていこうと考えています。

    一旦外させてほしいと言われているのに何ですが一言だけ。

    遅延ローディングを有効にするのは .NET Framework の EF ではベストプラクティス・・・とまでは言えないかもしれませんが、少なくともスタンダードプラクティスです。普通は使い分けするようなことではないはずです。

    virtual を外すのは、どうしても遅延ローディングでは困るという明確な理由がある場合のみにしておくべきと思います。明確な理由もないのに virtual を外すと、思わぬところで思わぬ好ましからざる副作用(1+N 問題とか)が出るかもしれません。

    ですが、virtual の有無と今回の問題は関係なさそうですので、とりあえずその話は置いときます。

    > そのjsonデータをView側に取得できていません。

    循環参照のエラーが出てませんか? 試しに先に紹介したチュートリアルの  Student ⇔ Enrollment ⇔ Course のケースを、以下のコードで試してみました。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                return Json(list, JsonRequestBehavior.AllowGet);
            }
        }
    }


    結果以下の通り循環参照エラーになります。

    なぜそうなるかは以下の記事に書いてあるようなことが起きているからだろうと思います。

    ASP.NET Web API で循環参照なモデルの公開を解決する
    https://devadjust.exblog.jp/19071546/

    であれば、解決策は JSON にシリアライズするオブジェクトに含まれるクラスからナビゲーションプロパティを除去すれば良いはずで、ちょっとベタですが、上のコードを以下のようにすれば循環参照は回避できるはずです。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class Student2
        {
            public int ID { get; set; }
            public string LastName { get; set; }
            public string FirstMidName { get; set; }
            public DateTime EnrollmentDate { get; set; }
        }
    
        public class Enrollment2
        {
            public int EnrollmentID { get; set; }
            public int CourseID { get; set; }
            public int StudentID { get; set; }
            public Nullable<int> Grade { get; set; }
        }
    
        public class StudentWithEnrollments
        {
            public Student2 Student { get; set; }
            public IList<Enrollment2> Enrollments { get; set; }
        }
    
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                var model = new List<StudentWithEnrollments>();
                foreach (Student s in list)
                {
                    var s2 = new Student2
                    {
                        ID = s.ID,
                        LastName = s.LastName,
                        FirstMidName = s.FirstMidName,
                        EnrollmentDate = s.EnrollmentDate
                    };
    
                    var studentWithEnrollments = new StudentWithEnrollments
                    {
                        Student = s2,
                        Enrollments = new List<Enrollment2>()
                    };
                    
                    foreach (Enrollment e in s.Enrollment)
                    {
                        var enrollment2 = new Enrollment2
                        {
                            EnrollmentID = e.EnrollmentID,
                            CourseID = e.CourseID,
                            StudentID = e.StudentID,
                            Grade = e.Grade
                        };
                        studentWithEnrollments.Enrollments.Add(enrollment2);
                    }
                    model.Add(studentWithEnrollments);
                }
    
                return Json(model, JsonRequestBehavior.AllowGet);
            }
        }
    }


    結果は以下のようになります。(注: DateTime は JSON にシリアライズできないので "\/Date(1125500400000)\/" のような文字列になります。形はシリアライザによって異なります)

    [
      {"Student":{"ID":1,"LastName":"Alexander","FirstMidName":"Carson","EnrollmentDate":"\/Date(1125500400000)\/"},
       "Enrollments":[{"EnrollmentID":1,"CourseID":1050,"StudentID":1,"Grade":0},
                      {"EnrollmentID":2,"CourseID":4022,"StudentID":1,"Grade":2},
                      {"EnrollmentID":3,"CourseID":4041,"StudentID":1,"Grade":1}]},
      {"Student":{"ID":2,"LastName":"Alonso","FirstMidName":"Meredith","EnrollmentDate":"\/Date(1030806000000)\/"},
       "Enrollments":[{"EnrollmentID":4,"CourseID":1045,"StudentID":2,"Grade":1},
                      {"EnrollmentID":5,"CourseID":3141,"StudentID":2,"Grade":4},
                      {"EnrollmentID":6,"CourseID":2021,"StudentID":2,"Grade":4}]},
     ・・・中略・・・
    ]

    お試しください。

    JSON 文字列の DateTime の問題をどう解決するかと ② の課題の議論は、上記のところまでできてからにしましょう。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年2月28日 1:55
  • 【追伸】

    > (Viewへ渡したいEmployeeのJSONデータ)

    今気が付きましたが、その形にするなら、わざわざ EmployeeViewModel クラスを定義して List<EmployeeViewModel> オブジェクトを作ってそれをシリアライズするのでなく、List<Employee> をそのままシリアライズすべきです。

    先に紹介したチュートリアルの  Student ⇔ Enrollment ⇔ Course のケースで言うと以下の通りです。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class Enrollment2
        {
            public int EnrollmentID { get; set; }
            public int CourseID { get; set; }
            public int StudentID { get; set; }
            public Nullable<int> Grade { get; set; }
        }
    
        public class Student3
        {
            public int ID { get; set; }
            public string LastName { get; set; }
            public string FirstMidName { get; set; }
            public DateTime EnrollmentDate { get; set; }
            public IList<Enrollment2> Enrollments { get; set; }
        }
    
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                var model = new List<Student3>();
                foreach (Student s in list)
                {
                    var s3 = new Student3
                    {
                        ID = s.ID,
                        LastName = s.LastName,
                        FirstMidName = s.FirstMidName,
                        EnrollmentDate = s.EnrollmentDate,
                        Enrollments = new List<Enrollment2>()
                    };
    
                    foreach (Enrollment e in s.Enrollment)
                    {
                        var enrollment2 = new Enrollment2
                        {
                            EnrollmentID = e.EnrollmentID,
                            CourseID = e.CourseID,
                            StudentID = e.StudentID,
                            Grade = e.Grade
                        };
                        s3.Enrollments.Add(enrollment2);
                    }
                    model.Add(s3);
                }
    
                return Json(model, JsonRequestBehavior.AllowGet);
            }
        }
    }

    結果の JSON 文字列は以下のようになります。(改行、インデントを入れてます)

    [
      {
        "ID":1,
        "LastName":"Alexander",
        "FirstMidName":"Carson",
        "EnrollmentDate":"\/Date(1125500400000)\/",
        "Enrollments":
           [
             {"EnrollmentID":1,"CourseID":1050,"StudentID":1,"Grade":0},
             {"EnrollmentID":2,"CourseID":4022,"StudentID":1,"Grade":2},
             {"EnrollmentID":3,"CourseID":4041,"StudentID":1,"Grade":1}
          ]
      },
      { 
        "ID":2,"LastName":"Alonso",
        "FirstMidName":"Meredith",
        "EnrollmentDate":"\/Date(1030806000000)\/",
        "Enrollments":
          [
            {"EnrollmentID":4,"CourseID":1045,"StudentID":2,"Grade":1},
            {"EnrollmentID":5,"CourseID":3141,"StudentID":2,"Grade":4},
            {"EnrollmentID":6,"CourseID":2021,"StudentID":2,"Grade":4}
          ]
      },
    ・・・中略・・・
    ]

    • 編集済み SurferOnWww 2020年2月29日 4:03 訂正
    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年2月29日 3:32
  • Json.NET でしかうまく動かなかったそうですが、うまく動かなかった方の Controller.Json の場合はどうなったのでしょう? (使っているシリアライザが違うので、それと JsonIgnore 属性の関係ではなかろうかと想像していますが)

    Edit の際のモデルバインディングの件ですが、コレクションのモデルバインディングがうまく行われるようにするには、レンダリングされる html の input 要素の name 属性が連番のインデックスを含む必要があります。ICollection ではそこのところに不都合があるので IList に変更してください。

    今、スマホしか使えないので詳しいことが書けません。あとでもう少し詳しく説明します。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月1日 23:09
  • 最初の質問の ② の Edit の際、モデルバインディングをどのようにできるかについてもう少し詳しく書きます。

    上の私のレスでも書きましたが、コレクションのモデルバインディングがうまく行われるようにするには、レンダリングされる html の input 要素の name 属性が連番のインデックスを含む必要があります。

    具体的には、name="prefix[index].Property" というパターンにします。prefix の部分にはアクションメソッドの引数の中の当該パラメータ名が入ります。index は 0 から始まる連番です。

    詳しくは以下の記事を見てください。

    コレクションのデータアノテーション検証
    http://surferonwww.info/BlogEngine/post/2014/09/01/validation-of-collection-data-during-model-binding-using-data-annotation.aspx

    先の私のレスで紹介した Microsoft のチュートリアルの一番最初の手順(URL 下記)に従って作ったアプリを例に取ってもっと具体的に説明します。

    Tutorial: Get Started with Entity Framework 6 Code First using MVC 5
    https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application

    上のチュートリアルの Student クラスと Course クラスに定義されているナビゲーションプロパティの ICollection<Enrollment>, ICollection<Enrollment> ですが、ICollection では name="prefix[index].Property" という連番を付与するのに不都合があるので IList に変更しました。

    それ以外はチュートリアルと全く同じコード手順で Visual Studio 2019 のスキャフォールディング機能を使って Student を CRUD できる Controller と View を自動生成し、EF Code First の機能を使って SQL Server に DB を生成したものを以下の説明に使います。

    Controller の Edit アクションメソッドはそのまま使います。ただし、Enrollments をホワイトリストに追加しています。以下の画像を見てください。

    View は自動生成されたコードに以下の画像の赤枠で囲ったコードを追加し、Enrollments も表示できるようにします。このとき、model.Enrollments[i].EnrollmentID のようにインデックスを付与しているところに注目してください。これが ICollection では不都合なので IList に変更した理由です。

    結果、以下の通り html ソースでは name="prefix[index].Property" というパターンになります。

    POST すると、以下の画像のように、期待通り Enrollment のコレクションもモデルバインドされます。

    ただし、 Enrollment も UPDATE されるようにするためには、以下の記事の「Edit アクションメソッド」のコードのようにする必要があると思います。(Student の例では未検証・未確認ですが)

    親子関係のあるデータの編集・削除
    http://surferonwww.info/BlogEngine/post/2014/12/22/edit-and-delete-relational-data-in-parent-and-chilid-tables-of-sql-server-database.aspx


    • 編集済み SurferOnWww 2020年3月2日 1:59 訂正
    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月2日 1:46
  • > JsonIgnore属性が Json()のほうで有効でなかったかはまだ確認できていません。

    .NET Framework MVC の Controller.Json メソッドを利用した場合、内部で使用されるシリアライザは JavaScriptSerializer です。

    一方、紹介した記事で JsonIgnore 属性を付与して解決したというのは Newtonsoft の Json.NET をシリアライザに使ったときの話です。

    試すまでもないと思ったので検証はしてませんが、JsonIgnore 属性は JavaScriptSerializer には無効ということなのだと思います。

    (ASP.NET Core 3.0 以降だと話は違うかもしれません)


    > ただしDBにデータがないとき、正しいvalueが取得できない状態なので調べているところです。
    > → value="{ id = EmployeeDepartments_0__Id } のような形式になってしまいます。

    意味が分からないのですが?

    「DBにデータがないとき」というのは Employee に紐づく EmployeeDepartment がない時ということであれば変ですね。やり方がどこか間違っているのではないかと思います。

    ちなみに Microsoft のチュートリアルの Student ⇔ Enrollment ⇔ Course のケースで試してみると、Student に紐づく Enrollment がない場合は以下の画像のようになるので、

    上のレスの View の画像にある for ループは一回も回らず、当然 Enrollment はレンダリングされない(input 要素は出力されない)ので value 云々は関係ないです。ブラウザの表示は以下のようになります。

    上の画像の状態から Save ボタンクリックで POST すると以下のようになります。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月3日 2:00

すべての返信

  • 話を進めていって方針変更になっては何ですので確認させてください。

    > 例として、(Employee) 1⇔多 (EmployeeDepartment) 多⇔1 (Department) のモデルがあり、

    Employee と Department を多対多で結びつけるためだけの役割しか EmployeeDepartment エンティティには定義されてないようです。もしそうであれば、EmployeeDepartment エンティティは不要ですのでそのあたりから考え直した方がよさそうです。

    Microsoft の MVC5 + EF6 のチュートリアルの一つのセクションの説明ですが、以下の記事を見てください。

    Many-to-Many Relationships
    https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application#many-to-many-relationships

    上のセクションの最初の図の Student ⇔ Enrollment ⇔ Course が質問者さんのケースとほぼ同じですが、Enrollment エンティティを使っているのは理由があって、「この学生はこのコースではこういうグレードとなっている」という情報を保持する Grade というプロパティが必要だからです。

    単純に多対多で関係づけたいだけなら、上のセクションの図の Instructor ⇔ Course ような 2 つのエンティティだけを定義すれば済みます。

    方針変更して  Instructor ⇔ Course ような 2 つのエンティティだけにするのか、今のままにするのかを考えて、結果を連絡してください。

    また、以下の点を見直してください。

    (1) EmployeeDepartment の EmployeeId と DepartmentId の型の null 許容は適切か?(そうは思えません)

    (2) ナビゲーションプロパティには virtual を付与した方が良い。

    • 回答としてマーク yuchan01 2020年3月3日 17:02
    2020年2月27日 1:31
  • ご回答ありがとうございます。
    EmployeeDepartmentには付加情報を含めるため、例としてStartDateを追加してます。
    そのため、Enrollmentに該当するクラスのEmployeeDepartmentは残したいと考えてます。

    ※int?が適切でないため訂正、また、virtualを追加してます。

    public class EmployeeDepartment { public int Id { get; set; }
    public int EmployeeId { get; set; }
    public int DepartmentId { get; set; }
    public DateTime StartDate { get; set; }
    public virtual Employee Employee { get; set; }
    public virtual Department Department { get; set; }
    }

    many to many relationshipsのInstructor-courseの多対多の設定方法については
    少し理解しました。

    ①のViewModelの変換方法は、Employeeにincludeした情報により、解決済ですが、
    viewに返したjson中の入れ子のデータ(EmployeeDepartment)ついては、うまく表示ができていません。




    • 編集済み yuchan01 2020年2月27日 4:14
    2020年2月27日 4:09
  • EmployeeViewModel は、

    public class EmployeeViewModel 
    {
        public Employee Employee { get; set; }
        public IList<EmployeeDepartment> EmployeeDepartments { get; set; }
    }

    ではなくて、

    public class EmployeeViewModel 
    {
        public Employee Employee { get; set; }
        public IList<Department> Departments { get; set; }
    }

    にする必要があるのではないですか?

    ① のケースでは、最終的に JSON 文字列にシリアライズしたいようですが、どのデータをどのような形で JSON 文字列にしたいのか書いてください。そうしてもらわないと話が噛み合わないと思います。
    • 編集済み SurferOnWww 2020年2月27日 9:17 追記
    • 回答としてマーク yuchan01 2020年3月3日 17:02
    2020年2月27日 8:19
  • virtualを使用したほうが良いかについては、一旦ここでの話からは外させてください。
    遅延実行プロパティについては今後、必要時に使い分けしていこうと考えています。
    ※私の勘違いで、今回必須である場合は必要にはなりますが。

    ■ 訂正後の現在のModel

    public class Employee { public int Id { get; set; } public string Name { get; set; } public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; } } public class EmployeeDepearment { public int Id { get; set; } public int EmployeeId { get; set; } public int DepartmentId { get; set; } public DateTime StartDate { get; set; } // 例として追加情報 public Employee Employee { get; set; } public Department Department { get; set; } }

    public class Department { public int Id { get; set; } public string Name { get; set; } public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; } }

    public class EmployeeViewModel {
        public Employee Employee { get; set; }
    public IList<EmployeeEnrollment> EmployeeEnrollments { get; set; }
    }

    今回はEmployeeの画面でEmployeeと、EmployeeEnrollmentの2つを同時に更新したいため、
    ViewModelについて、IList<Department>ではなく、IList<EmployeeEnrollment>を使用したいです。
    考え方が間違っているのでしょうか?

    ※そもそもViewModelで、ICollectionをIListに変換する必要はないかもしれません。

    また、MSドキュメントの手順は似ているようで、やりたいことが若干異なっているかもしれません。

    (Viewへ渡したいEmployeeのJSONデータ)

    {
      { "Id":"1",
        "Name":"emp1",
        "EmployeeDepartments": [
          { "Id":"1", "EmployeeId":"1", "DepartmentId":"1", "DateTime":"2020-02-01", ... }, 
          { "Id":"2", "EmployeeId":"1", "DepartmentId":"2", "DateTime":"2020-02-01", ... }, 
          { "Id":"3", "EmployeeId":"1", "DepartmentId":"3", "DateTime":"2020-02-01", ... }, 
        ],
      },
      { "Id":"2",
        "Name":"emp2",
        "EmployeeDepartments": [
          { "Id":"4", "EmployeeId":"2", "DepartmentId":"1", "DateTime":"2020-02-01", ... }, 
        ],
      },
    }

    ①のViewModelの操作について

    EmployeeViewModelを下記のように対応しましたが、Controllerのvar jsonにはデータが入っていますが、

    そのjsonデータをView側に取得できていません。


    (Controller)

    public async Task<ActionResult> Gets() {
        IList<EmployeeViewModel> employeesViewModel = new List<EmployeeViewModel>();
        IEnumerable<Employee> employees = await unitOfWork.EmployeeRepository.GetEmployeesAsync();
    
        foreach (Employee employee in employees) {
            employeesViewModel.Add(new EmployeeViewModel() {
                Employee = employee,
                EmployeeDepartments = employee.EmployeeDepartments.ToList()
            });
        }
    
        var json = Json(employeesViewModel, JsonRequestBehavior.AllowGet);
        json.MaxJsonLength = int.MaxValue;
        return json;
    }


    (Viewで$.getJSONを使ってテスト)
    $.getJSONを使用しても実行できず、エラーが発生しています。

    var x = {};
    $.getJSON("@Url.Action("Gets", "Employees")", null, function (response) {
         x = response;
    });


    Repositoryで.Include(e => e.EmployeeDepartments) を外して、
    ViewModelを使用せず、Employeeで対応していた時は動作していました。  

    jsonデータに入れ子の配列(employeeDepartments)があるとデータ取得できないような気がしています。




    • 編集済み yuchan01 2020年2月27日 16:07
    2020年2月27日 15:50
  • > virtualを使用したほうが良いかについては、一旦ここでの話からは外させてください。
    > 遅延実行プロパティについては今後、必要時に使い分けしていこうと考えています。

    一旦外させてほしいと言われているのに何ですが一言だけ。

    遅延ローディングを有効にするのは .NET Framework の EF ではベストプラクティス・・・とまでは言えないかもしれませんが、少なくともスタンダードプラクティスです。普通は使い分けするようなことではないはずです。

    virtual を外すのは、どうしても遅延ローディングでは困るという明確な理由がある場合のみにしておくべきと思います。明確な理由もないのに virtual を外すと、思わぬところで思わぬ好ましからざる副作用(1+N 問題とか)が出るかもしれません。

    ですが、virtual の有無と今回の問題は関係なさそうですので、とりあえずその話は置いときます。

    > そのjsonデータをView側に取得できていません。

    循環参照のエラーが出てませんか? 試しに先に紹介したチュートリアルの  Student ⇔ Enrollment ⇔ Course のケースを、以下のコードで試してみました。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                return Json(list, JsonRequestBehavior.AllowGet);
            }
        }
    }


    結果以下の通り循環参照エラーになります。

    なぜそうなるかは以下の記事に書いてあるようなことが起きているからだろうと思います。

    ASP.NET Web API で循環参照なモデルの公開を解決する
    https://devadjust.exblog.jp/19071546/

    であれば、解決策は JSON にシリアライズするオブジェクトに含まれるクラスからナビゲーションプロパティを除去すれば良いはずで、ちょっとベタですが、上のコードを以下のようにすれば循環参照は回避できるはずです。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class Student2
        {
            public int ID { get; set; }
            public string LastName { get; set; }
            public string FirstMidName { get; set; }
            public DateTime EnrollmentDate { get; set; }
        }
    
        public class Enrollment2
        {
            public int EnrollmentID { get; set; }
            public int CourseID { get; set; }
            public int StudentID { get; set; }
            public Nullable<int> Grade { get; set; }
        }
    
        public class StudentWithEnrollments
        {
            public Student2 Student { get; set; }
            public IList<Enrollment2> Enrollments { get; set; }
        }
    
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                var model = new List<StudentWithEnrollments>();
                foreach (Student s in list)
                {
                    var s2 = new Student2
                    {
                        ID = s.ID,
                        LastName = s.LastName,
                        FirstMidName = s.FirstMidName,
                        EnrollmentDate = s.EnrollmentDate
                    };
    
                    var studentWithEnrollments = new StudentWithEnrollments
                    {
                        Student = s2,
                        Enrollments = new List<Enrollment2>()
                    };
                    
                    foreach (Enrollment e in s.Enrollment)
                    {
                        var enrollment2 = new Enrollment2
                        {
                            EnrollmentID = e.EnrollmentID,
                            CourseID = e.CourseID,
                            StudentID = e.StudentID,
                            Grade = e.Grade
                        };
                        studentWithEnrollments.Enrollments.Add(enrollment2);
                    }
                    model.Add(studentWithEnrollments);
                }
    
                return Json(model, JsonRequestBehavior.AllowGet);
            }
        }
    }


    結果は以下のようになります。(注: DateTime は JSON にシリアライズできないので "\/Date(1125500400000)\/" のような文字列になります。形はシリアライザによって異なります)

    [
      {"Student":{"ID":1,"LastName":"Alexander","FirstMidName":"Carson","EnrollmentDate":"\/Date(1125500400000)\/"},
       "Enrollments":[{"EnrollmentID":1,"CourseID":1050,"StudentID":1,"Grade":0},
                      {"EnrollmentID":2,"CourseID":4022,"StudentID":1,"Grade":2},
                      {"EnrollmentID":3,"CourseID":4041,"StudentID":1,"Grade":1}]},
      {"Student":{"ID":2,"LastName":"Alonso","FirstMidName":"Meredith","EnrollmentDate":"\/Date(1030806000000)\/"},
       "Enrollments":[{"EnrollmentID":4,"CourseID":1045,"StudentID":2,"Grade":1},
                      {"EnrollmentID":5,"CourseID":3141,"StudentID":2,"Grade":4},
                      {"EnrollmentID":6,"CourseID":2021,"StudentID":2,"Grade":4}]},
     ・・・中略・・・
    ]

    お試しください。

    JSON 文字列の DateTime の問題をどう解決するかと ② の課題の議論は、上記のところまでできてからにしましょう。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年2月28日 1:55
  • 【追伸】

    > (Viewへ渡したいEmployeeのJSONデータ)

    今気が付きましたが、その形にするなら、わざわざ EmployeeViewModel クラスを定義して List<EmployeeViewModel> オブジェクトを作ってそれをシリアライズするのでなく、List<Employee> をそのままシリアライズすべきです。

    先に紹介したチュートリアルの  Student ⇔ Enrollment ⇔ Course のケースで言うと以下の通りです。

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Threading.Tasks;
    using System.Net;
    using System.Web.Mvc;
    using Mvc5App;
    
    namespace Mvc5App.Controllers
    {
        public class Enrollment2
        {
            public int EnrollmentID { get; set; }
            public int CourseID { get; set; }
            public int StudentID { get; set; }
            public Nullable<int> Grade { get; set; }
        }
    
        public class Student3
        {
            public int ID { get; set; }
            public string LastName { get; set; }
            public string FirstMidName { get; set; }
            public DateTime EnrollmentDate { get; set; }
            public IList<Enrollment2> Enrollments { get; set; }
        }
    
        public class StudentsController : Controller
        {
            private ContosoUniversityEntities db = new ContosoUniversityEntities();
    
            public ActionResult Json()
            {
                var list = db.Student.Include(s => s.Enrollment);
                var model = new List<Student3>();
                foreach (Student s in list)
                {
                    var s3 = new Student3
                    {
                        ID = s.ID,
                        LastName = s.LastName,
                        FirstMidName = s.FirstMidName,
                        EnrollmentDate = s.EnrollmentDate,
                        Enrollments = new List<Enrollment2>()
                    };
    
                    foreach (Enrollment e in s.Enrollment)
                    {
                        var enrollment2 = new Enrollment2
                        {
                            EnrollmentID = e.EnrollmentID,
                            CourseID = e.CourseID,
                            StudentID = e.StudentID,
                            Grade = e.Grade
                        };
                        s3.Enrollments.Add(enrollment2);
                    }
                    model.Add(s3);
                }
    
                return Json(model, JsonRequestBehavior.AllowGet);
            }
        }
    }

    結果の JSON 文字列は以下のようになります。(改行、インデントを入れてます)

    [
      {
        "ID":1,
        "LastName":"Alexander",
        "FirstMidName":"Carson",
        "EnrollmentDate":"\/Date(1125500400000)\/",
        "Enrollments":
           [
             {"EnrollmentID":1,"CourseID":1050,"StudentID":1,"Grade":0},
             {"EnrollmentID":2,"CourseID":4022,"StudentID":1,"Grade":2},
             {"EnrollmentID":3,"CourseID":4041,"StudentID":1,"Grade":1}
          ]
      },
      { 
        "ID":2,"LastName":"Alonso",
        "FirstMidName":"Meredith",
        "EnrollmentDate":"\/Date(1030806000000)\/",
        "Enrollments":
          [
            {"EnrollmentID":4,"CourseID":1045,"StudentID":2,"Grade":1},
            {"EnrollmentID":5,"CourseID":3141,"StudentID":2,"Grade":4},
            {"EnrollmentID":6,"CourseID":2021,"StudentID":2,"Grade":4}
          ]
      },
    ・・・中略・・・
    ]

    • 編集済み SurferOnWww 2020年2月29日 4:03 訂正
    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年2月29日 3:32
  • 上の私のレスの後、質問者さんが無反応になってしまいましたが、もう解決したのですか? それとも諦めたのですか? とにかくレスに対しては、読んだ・後で読む、役に立った・立たなかったぐらいはタイムリーにフィードバックしていただきたく。
    2020年3月1日 7:19
  • ご返信遅くなり申し訳ありません。

    うまくいかなった原因はご指摘のとおり、循環参照が原因でした。

    教えて頂いたURLより、[JsonIgnore]属性を使用して、含めないように対応しました。

    public class Employee {
        public int Id { get; set; }
        public string Name { get; set; }
    	
        public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; }
    }
    
    public class EmployeeDepearment {
        public int Id { get; set; }
        public int EmployeeId { get; set; }
        public int DepartmentId { get; set; }
        public DateTime StartDate { get; set; }    // 例として追加情報
      
        [JsonIgnore]
        public Employee Employee { get; set; }
    
        [JsonIgnore]
        public Department Department { get; set; }
    }
    public class Department {
        public int Id { get; set; }
        public string Name { get; set; }
    	
        public ICollection<EmployeeDepartment> EmployeeDepartments { get; set; }
    }
    


    また、ご指摘のとおり、EmployeeViewModelを作る必要がないことも気づきましたので、

    EmployeeDepartmentsのCollectionをそのまま使うようにしようと思います。

    私の環境では、ICollectionを使った時に、下記のコードがうまく働かなく

    var json = Json(employees, JsonRequestBehavior.AllowGet);
    json.MaxJsonLength = int.MaxValue;
    return json;
    下記のコードでしか動かなかったのですが
    var json = Newtonsoft.Json.JsonConvert.SerializeObject(employees);
    return Content(json, "application/json")


    ご掲示頂いたコードのListのそのまま詰める方法はまだ試していませんでしたので、一旦試した後でご報告させていただきます。

    View側のほうについてですが、Listの値を取り出すところまではできました。

    @foreach (var employeeDepartment in Model.EmployeeDepartments) { @employeeDepartment.Id<br /> @employeeDepartment.EmployeeId<br /> @employeeDepartment.DepartmentId<br /> @employeeDepartment.StartDate<br /> }


    ② Edit時に、Modelにあるコレクションについて、Controllerへのバインド方法については、

    ViewModelを使用しなくてよくなったことで、どの値を渡せばよいかわかりましたので、

    EmployeeDepartmentのListに値を正しく入れれば、バインドができそうですが、

    こちらについても結果を後ほどご報告いたします。



    • 編集済み yuchan01 2020年3月1日 16:19
    2020年3月1日 16:15
  • Json.NET でしかうまく動かなかったそうですが、うまく動かなかった方の Controller.Json の場合はどうなったのでしょう? (使っているシリアライザが違うので、それと JsonIgnore 属性の関係ではなかろうかと想像していますが)

    Edit の際のモデルバインディングの件ですが、コレクションのモデルバインディングがうまく行われるようにするには、レンダリングされる html の input 要素の name 属性が連番のインデックスを含む必要があります。ICollection ではそこのところに不都合があるので IList に変更してください。

    今、スマホしか使えないので詳しいことが書けません。あとでもう少し詳しく説明します。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月1日 23:09
  • 最初の質問の ② の Edit の際、モデルバインディングをどのようにできるかについてもう少し詳しく書きます。

    上の私のレスでも書きましたが、コレクションのモデルバインディングがうまく行われるようにするには、レンダリングされる html の input 要素の name 属性が連番のインデックスを含む必要があります。

    具体的には、name="prefix[index].Property" というパターンにします。prefix の部分にはアクションメソッドの引数の中の当該パラメータ名が入ります。index は 0 から始まる連番です。

    詳しくは以下の記事を見てください。

    コレクションのデータアノテーション検証
    http://surferonwww.info/BlogEngine/post/2014/09/01/validation-of-collection-data-during-model-binding-using-data-annotation.aspx

    先の私のレスで紹介した Microsoft のチュートリアルの一番最初の手順(URL 下記)に従って作ったアプリを例に取ってもっと具体的に説明します。

    Tutorial: Get Started with Entity Framework 6 Code First using MVC 5
    https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application

    上のチュートリアルの Student クラスと Course クラスに定義されているナビゲーションプロパティの ICollection<Enrollment>, ICollection<Enrollment> ですが、ICollection では name="prefix[index].Property" という連番を付与するのに不都合があるので IList に変更しました。

    それ以外はチュートリアルと全く同じコード手順で Visual Studio 2019 のスキャフォールディング機能を使って Student を CRUD できる Controller と View を自動生成し、EF Code First の機能を使って SQL Server に DB を生成したものを以下の説明に使います。

    Controller の Edit アクションメソッドはそのまま使います。ただし、Enrollments をホワイトリストに追加しています。以下の画像を見てください。

    View は自動生成されたコードに以下の画像の赤枠で囲ったコードを追加し、Enrollments も表示できるようにします。このとき、model.Enrollments[i].EnrollmentID のようにインデックスを付与しているところに注目してください。これが ICollection では不都合なので IList に変更した理由です。

    結果、以下の通り html ソースでは name="prefix[index].Property" というパターンになります。

    POST すると、以下の画像のように、期待通り Enrollment のコレクションもモデルバインドされます。

    ただし、 Enrollment も UPDATE されるようにするためには、以下の記事の「Edit アクションメソッド」のコードのようにする必要があると思います。(Student の例では未検証・未確認ですが)

    親子関係のあるデータの編集・削除
    http://surferonwww.info/BlogEngine/post/2014/12/22/edit-and-delete-relational-data-in-parent-and-chilid-tables-of-sql-server-database.aspx


    • 編集済み SurferOnWww 2020年3月2日 1:59 訂正
    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月2日 1:46
  • EmployeeDepartmentsをList型に変更して、教えていただいた方法でList変換したところ、

    循環参照なく、変換することができました。 JsonIgnore属性が Json()のほうで有効でなかったかはまだ確認できていません。

    public async Task<ActionResult> Gets() { IEnumerable<Employee> employees = await unitOfWork.EmployeeRepository.GetEmployeesAsync(); IList<Employee> employeesViewModel = new List<Employee>(); foreach (Employee r in employees) { Employee employee = new Employee() { Id = r.Id, Name = r.Name, EmployeeDepartments = new List<EmployeeDepartment>() }; foreach (EmployeeDepartment r2 in r.EmployeeDepartments) { EmployeeDepartment employeeDepartment = new EmployeeDepartment() { Id = r2.Id, EmployeeId = r2.EmployeeId, DepartmentId = r2.DepartmentId, }; employee.EmployeeDepartments.Add(employeeDepartment); } employeesViewModel.Add(employee); } var json = Json(employeesViewModel, JsonRequestBehavior.AllowGet); json.MaxJsonLength = int.MaxValue; return json; }


    input 要素の name 属性が連番のインデックスを含むようにして、データがあるときにControllerへのモデルバインディングを確認できました。(SQL Server Management Studioでデータを手動で入れた時)

    ただしDBにデータがないとき、正しいvalueが取得できない状態なので調べているところです。

    → value="{ id = EmployeeDepartments_0__Id } のような形式になってしまいます。

    一旦、①②の方法については解決できましたので、

    あとは、javascriptの部分は書き間違い等かもしれませんので、明日引き続き調べて試してみたいと思います。

    データの更新については該当モデルのRepositoryを2つ用意して、同じトランザクションで更新しようかと考えています。

    Htmlヘルパーのidは事前に記述する必要はあるかと思います。

    @{ name = "EmployeeDepartments1; // jqueryプラグインのselect2を使用しているため、select要素はDepartmentから取得、 // Employeeモデルのほうは List<EmployeeDepartment>の値を格納するのにHiddenForを使用(説明は省略) // 実際はForループで複数作成 <div><select id = "@name"></select></div>

    // DBにデータがないとき、正しいvalueが取得できない // → value="{ id = EmployeeDepartments_0__Id }" のようなvalueになる if(Model.EmployeeDepartments != null) { @Html.HiddenFor(model => model.EmployeeDepartments[0].Id) @Html.HiddenFor(model => model.EmployeeDepartments[0].EmployeeId) @Html.HiddenFor(model => model.EmployeeDepartments[0].DepartmentId) @Html.HiddenFor(model => model.EmployeeDepartments[0].StartDate) } else { @Html.Hidden("EmployeeDepartments[0].Id", new { @id = "EmployeeDepartments_0__Id" }) @Html.Hidden("EmployeeDepartments[0].EmployeeId", new { @id = "EmployeeDepartments_0__EmployeeId" }) @Html.Hidden("EmployeeDepartments[0].DepartmentId", new { @id = "EmployeeDepartments_0__DepartmentId" }) @Html.Hidden("EmployeeDepartments[0].StartDate", new { @id = "EmployeeDepartments_0__StartDate" }) } }





    • 編集済み yuchan01 2020年3月2日 17:28
    2020年3月2日 17:22
  • > JsonIgnore属性が Json()のほうで有効でなかったかはまだ確認できていません。

    .NET Framework MVC の Controller.Json メソッドを利用した場合、内部で使用されるシリアライザは JavaScriptSerializer です。

    一方、紹介した記事で JsonIgnore 属性を付与して解決したというのは Newtonsoft の Json.NET をシリアライザに使ったときの話です。

    試すまでもないと思ったので検証はしてませんが、JsonIgnore 属性は JavaScriptSerializer には無効ということなのだと思います。

    (ASP.NET Core 3.0 以降だと話は違うかもしれません)


    > ただしDBにデータがないとき、正しいvalueが取得できない状態なので調べているところです。
    > → value="{ id = EmployeeDepartments_0__Id } のような形式になってしまいます。

    意味が分からないのですが?

    「DBにデータがないとき」というのは Employee に紐づく EmployeeDepartment がない時ということであれば変ですね。やり方がどこか間違っているのではないかと思います。

    ちなみに Microsoft のチュートリアルの Student ⇔ Enrollment ⇔ Course のケースで試してみると、Student に紐づく Enrollment がない場合は以下の画像のようになるので、

    上のレスの View の画像にある for ループは一回も回らず、当然 Enrollment はレンダリングされない(input 要素は出力されない)ので value 云々は関係ないです。ブラウザの表示は以下のようになります。

    上の画像の状態から Save ボタンクリックで POST すると以下のようになります。

    • 回答としてマーク yuchan01 2020年3月3日 17:03
    2020年3月3日 2:00
  • ご指摘の部分、他、間違いがありましたので修正したところ、動くようになりました。ありがとうございます。

    アドバイスいただいた一連の流れを自分で行った手順を記載させていただきます。

    (今回したかったこと)

    関連する2つのモデルを更新したい (メインのモデルと関連するサブのモデルを更新する処理)

    ■ Model

    単一データ(Employee employee)の時、Includeを指定する方法がわかりませんでしたので、virtualの設定をして読込させました。私の環境で、jsonで1000~2000レコードのデータを扱うとき、virtualを設定するとレスポンスが悪くなることがありましたので、遅延実行プロパティは大容量のデータを扱いにくいのかもしれませんが。

    関連するモデルの設定に通常 ICollectionを使うところ、添え字を使いたいため、IListを使用

    ※RowVersion追加 (最初は省略して記載させていただいてましたが、Repositoryで子データの新規 or 更新の判定に使用しているので例として追加してます)

    ※Departmentを選択しない時があるので、DepartmentIdはint?でNULLを許可する必要がありました。EmployeeIdはDepartmentを選択しない時があるので、NULL 許可しました。(ただし、データがある時はかならず自分自身のEmployeeIdを入れてあげる必要がある)

    public class Employee {
        public int Id { get; set; }
        public string Name { get; set; }
        public byte[] RowVersion { get; set; }
    	
        public virtual IList<EmployeeDepartment> EmployeeDepartments { get; set; }
    }
    
    public class EmployeeDepearment {
        public int Id { get; set; }
        public int? EmployeeId { get; set; }
        public int? DepartmentId { get; set; }
        public DateTime StartDate { get; set; }    // 例として追加情報
        public byte[] RowVersion { get; set; }
    
        public virtual Employee Employee { get; set; }
        public virtual Department Department { get; set; }
    }
    
    public class Department {
        public int Id { get; set; }
        public string Name { get; set; }
        public byte[] RowVersion { get; set; }
    	
        public virtual IList<EmployeeDepartment> EmployeeDepartments { get; set; }
    }

    ■ Controller (例として Gets:データ取得, Edit:変更 のみ)

    親と子のデータ(Employee - EmployeeDepartment) の更新方法として、json変換前に循環参照が起きないようにListを使用して調整

    public async Task<ActionResult> Modal(string id) {
        Employee employee = new Employee();
        if (!String.IsNullOrEmpty(id)) {
            employee = await unitOfWork.EmployeeRepository.GetEmployeeByIdAsync(id);
        }
    
        return PartialView("Modal", employee);
    }
    
    public async Task<ActionResult> Gets() {
        IEnumerable<Employee> employees = await unitOfWork.EmployeeRepository.GetEmployeesAsync();
    
        // jsonの循環参照回避のためListに手動変換
        IList<Employee> lists = new List<Employee>();
        foreach (Employee r in employees) {
            Employee employee = new Employee() {
                Id = r.Id,
                EmployeeName = r.EmployeeName,
                RowVersion = r.RowVersion,
                EmployeeDepartments = (r.EmployeeDepartments != null ? new List<EmployeeDepartment>() : null)
            };
    
            foreach (EmployeeDepartment r2 in r.EmployeeDepartments) {
                EmployeeDepartment employeeDepartment = new EmployeeDepartment() {
                    Id = r2.Id,
                    EmployeeId = r2.EmployeeId,
                    DepartmentId = r2.DepartmentId,
                    RowVersion = r2.RowVersion
                };
    
                employee.EmployeeDepartments.Add(employeeDepartment);
            }
    
            lists.Add(employee);
        }
    
        var json = Json(lists, JsonRequestBehavior.AllowGet);
        json.MaxJsonLength = int.MaxValue;
        return json;            
    }

    ■Repository

    関連プロパティを忘れていたので追加したことと、ご解説頂いていました子のデータのModifiedを追加しました。

    親データ- 子のデータを更新するときはそれぞれModifiedをする必要がある。

    子のデータのModifiedをしないと、Idのある更新データでも新規データになってしまう。


    public class EmployeeRepository : IEmployeeRepository, IDisposable {
        private readonly ApplicationDbContext context;
    
        public EmployeeRepository(ApplicationDbContext context) {
           this.context = context;
        }
    
        public async Task<IEnumerable<Employee>> GetEmployeesAsync() {
            return await context.Employees
                .OrderBy(e => e.Order).ToListAsync();
                // .Include(e => e.EmployeeDepartments)
            }
    
        public async Task UpdateAsync(Employee employee) {
            Employee r = await context.Employees.FindAsync(employee.Id);
            r.EmployeeName = employee.EmployeeName;
            r.RowVersion = employee.RowVersion;
            r.EmployeeDepartments = employee.EmployeeDepartments
            context.Employees.Attach(r);
    
            // Parent data modified
            context.Entry(r).State = EntityState.Modified;
    
            // Children data modified
            foreach (EmployeeDepartment employeeDapartment in r.EmployeeDepartments = employee.EmployeeDepartments) {
                if (employeeDapartment.RowVersion != null) {
                    context.Entry(employeeDapartment).State = EntityState.Modified;
                }
            }
        }
    }


    ■ View (ModalのEmployeeDepartment項目のみ、combox⇔input hidden の間はjavascriptで反映)

    EmployeeDepartments を3個まで指定できるようにしてみました。

    input hiddenのデータがおかしかったのは、初期値が設定されていなかったことが一部原因でしたので設定しました。

    <div class="col-md-9">
        <!-- Select2 & input hidden -->
        @{
            string name = "";
            int rowCount = 3;
    
            int count = 0;
            if (Model.EmployeeDepartments != null) {
                count = Model.EmployeeDepartments.Count();
            }
            
            for (int i = 0; i < rowCount; i++) {
                name = "EmployeeDepartments" + i;
                <div><select id = "@name"></select></div>
            
                if (i < count) {
                    @Html.HiddenFor(model => model.EmployeeDepartments[i].Id, new { @value = Model.EmployeeDepartments[i].Id })
                    @Html.HiddenFor(model => model.EmployeeDepartments[i].EmployeeId, new { @value = Model.EmployeeId })
                    @Html.HiddenFor(model => model.EmployeeDepartments[i].DepartmentId, new { @value = (Model.EmployeeDepartments[i].DepartmentId != null ? Model.EmployeeDepartments[i].DepartmentId : -1) })
                    @Html.ValidationMessageFor(model => model.EmployeeDepartments[i].DepartmentId)
                    @Html.HiddenFor(model => model.EmployeeDepartments[i].StartDate)
                    @Html.HiddenFor(model => model.EmployeeDepartments[i].RowVersion)
                } else {
                    @Html.Hidden("EmployeeDepartments[" + i + "].Id", 0, new { @id = "EmployeeDepartments_" + i + "__Id" })
                    @Html.Hidden("EmployeeDepartments[" + i + "].EmployeeId", null, new { @id = "EmployeeDepartments_" + i + "__EmployeeId" })
                    @Html.Hidden("EmployeeDepartments[" + i + "].DepartmentId", -1, new { @id = "EmployeeDepartments_" + i + "__DepartmentId" })
                    @Html.ValidationMessage("EmployeeDepartments[" + i + "].DepartmentId")
                    @Html.Hidden("EmployeeDepartments[" + i + "].StartDate", DateTime.Now, new { @id = "EmployeeDepartments_" + i + "__StartDate" })
                    @Html.Hidden("EmployeeDepartments[" + i + "].RowVersion", null, new { @id = "EmployeeDepartments_" + i + "__RowVersion" })
                }
            }
        }
    </div>



    • 編集済み yuchan01 2020年3月3日 17:14
    2020年3月3日 17:00
  • 質問者さん的には終わった話のようで、すでにご承知&余計なお世話かもしれませんが、気になる点があるので書いておきます。

    > 私の環境で、jsonで1000~2000レコードのデータを扱うとき、virtualを設定するとレスポンスが悪くなることがありましたので、遅延実行プロパティは大容量のデータを扱いにくいのかもしれませんが。

    やり方によると思います。

    遅延ローディングのためレスポンスが悪くなったということは、その「1000~2000レコードのデータを扱うとき」にナビゲーションプロパティにアクセスせざるを得ず、ナビゲーションプロパティにアクセスするたびに遅延ローディングによって SELECT クエリが投げられ時間がかかるということになったのだろうと思います。N+1 問題と言われているようです。詳しくは以下の記事を見てください。

    N+1問題を回避せよ! LINQから出力されるSQLを見てみよう&遅延ローディングの光と闇
    https://codezine.jp/article/detail/8415

    「1000~2000レコードのデータを扱うとき」にナビゲーションプロパティにアクセスする必要がなければ遅延ローディングによってレスポンスが悪くなるということはないはずです。

    Include については、上の記事の 3 ページ目の「N+1問題の回避」に書いてありますが、Include を使うと JOIN した SELECT クエリを投げるので、遅延ローディングによる N+1 問題は避けることができるということらしいです。

    記事に書いてあるように "遅延ローディングの恩恵は捨てがたいため、パフォーマンスが必要な箇所では遅延ローディングを無効化してIncludeで関連するテーブルを明示的に指定し、特に問題とならない箇所では遅延ローディングにお任せするようなコーディングを実践" するのが良さそうだと自分も思います。(無効にする必要はなくて Include を使うだけで済むと思いますが)


    > DepartmentIdはint?でNULLを許可する必要がありました。EmployeeIdはDepartmentを選択しない時があるので、NULL 許可しました。

    そうすると連鎖削除(ある Employee レコードを削除すると連鎖的に関連する EmployeeDepearment も削除する)ができなくなり、そのための対応が必要になると思います。

    詳しくは下記記事参照。本文を cascade delete で検索してください。

    Relationship Convention
    https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/conventions/built-in?redirectedfrom=MSDN#relationship-convention

    2020年3月4日 1:20