web-dev-qa-db-ja.com

APIリソースでネストされたオブジェクトを使用する方法

私は最初のWebアプリケーションを構築しています。RESTfullWeb APIを介してサーバー側にリンクしています(クライアント側のAngular、サーバー側のASP.Net CoreとEF Core、ドメインモデルとの間でAPIリソースをマップするAutomapper)。
アプリケーションには2つのメインモデル(以下ではDeliveryOrderとして簡略化)があり、それぞれに独自のAPIコントローラーとエンドポイント(api/deliveriesapi/orders)。 1. Orderが存在する前にDeliveryを作成できる、2。DeliveryOrderの配列を保持できる3. OrderDeliveryに含まれた後で修正できます。

私のドメインモデル(APIリソースはほぼ同一):

public class Delivery
{
    public int Id { get; set; }
    public Customer Customer { get; set; }
    public Address Address { get; set; }
    public DateTime EstimatedDelivery { get; set; }
    public DeliveryOrder[] Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public Product Product { get; set; }
    public int Quantity { get; set; }
}

public class DeliveryOrder
{
    public int DeliveryId { get; set; }
    public Delivery Delivery { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
}

クライアントユーザーインターフェイスには、orderを作成/修正するビュー(Orderのインスタンス)と、deliveryを作成/修正するビュー(Deliveryのインスタンス)があります。 ordersを作成/修正する機能が含まれています。ビジネスルールの結果として、deliveryビューで、orderを個別に直接更新することも、delivery.ordersを更新することもできました。適切な実装を実現するのに苦労しています。

クライアント側でのdeliveryオブジェクトの例。最初のorderは既存の注文で、2番目のorderは新しい注文です。

{
    "id": 5,
    "customer": {
        "id": 15,
        "name": "Jane"
    },
    "address": {
        "street": "Something Ave",
        "number": "145"
    },
    "estimatedDelivery": "2019-05-21T13:28:06.419",
    "orders": [
        {
            "orderId": 6,
            "order": {
                "id": 6,
                "product": {
                    "id": 112,
                    "categoryId": 36
                },
                "quantity": 4
            }
        },
        {
            "order": {
                "product": {
                    "id": 115,
                    "categoryId": 36
                },
                "quantity": 6
            }
        }
    ]
}

私が検討したソリューション:

最初deliveryのビュー(角度コンポーネント)では、「保存」アクションに2つのステップがあります。この場合、注文にidがあるかどうか、およびputが修正されたorderまたはpost新しいorderがあるかどうかを確認する必要があります私のapi/ordersに。この場合、私のDelivery AP​​I(Automapperプロファイルを使用)はDelivery.Orders.Orderを無視し、Delivery.Orders.OrderIdの値のみを比較します。クライアントのアクションは次のようになります。

saveAction() {
    delivery.orders.forEach(o => {
        if (o.order.id) { this.httpClient.put('api/orders/' + o.order.id, o.order).subscribe( ... ); } 
        else { this.httpClient.post('api/orders/', o.order).subscribe( ... ) });
    this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe( ... );
}

他のトピックを読んで私が理解しているのは 非推奨 forループで複数のHTTPリクエストを作成してサブスクリプションの数を管理することです。おそらく、サブスクリプションがforkJoinで結合されている場合、これはそれほど問題ではありませんか?

。保存アクションの前に特定のorderが存在するかどうかを判断する代わりに、delivery.orders全体をAPIに送信し、サーバーサイドにOrderのインスタンスを作成または修正する必要があるかどうかを判断させます。その場合、配列全体をpatchします ここで説明します 。繰り返しますが、私のDelivery AP​​I(Automapperプロファイルを使用)はDelivery.Orders.Orderを無視し、Delivery.Orders.OrderIdの値のみを比較します。保存アクションは次のようになります。

saveAction() {
    this.httpClient.patch('api/orders/' delivery.orders).subscribe(data => {
            delivery.orders = data;
            this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe( ... )
        });
}

これにはサーバーサイドでの作業が必要になるため、サーバーサイドですべてを管理する方がよいと考えました。これは、検討している3番目のソリューションです。

サード:サーバー側でDelivery.Ordersを完全に消費し、カスタムマッピングを実行して、Delivery.Orders.Orderのメンバーが正しく修正されるようにしますまたは作成されました。これは、DeliveryコントローラーまたはMappingProfileのいずれかで行うことができます。その後、データは単純に以下を介して送信されます。

saveAction() {
    this.httpClient.put('api/deliveries' + delivery.id, delivery).subscribe(data => delivery = data);
}

質問:私はこれをやったことがなく、自分の解決策に自信がありません。私の3番目のソリューションは最も理にかなっていますか、それともDelivery AP​​IエンドポイントにネストされたOrderの作成/修正の責任を持たせるべきではありませんか?ベストプラクティスと見なされている、または少なくとも私が思っていたよりも一般的に使用されているデザインパターンはありますか?

1
Superman.Lopez

私は、物理的な荷物であるDeliveryを、住所、連絡先、時間などであるDeliveryInstructionsとは別に扱います...

注文は配達に依存しますか?

あなたの場合、OrderDeliveryとは独立して作成できるようです。

Orderには独自のAPIエンドポイントが必要です。

配送は実際に注文または注文された製品を配送していますか?

これはあなたが尋ねたことではないことを知っていますが、そこには意味論が潜んでいます。どちらか:

  • 配送中の注文は変更しないでください(いったいどのようにしてぬいぐるみを追加するか、またはトラックに乗っているときに靴下を取り外しますか?)
  • 個別に注文された製品は配達で送られ、注文は未配達のアイテムを削除するか、新しいアイテムを注文に追加することができます(後続の個別配達の場合)。

最初のケースでは、DeliveryOrderオブジェクトを保持せず、それらへの参照(識別子やURLなど)を保持します。

2番目のケースでは、DeliveryOrderの一連のラインアイテムを指します。これらのラインアイテムは、注文/配信も参照します。

トランザクション性

サーバー上の一連のオブジェクトをトランザクションで更新するのは誰ですか?

基本的に一度に1つのURLで1つの更新を実行するワークフローを作成できます。各変換では、システムが一貫した状態に保たれる必要があります(目的の最終状態でなくても)。

このワークフローはクライアントから実行できます。

  • クライアントがタイムアウトするリスクを負う
  • 注文がUIに表示されていると表示されても注文が処理されない場合、顧客は少し動揺することがあります。

このワークフローを/requestなどのサーバーに渡すことができます。

  • ワークフローの進行状況を監視するためのAPIを提供する必要があります。
  • 進行状況を観察するには、いくつかのUIビューを提供する必要があります。
  • ただし、受信するとクライアントは切断され、サービスは引き続き続行されます。

もちろん、それをサーバーに直接渡す場合は、単純にインラインで要求を実行できます。

  • プログレスUIを提供する必要はありません。
  • 更新は、単一のSQL/NoSQLトランザクションで実行できます。
  • ワークフローは、Webサーバーのスレッド/クライアント接続を占有するため、あまり長くかかることはありません。

自分に最適なものを選んでください。

0
Kain0_0

結局、私は3番目のソリューションを採用することにしました。これは、Automapperを使用することで大幅に促進され、AutoMapper.EquivalencyExpressionを使用することでさらに促進されました。複雑な入れ子の関係の場合、グラフ全体をサーバーに投稿/送信する前に、クライアント側で参照を作成するしかないと感じました。

クライアントアプリケーションでは、これらにGUID(またはUUID)を使用することにしました。サーバー側では、これらを Alternate Keys にして、このプロパティを外部キーとして信頼できるようにしました。そうすることで、さまざまなシナリオ(postput、および既存のネストされたオブジェクトとネストされたオブジェクトの新しいインスタンス)を処理するために、アプリケーションにロジックを記述する必要がなくなりました。 抽象化の場合、パターンは不必要に複雑です。しかし、実際の実装では、Delivery内のネストされたオブジェクト間の相互参照が可能になりましたおよびOrderこれらがサーバー側で作成される前でも。私のアプローチの例を以下に含めました:

プログラマー関連ドメインモデル

public class Programmer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Keyboard> Keyboard { get; set; }
    public List<Finger> Fingers { get; set; }
}

public class Finger
{
    public int Id { get; set; }
    public Guid Guid { get; set; }
    public Hand Hand { get; set; }
    public int Sequence { get; set; }
    public List<KeyStroke> KeyStrokes { get; set; }

}

public enum Hand
{
    Left = 1,
    Right = 2
}

キーボード関連のドメインモデル。ここで、KeyStrokeは、KeyFinger間の結合テーブルとして使用されるクラスです。

public class Keyboard
{
    public int Id { get; set; }
    public int SerialNumber { get; set; }
    public Programmer Programmer { get; set; }
    public List<Key> Keys { get; set; }
}

public class Key
{
    public int Id { get; set; }
    public string KeyValue { get; set; }
    public List<KeyStroke> KeyStrokes { get; set; }
}

public class KeyStroke
{
    public int KeyId { get; set; }
    public Key Key { get; set; }
    public Guid FingerGuid { get; set; }
    public Finger Finger { get; set; }
}

DbContextが代替キーとしてマークされているGuidプロファイル:

modelBuilder.Entity<Programmer>();
modelBuilder.Entity<Finger>()
    .HasAlternateKey(f => f.Guid);
modelBuilder.Entity<KeyStroke>()
    .HasKey(ks => new {ks.FingerGuid, ks.KeyId});

modelBuilder.Entity<KeyStroke>()
    .HasOne(ks => ks.Finger)
    .WithMany(f => f.KeyStrokes)
    .HasForeignKey(ks => ks.FingerGuid)
    .HasPrincipalKey(f => f.Guid);

私のリソース(またはViewModel):

public class ProgrammerResource
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<KeyboardResource> Keyboard { get; set; }
    public List<FingerResource> Fingers { get; set; }
}

public class FingerResource
{
    public int Id { get; set; }
    public Guid Guid { get; set; }

    public Hand Hand { get; set; }
    public int Sequence { get; set; }
}

public class KeyboardResource
{
    public int Id { get; set; }
    public int SerialNumber { get; set; }
    public List<KeyResource> Keys { get; set; }
}

public class KeyResource
{
    public int Id { get; set; }
    public string KeyValue { get; set; }
    public List<KeyStrokeResource> KeyStrokes { get; set; }
}

public class KeyStrokeResource
{
    public int KeyId { get; set; }
    public Guid FingerGuid { get; set; }
}

この構造では、Keyboardを作成して独自のコントローラーにポストするか、Programmer関連グラフに含めてProgrammerコントローラーにポストすることができます。

Programmerコントローラ:

[HttpPost("programmer")]
public async Task<IActionResult> CreateProgrammer([FromBody] ProgrammerResource programmerResource)
{
    var programmer = mapper.Map<ProgrammerResource, Programmer>(programmerResource);

    context.Programmers.Add(programmer);
    await context.SaveChangesAsync();

    var savedProgrammer = await context.Programmers
        .Include(p => p.Fingers)
        .Include(p => p.Keyboard).ThenInclude(kb => kb.Keys).ThenInclude(k => k.KeyStrokes)
        .SingleOrDefaultAsync(v => v.Id == programmer.Id);

    var result = mapper.Map<Programmer, ProgrammerResource>(savedProgrammer);

    return Ok(result);
}

Automapperプロファイルの場合:

CreateMap<Keyboard, KeyboardResource>();
CreateMap<KeyboardResource, Keyboard>()
    .EqualityComparison((kr, k) => kr.Id == k.Id);

CreateMap<KeyResource, Key>()
    .EqualityComparison((kr, k) => kr.Id == k.Id);
CreateMap<FingerResource, Finger>()
    .EqualityComparison((fr, f) => fr.Id == f.Id);
CreateMap<KeyStrokeResource, KeyStroke>()
    .ForMember(k => k.Finger, opt => opt.Ignore())
    .ForMember(k => k.Key, opt => opt.Ignore());

この設定により、グラフ全体を投稿し、ProgrammerKeyboardの間の関係、およびFingerKeyの間の関係を作成することができますただし、投稿時にProgrammerおよびKeyboardのインスタンスはまだ存在しません。

{
    "name": "John Doe",
    "keyboard": [
        {
            "serialNumber": 6584654,
            "keys": [
                {
                    "keyValue": "A",
                    "keyStrokes": [
                        {
                            "fingerGuid": "f911ac90-48fc-4eae-ae3d-b727f005eb53"
                        },
                        {
                            "fingerGuid": "52ea936a-3922-41e6-95f2-1408fa19af74"
                        }
                    ]
                },
                {
                    "keyValue": "B",
                    "keyStrokes": []
                }
            ]
        }
    ],
    "fingers": [
        {
            "guid": "f911ac90-48fc-4eae-ae3d-b727f005eb53",
            "hand": 1,
            "sequence": 1
        },
        {
            "guid": "52ea936a-3922-41e6-95f2-1408fa19af74",
            "hand": 1,
            "sequence": 2
        }
    ]
}

上記をポストすると、関係全体が正しく作成されます(およびPutによる変更は期待どおりに実行されます)。

{
  "id": 4,
  "name": "John Doe",
  "keyboard": [
    {
      "id": 3,
      "serialNumber": 6584654,
      "keys": [
        {
          "id": 5,
          "keyValue": "A",
          "keyStrokes": [
            {
              "keyId": 5,
              "fingerGuid": "f911ac90-48fc-4eae-ae3d-b727f005eb53"
            },
            {
              "keyId": 5,
              "fingerGuid": "52ea936a-3922-41e6-95f2-1408fa19af74"
            }
          ]
        },
        {
          "id": 6,
          "keyValue": "B",
          "keyStrokes": []
        }
      ]
    }
  ],
  "fingers": [
    {
      "id": 6,
      "guid": "f911ac90-48fc-4eae-ae3d-b727f005eb53",
      "hand": 1,
      "sequence": 1
    },
    {
      "id": 7,
      "guid": "52ea936a-3922-41e6-95f2-1408fa19af74",
      "hand": 1,
      "sequence": 2
    }
  ]
}

クライアントアプリケーションでのGuidの作成には、Steve Fentonによる非常に単純な GUID作成クラス を使用しました。

0
Superman.Lopez