私は最初のWebアプリケーションを構築しています。RESTfullWeb APIを介してサーバー側にリンクしています(クライアント側のAngular、サーバー側のASP.Net CoreとEF Core、ドメインモデルとの間でAPIリソースをマップするAutomapper)。
アプリケーションには2つのメインモデル(以下ではDelivery
とOrder
として簡略化)があり、それぞれに独自のAPIコントローラーとエンドポイント(api/deliveries
とapi/orders
)。 1. Order
が存在する前にDelivery
を作成できる、2。Delivery
がOrder
の配列を保持できる3. Order
はDelivery
に含まれた後で修正できます。
私のドメインモデル(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
API(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
API(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
APIエンドポイントにネストされたOrder
の作成/修正の責任を持たせるべきではありませんか?ベストプラクティスと見なされている、または少なくとも私が思っていたよりも一般的に使用されているデザインパターンはありますか?
私は、物理的な荷物であるDelivery
を、住所、連絡先、時間などであるDeliveryInstructions
とは別に扱います...
注文は配達に依存しますか?
あなたの場合、Order
はDelivery
とは独立して作成できるようです。
Order
には独自のAPIエンドポイントが必要です。
配送は実際に注文または注文された製品を配送していますか?
これはあなたが尋ねたことではないことを知っていますが、そこには意味論が潜んでいます。どちらか:
最初のケースでは、Delivery
はOrder
オブジェクトを保持せず、それらへの参照(識別子やURLなど)を保持します。
2番目のケースでは、Delivery
はOrder
の一連のラインアイテムを指します。これらのラインアイテムは、注文/配信も参照します。
トランザクション性
サーバー上の一連のオブジェクトをトランザクションで更新するのは誰ですか?
基本的に一度に1つのURLで1つの更新を実行するワークフローを作成できます。各変換では、システムが一貫した状態に保たれる必要があります(目的の最終状態でなくても)。
このワークフローはクライアントから実行できます。
このワークフローを/request
などのサーバーに渡すことができます。
もちろん、それをサーバーに直接渡す場合は、単純にインラインで要求を実行できます。
自分に最適なものを選んでください。
結局、私は3番目のソリューションを採用することにしました。これは、Automapper
を使用することで大幅に促進され、AutoMapper.EquivalencyExpression
を使用することでさらに促進されました。複雑な入れ子の関係の場合、グラフ全体をサーバーに投稿/送信する前に、クライアント側で参照を作成するしかないと感じました。
クライアントアプリケーションでは、これらにGUID(またはUUID)を使用することにしました。サーバー側では、これらを Alternate Keys にして、このプロパティを外部キーとして信頼できるようにしました。そうすることで、さまざまなシナリオ(post
とput
、および既存のネストされたオブジェクトとネストされたオブジェクトの新しいインスタンス)を処理するために、アプリケーションにロジックを記述する必要がなくなりました。 抽象化の場合、パターンは不必要に複雑です。しかし、実際の実装では、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
は、Key
とFinger
間の結合テーブルとして使用されるクラスです。
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());
この設定により、グラフ全体を投稿し、Programmer
とKeyboard
の間の関係、およびFinger
とKey
の間の関係を作成することができますただし、投稿時に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作成クラス を使用しました。