今回は前回記事や4の記事でCRUDを作成した所からの解説です。CRUDまでを参照したい方は以下の記事を参照してください。4は通常実装向け/5はスキャフォールディング向けです。
目次
作成・更新されるデータには作成日時・更新日時を入れる
これは実務でもそうなのですが通常テーブルのデータには対象のデータは「いつ登録されたものなのか?」また、更新があるなら「いつ更新されたものか?」を保存します。
- アカウントだとしたら何時からのユーザなのか、現在もアクティブユーザなのかの指標になる。
- 該当データで問題が起こった場合、登録/更新日付があればバージョン管理された登録/更新当時のソースと照らし合わせて問題を解決することができる。
等、作成日時/更新日時を入れることは統計を取る上でも問題解決の意味でも重要になる貴重な情報ですので覚えておいた方がいいでしょう。
作成日時・更新日時は自動で更新するようにする。
作成日時・更新日時は基本「登録/日時」の現在時刻が入るので手動で更新するものではありません。スキャフォールディングで作った場合は画面上から消しましょう。
作成時の作成日時・更新日時
作成時の作成日時・更新日時を更新するのは難しくありません。
画像は修正前のものですが以下のように修正します。
// POST: ArticleEntities/Create // To protect from overposting attacks, 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("Id,Title,Name,Description")] ArticleEntity articleEntity) { if (ModelState.IsValid) { articleEntity.Created = DateTime.Now; articleEntity.Updated = DateTime.Now; _context.Add(articleEntity); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(articleEntity); }
修正後の動作はこちら
日時を投稿するフォームを削除後に上記の文章を投稿するとします。
作成すると投稿日・更新日が共に更新されましたね^^ ここまでならそんなに難しくありません。問題は更新です。
更新時の作成日時・更新日時
更新時に気をつけないといけない事は、既に作られた文章を更新するので作成日時は更新せず、更新日と入力した言葉だけを更新する必要があります。スキャフォールディングで作成されたソースを元に作成時の処理と同じように変更すると以下のようになると思います。
失敗パターン1:作成日時がクリアされてしまう
上記のソースで更新をしようとするとどうなるでしょうか?修正画面を開いて本文を変更してみます。
作成と同じ要領で編集データを保存しようとするとあら不思議。
本文は更新されて更新日も更新されましたが投稿日データがおかしくなってしまいました。
Editの処理は画面(View)から渡されたデータのみを参照しているため、入力しておらず、BindもされていないCreatedは初期値が入ってしまっているのです。
Bind処理は過多ポスティング攻撃を防ぐためにCreateを入れられないものです。今回はここでは過多ポスティングについては触れません。他のサイトの解説をご覧頂ければと思います。
要は渡されたデータには作成日時は入っていないのですから、現在入ってる作成日時データを持ってくる必要があります。現在のデータは _contextに入っていますのでリードしたテーブルの作成日時を更新されたデータに入れて更新しようとします。
失敗パターン2:An unhandled exception occurred while processing the request.
上記のソースに修正して同じように更新しようとするとこのエラーが出てしまいます。
InvalidOperationException: The instance of entity type 'ArticleEntity' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
翻訳すると以下のようになります。
{‘Id’}と同じキー値を持つ別のインスタンスがすでに追跡されているため、エンティティタイプ ‘ArticleEntity’のインスタンスを追跡できません。既存のエンティティをアタッチするときは、特定のキー値を持つエンティティインスタンスが1つだけアタッチされていることを確認してください。 ‘DbContextOptionsBuilder.EnableSensitiveDataLogging’を使用して、競合するキー値を確認することを検討してください。
日本語訳されたデータで検索するとMicrosoftの公式サイトがヒットしますのでご覧ください。
とはいえ、この例はすごいわかりにくいのですが要約すると上記で設定した「nowDataは読み込んだ時点でアタッチされた状態」になってしまっており、それと関係ないarticleEntityを使用したUpdateでは複数のアタッチ行為とみなされて、エラーになってしまっているのです。
Contextから読んだデータで更新するのが必要
という事で上記問題の解決策は下記のようになります。
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Name,Description")] ArticleEntity articleEntity) { if (id != articleEntity.Id) { return NotFound(); } if (ModelState.IsValid) { try { var nowData = _context.ArticleEntity.Find(id); nowData.Title = articleEntity.Title; nowData.Name = articleEntity.Name; nowData.Description = articleEntity.Description; nowData.Updated = DateTime.Now; _context.Update(nowData); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ArticleEntityExists(articleEntity.Id)) { return NotFound(); } else { throw; } } return RedirectToAction(nameof(Index)); } return View(articleEntity); }
コードが多くなってしまいますが、Contextから読んだデータを使って更新するのが正しいです。私もMVCは慣れていないのでやってしまいましたが、初心者が陥りやすいミスなので気を付けましょう。
修正後の動作確認
先ほど作成日時を壊してしまったデータは削除して新しく作り直しました。
この状態でタイトル部分を修正して実行してみます。すると。
いかがですか?今度はちゃんと投稿日が変わらず、更新日だけを更新することができました。
まとめ
今回の例のように作成と更新で同じようにできそうでもルールに縛れてうまくいかなったりします。今回はASP.NETでしたが、LaravelやRuby on Railsも同じMVCなのでこういった細かいルールに縛られることが多いでしょう。
そういったトライアンドエラーを楽しいと思うか、辛いと思うかがエンジニア適性だと思います。それではー