UpdateModel で子エンティティも含めて更新しようとしたらうまくいかなかった
はじめに
Controller の UpdateModel
(または TryUpdateModel
) で子エンティティも含めて更新しようと思ったらうまくいかなかったので、実際にコードを書いて検証してみます。
実行手順
Model
以下のような親子関係を持つエンティティを用意します。
Models/Group.cs
public class Group
{
[Key()]
public int Id { get; set; }
[Required()]
[StringLength(64)]
public string Name { get; set; }
public ICollection<Member> Members { get; set; }
public Group()
{
this.Members = new HashSet<Member>();
}
}
Models/Member.cs
public class Member
{
[Key()]
public int Id { get; set; }
[ForeignKey("Group")]
public int GroupId { get; set; }
public Group Group { get; set; }
[Required()]
[StringLength(64)]
public string Name { get; set; }
}
View
Views/Home/Index.cshtml
先ほど作った Group
と Member
を一括更新できるようにしています。データの表示は knockout.js を使っています。検索 ボタンをクリックしたときに、ID
で Group
エンティティを検索します。更新 ボタンをクリックしたときに、画面の情報を jQuery の ajax メソッドで送信します。MIME を application/json
とすることで Controller から受け取れるようになります。
@model Karamem0.samples.Models.Group
@{
ViewBag.Title = "ホーム";
}
@using (Html.BeginForm()) {
<fieldset>
<table>
<tbody>
<tr>
<td class="editor-label">@Html.LabelFor(model => model.Id)
</td>
<td class="editor-field">
@Html.TextBoxFor(model => model.Id, new Dictionary<string, object>() {
{ "data-bind", "value: id" },
})
<input type="button" value="検索" data-bind="click: search" />
</td>
</tr>
<tr>
<td class="editor-label">@Html.LabelFor(model => model.Name)
</td>
<td class="editor-field">
@Html.TextBoxFor(model => model.Name, new Dictionary<string, object>() {
{ "data-bind", "value: name" },
})
</td>
</tr>
</tbody>
</table>
</fieldset>
<fieldset>
<table>
<thead>
<tr>
<td class="header-label" colspan="2">
<input type="button" value="追加" data-bind="click: add" />
</td>
</tr>
</thead>
<tfoot>
<tr>
<td class="header-label" colspan="2">
<input type="button" value="更新" data-bind="click: update" />
</td>
</tr>
</tfoot>
<tbody data-bind="foreach: members">
<tr>
<td class="editor-label">@Html.LabelFor(model => Model.Members.FirstOrDefault().Name)
</td>
<td class="editor-field">
@Html.HiddenFor(model => Model.Members.FirstOrDefault().Id)
@Html.TextBoxFor(model => Model.Members.FirstOrDefault().Name, new Dictionary<string, object>() {
{ "data-bind", "value: name" },
})
<input type="button" value="削除" data-bind="click: $parent.remove" />
</td>
</tr>
</tbody>
</table>
</fieldset>
}
<script type="text/javascript">
function ViewModel() {
var self = this;
this.id = ko.observable(0);
this.name = ko.observable("");
this.members = ko.observableArray([]);
this.search = function () {
var url = "@Url.Action("Search")";
var param = { Id: self.id() };
var callback = function(data) {
if (data != null) {
self.id(data.Id);
self.name(data.Name);
self.members.removeAll();
$.each(data.Members, function(i, e) {
self.members.push({
id: e.Id,
name: e.Name
});
});
}
};
$.post(url, param, callback, "json");
};
this.update = function() {
var url = "@Url.Action("Update")";
var param = {
Id: self.id(),
Name: self.name(),
Members: Enumerable.From(self.members())
.Select(function(member) { return {
Id: member.id,
Name: member.name
}})
.ToArray()
};
var callback = function(data) {
if (data != null) {
self.id(data.Id);
}
};
$.ajax({
url: url,
type: "POST",
contentType: "application/json",
processData: false,
traditional: true,
success: callback,
data: JSON.stringify(param),
dataType: "json"
});
};
this.add = function() {
self.members.push({
id: 0,
name: null
});
};
this.remove = function(item) {
self.members.remove(item);
};
};
$(function () {
ko.applyBindings(new ViewModel());
});
</script>
表示すると以下のような感じになります。
Controller
Controllers/HomeController.cs
Controller の Update
メソッドでは、データがなければ DbContext
に追加し、あれば UpdateModel
で更新するようにしています。
public class HomeController : Controller
{
[HttpGet()]
public ActionResult Index()
{
return this.View();
}
[HttpPost()]
public ActionResult Search(int id)
{
using (var context = new EntityContext())
{
var model = context.Groups
.Include(x => x.Members)
.SingleOrDefault(x => x.Id == id);
if (model != null) {
model.Members.ToList().ForEach(x => x.Group = null);
}
return this.Json(model);
}
}
[HttpPost()]
public ActionResult Update(Group model)
{
using (var context = new EntityContext())
{
if (this.ModelState.IsValid) {
model = context.Groups
.Include(x => x.Members)
.SingleOrDefault(x => x.Id == model.Id);
if (model == null) {
model = new Group();
this.UpdateModel(model);
context.Groups.Add(model);
} else {
this.UpdateModel(model);
}
context.SaveChanges();
}
}
model.Members.ToList().ForEach(x => x.Group = null);
return this.Json(model);
}
}
実行結果
データを入れて登録してみます。
データが正常に追加されると画面の ID
が更新されます。
しかし、この状態でもう一度更新すると、例外が発生します。
System.InvalidOperationException: 'The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.'
おそらく UpdateModel
のタイミングでコレクション自体が書き換わっているのですが、元のデータに Deleted
のフラグがうまく立たないようです。
おわりに
とりあえずいったん全部 Delete
してから追加するように逃げましたが、オート ナンバーだと ID
も変わってしまうため、何か解決方法があれば教えてほしいです。
Discussion