zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NET Core 一覧表示のデータ取得と更新処理を分ける

注文一覧では、注文番号、顧客名、状態など、少数の項目だけを表示することがあります。

一方、注文を更新するときは、明細や現在の状態を確認し、変更できる条件を守る必要があります。表示と更新では、必要なデータや処理が異なります。

今回は、一覧表示用のデータ取得と、業務ルールを守る更新処理を分ける方法を紹介します。

注文一覧を例に、画面へ返すデータの型と取得処理を、注文を更新する処理から分けて考えます。

一覧のために更新用の構造をすべて読む必要はない

更新用のオブジェクトには、明細、判断履歴、通知先など、操作の正しさを確認するための情報を持たせることがあります。

しかし、一覧に必要なのが注文番号、顧客名、合計金額だけなら、その全体を組み立てる必要はありません。

役割を分ける例
注文を変更する
    注文を取得 → 変更条件を確認 → 状態を変更 → 保存

注文一覧を表示する
    一覧用の項目を取得 → 一覧用のデータとして返す

一覧向けの処理では、SQLで必要なテーブルを結び付け、表示用の結果を返せます。更新用オブジェクトをすべて組み立てる処理を省き、表示に必要な項目へ絞れます。

一覧に必要な型を作る

次のように、表示に必要な項目だけを型にします。

OrderListItem.cs
using System;

public sealed record OrderListItem(
    Guid OrderId,
    string CustomerName,
    decimal TotalAmount,
    string Status
);

recordは、複数の値をまとめるC#の機能です。この型には承認やキャンセルのメソッドを持たせず、表示用のデータとして扱います。

このように、読み取りに合わせたデータの形を、読み取りモデルと呼ぶことがあります。難しく考えず、「その画面で必要な項目をまとめた型」から始められます。

recordでデータ受け渡し用の型を作る方法

読み取りの入口も用途を表す名前にする

IOrderListReader.cs
using 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だけで十分な場合もあります。

また、列を減らせば必ず高速になるとは限りません。大量のデータを扱う一覧では、検索条件、並び順、インデックス、実行計画も確認します。

画面の都合が更新用クラスへ増えてきたら検討する

一覧に顧客名や件数を表示するたびに、更新用クラスへ表示専用の項目を追加しているなら、読み取り用の型を分ける目安になります。

確認するときは、表示項目が取得できることに加え、他の組織のデータが混ざらないこと、取得件数が制限されること、状態が変わった後の更新が拒否されることも見ます。

表示用の取得を独立させると、一覧に必要なデータを考える作業と、更新時のルールを守る作業を分けて扱えます。画面を増やすたびに更新用の構造が複雑になっている場合に、取り入れてみてください。


関連記事