ASP.NET Core 一覧表示のデータ取得と更新処理を分ける
注文一覧では、注文番号、顧客名、状態など、少数の項目だけを表示することがあります。
一方、注文を更新するときは、明細や現在の状態を確認し、変更できる条件を守る必要があります。表示と更新では、必要なデータや処理が異なります。
今回は、一覧表示用のデータ取得と、業務ルールを守る更新処理を分ける方法を紹介します。
注文一覧を例に、画面へ返すデータの型と取得処理を、注文を更新する処理から分けて考えます。
一覧のために更新用の構造をすべて読む必要はない
更新用のオブジェクトには、明細、判断履歴、通知先など、操作の正しさを確認するための情報を持たせることがあります。
しかし、一覧に必要なのが注文番号、顧客名、合計金額だけなら、その全体を組み立てる必要はありません。
役割を分ける例注文を変更する
注文を取得 → 変更条件を確認 → 状態を変更 → 保存
注文一覧を表示する
一覧用の項目を取得 → 一覧用のデータとして返す一覧向けの処理では、SQLで必要なテーブルを結び付け、表示用の結果を返せます。更新用オブジェクトをすべて組み立てる処理を省き、表示に必要な項目へ絞れます。
一覧に必要な型を作る
次のように、表示に必要な項目だけを型にします。
OrderListItem.csusing System;
public sealed record OrderListItem(
Guid OrderId,
string CustomerName,
decimal TotalAmount,
string Status
);recordは、複数の値をまとめるC#の機能です。この型には承認やキャンセルのメソッドを持たせず、表示用のデータとして扱います。
このように、読み取りに合わせたデータの形を、読み取りモデルと呼ぶことがあります。難しく考えず、「その画面で必要な項目をまとめた型」から始められます。
読み取りの入口も用途を表す名前にする
IOrderListReader.csusing System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public interface IOrderListReader
{
Task<IReadOnlyList<OrderListItem>> FindAsync(
Guid organizationId,
int pageSize,
CancellationToken cancellationToken);
}Readerはここでは、表示用のデータを読む処理を表します。このinterfaceだけではデータは取得されないため、実際にはデータベースに接続するクラスを実装します。
更新用の取得・保存処理と名前を分けると、この処理が「一覧を表示するための取得」なのか、「更新する注文を読み込む取得」なのかを判別しやすくなります。
読み取り用のinterfaceを利用する側に置き、データベースに接続する側で実装すると、画面向けの処理にSQLの組み立てまで持たせずに済みます。
必要な列と対象範囲を指定する
SQLの考え方は次のようになります。これはテーブル名を簡略化した例で、記事だけで実行できるテーブル定義や接続コードは含めていません。
一覧向けSQLの例SELECT
o.order_id AS OrderId,
c.display_name AS CustomerName,
o.total_amount AS TotalAmount,
o.status AS Status
FROM orders o
INNER JOIN customers c
ON c.customer_id = o.customer_id
AND c.organization_id = o.organization_id
WHERE o.organization_id = @OrganizationId
ORDER BY o.created_at DESC, o.order_id DESC
LIMIT @PageSize;@OrganizationIdと@PageSizeは、利用するデータアクセス処理から渡すパラメーターです。値を文字列としてSQLへ直接埋め込まない想定です。
この例は先頭の指定件数だけを取得します。次のページを取得したい場合は、ページ番号や取得位置の条件を追加します。
取得件数には上限を設けます。また、ログイン中の利用者がその組織のデータを読めるかを確認したうえで、許可された組織IDを渡します。画面から送られたIDをそのまま信用する設計にはしません。
組織だけでなく利用者ごとの制限があるなら、読み取り処理へ利用者IDも渡すなどして取得範囲を絞ります。読み取り専用でも、権限の確認は必要です。
一覧用の計算結果を更新の許可に使い回さない
一覧に「キャンセルできるか」という項目を追加すると、画面でボタンを出し分けやすくなります。
ただし、一覧を開いてからボタンを押すまでに、注文が発送済みへ変わる場合があります。取得時にはキャンセルできても、実行時にはできないかもしれません。
一覧のCanCancelなどは、そのデータを取得した時点の表示用情報です。
実際にキャンセルするときは、現在の状態と権限をサーバー側で改めて確認します。
読み取りと更新を分けることで、この違いをコード上でも明確にできます。ただし、同じ業務ルールをSQLとC#へ別々に書く場合は、内容がずれないか確認する必要があります。
同じデータベースのまま始められる
読み取りと更新を分けるために、最初からデータベースを2つ用意する必要はありません。
同じテーブルを使いながら、読み取り用の型と取得処理だけを分ける構成にできます。読み取りと書き込みの責務を分ける考え方はCQRSと呼ばれ、同じデータストアを使う構成もあります。Microsoft LearnのCQRSの説明で紹介されています。
この記事では、同じデータベースからSQLで画面用のデータを取得する範囲に絞っています。
別の保存先へコピーする方式に進むと、更新直後に一覧へ反映されない場合の扱いなど、新しい課題が増えます。まずは、同じデータベースで役割を分けるだけで足りるかを考えます。
分けることで増える保守もある
| 変更 | 主に確認する場所 |
|---|---|
| 一覧へ顧客名を追加する | 一覧用の型、SQL、画面 |
| 注文をキャンセルできる条件を変える | 更新処理と、一覧に表示する操作可否 |
| データベースの列名を変える | 更新用の保存処理と、読み取り用SQLの両方 |
読み取りを分けると表示に合わせやすくなる一方、型やSQLは増えます。テーブル変更時に確認する場所がなくなるわけではありません。
単純な設定画面のように、表示する項目と更新する項目がほぼ同じなら、通常の取得処理とDTOだけで十分な場合もあります。
また、列を減らせば必ず高速になるとは限りません。大量のデータを扱う一覧では、検索条件、並び順、インデックス、実行計画も確認します。
画面の都合が更新用クラスへ増えてきたら検討する
一覧に顧客名や件数を表示するたびに、更新用クラスへ表示専用の項目を追加しているなら、読み取り用の型を分ける目安になります。
確認するときは、表示項目が取得できることに加え、他の組織のデータが混ざらないこと、取得件数が制限されること、状態が変わった後の更新が拒否されることも見ます。
表示用の取得を独立させると、一覧に必要なデータを考える作業と、更新時のルールを守る作業を分けて扱えます。画面を増やすたびに更新用の構造が複雑になっている場合に、取り入れてみてください。