合成階層
WCF RIA サービス では、格納オブジェクト (全体または親) が、含まれるオブジェクト (部分または子孫) の作成と有効期間を制御する、"持つ" リレーションシップによって関連付けられているクラスを含む合成階層に属しているデータ クラスのアプリケーション ロジックを作成できます。たとえば、SalesOrderHeader
エンティティは、注文の一部としてのみ存在する注文に関する詳細として、SalesOrderDetail
エンティティを持ちます。明確に説明すると、クラスの構成は、クラスのサブタイプとは区別されます。クラスのサブタイプでは、より一般的な種類 (乗り物) に詳細を追加することで、特定の種類 (自動車) を作成して構成します。これにより、継承階層が生まれます。継承階層では、詳細な (派生) クラスは引き続き一般的な (基本) 型として扱われますが、これは "自動車" は "乗り物" であることに変わりはないためです。
関連するクラス間の複合リレーションシップを定義すると、エンティティでのデータ変更操作を別々のエンティティとして実行するのではなく、1 つの単位として実行できるようになります。こうすると、ロジックを各エンティティに適用するように分割してデータ操作中の分割ロジックの調整を図ることなく、階層全体に対してアプリケーション ロジックを作成することができるため、中間層のロジックが簡略化されます。
合成階層について
エンティティの階層では、1 つのエンティティが親エンティティと呼ばれ、その他の関連エンティティが子孫エンティティと呼ばれます。親エンティティはデータを表すクラスで、子孫エンティティ内のデータの単一のルートです。たとえば、SalesOrderHeader
エンティティは親エンティティ、SalesOrderDetail
は子孫エンティティです。SalesOrderHeader
エンティティ内の 1 つのレコードを、SalesOrderDetail
エンティティ内の複数のレコードにリンクできます。
階層リレーションシップに含まれるデータ クラスには、一般的に、次の特性があります。
エンティティ間のリレーションシップは、子孫エンティティが 1 つの親エンティティに接続されているツリーとして表すことができます。子孫エンティティは任意のレベル数まで拡張できます。
子孫エンティティの有効期間は、親エンティティの有効期間内に含まれます。
子孫エンティティは、親エンティティのコンテキストの外側で有効な ID を持っていません。
エンティティでのデータ操作では、エンティティを 1 つの単位として扱う必要があります。たとえば、子孫エンティティでレコードの追加、削除、または更新を行うには、親エンティティでも対応する変更が必要になります。
複合リレーションシップの定義
エンティティ間の複合リレーションシップを定義するには、エンティティ間の関連付けを表すプロパティに CompositionAttribute 属性を適用します。メタデータ クラスを使用して SalesOrderHeader
と SalesOrderDetail
の複合リレーションシップを定義する方法を次の例に示します。CompositionAttribute 属性は System.ComponentModel.DataAnnotations 名前空間にあります。次のコードに示すように、using ステートメントまたは Imports ステートメントを使用して属性を適用することで、その名前空間を参照する必要があります。
<MetadataTypeAttribute(GetType(SalesOrderHeader.SalesOrderHeaderMetadata))> _
Partial Public Class SalesOrderHeader
Friend NotInheritable Class SalesOrderHeaderMetadata
Private Sub New()
MyBase.New
End Sub
<Include()> _
<Composition()> _
Public SalesOrderDetails As EntityCollection(Of SalesOrderDetail)
End Class
End Class
[MetadataTypeAttribute(typeof(SalesOrderHeader.SalesOrderHeaderMetadata))]
public partial class SalesOrderHeader
{
internal sealed class SalesOrderHeaderMetadata
{
private SalesOrderHeaderMetadata()
{
}
[Include]
[Composition]
public EntitySet<SalesOrderDetail> SalesOrderDetails;
}
}
CompositionAttribute 属性をプロパティに適用しても、子孫エンティティのデータが自動的に親エンティティで取得されることはありません。子孫エンティティをクエリ結果に含めるには、子孫エンティティを表すプロパティに IncludeAttribute 属性を適用し、クエリ メソッドに子孫エンティティを含める必要があります。次のセクションの最後にある例では、クエリ メソッドに子孫エンティティを含める方法を示しています。
合成階層によるドメイン サービス操作
合成階層を定義するときに、親エンティティおよび子孫エンティティとの対話方法を変更する必要があります。ドメイン サービスで使用するロジックでは、エンティティ間のリンクを考慮する必要があります。通常、階層のロジックは、親エンティティのドメイン サービス メソッドを使用して定義します。親エンティティのドメイン サービス操作では、親エンティティに対する変更、および子孫エンティティに対する変更を処理します。
複合リレーションシップを持つエンティティのドメイン サービス操作には、次の規則が適用されます。
親エンティティまたは子孫エンティティのクエリ メソッドが許可されます。ただし、親エンティティのコンテキストで子孫エンティティを取得することをお勧めします。親エンティティなしで読み込まれた子孫エンティティを変更すると、例外がスローされます。
データ変更操作は子孫エンティティに追加できますが、子孫エンティティで許可されている操作は、親エンティティで許可されている操作の影響を受けます。
親エンティティで更新が許可されている場合、子孫エンティティでは更新、挿入、および削除が許可されます。
親エンティティに名前付き更新メソッドがある場合、すべての子孫で更新が有効になっている必要があります。
親エンティティで挿入または削除が許可されている場合、子孫エンティティでは対応する操作が再帰的に許可されます。
クライアント プロジェクト内では、複合リレーションシップを持つエンティティの使用に次の規則が適用されます。
子孫エンティティに変更が含まれる場合、変更の通知が親エンティティまで伝達されます。親エンティティの HasChanges プロパティは true に設定されます。
親エンティティを変更すると、その子孫エンティティすべて (変更されていない子孫を含む) が変更セットに含まれます。
ドメイン コンテキスト内のパブリック EntitySet は、子孫エンティティのクライアントでは生成されません。親エンティティを介して子孫エンティティにアクセスする必要があります。
同じレベルに複数の祖先を持つ子孫エンティティは定義できますが、1 つの祖先のみのコンテキスト内に読み込まれるようにする必要があります。
データ変更操作は、次の規則に従って実行されます。
最初に親エンティティで更新、挿入、または削除操作が実行された後、子孫エンティティでデータ変更操作が再帰的に実行されます。
必要なデータ操作が子孫エンティティにない場合は、再帰的な実行が中止されます。
親エンティティを更新しても、子孫のデータ操作の実行順序は指定されません。
次の例では、SalesOrderHeader
エンティティを照会、更新、および削除するためのメソッドを示します。メソッドには、子孫エンティティ内の変更を処理するためのロジックが含まれています。
<EnableClientAccess()> _
Public Class OrderDomainService
Inherits LinqToEntitiesDomainService(Of AdventureWorksLT_DataEntities)
Public Function GetSalesOrders() As IQueryable(Of SalesOrderHeader)
Return Me.ObjectContext.SalesOrderHeaders.Include("SalesOrderDetails")
End Function
Public Sub UpdateSalesOrder(ByVal currentSalesOrderHeader As SalesOrderHeader)
Dim originalOrder As SalesOrderHeader = Me.ChangeSet.GetOriginal(currentSalesOrderHeader)
If (currentSalesOrderHeader.EntityState = EntityState.Detached) Then
If (IsNothing(originalOrder)) Then
Me.ObjectContext.Attach(currentSalesOrderHeader)
Else
Me.ObjectContext.AttachAsModified(currentSalesOrderHeader, Me.ChangeSet.GetOriginal(currentSalesOrderHeader))
End If
End If
For Each detail As SalesOrderDetail In Me.ChangeSet.GetAssociatedChanges(currentSalesOrderHeader, Function(o) o.SalesOrderDetails)
Dim op As ChangeOperation = Me.ChangeSet.GetChangeOperation(detail)
Select Case op
Case ChangeOperation.Insert
If ((detail.EntityState = EntityState.Added) _
= False) Then
If ((detail.EntityState = EntityState.Detached) _
= False) Then
Me.ObjectContext.ObjectStateManager.ChangeObjectState(detail, EntityState.Added)
Else
Me.ObjectContext.AddToSalesOrderDetails(detail)
End If
End If
Case ChangeOperation.Update
Me.ObjectContext.AttachAsModified(detail, Me.ChangeSet.GetOriginal(detail))
Case ChangeOperation.Delete
If (detail.EntityState = EntityState.Detached) Then
Me.ObjectContext.Attach(detail)
End If
Me.ObjectContext.DeleteObject(detail)
End Select
Next
End Sub
Public Sub DeleteSalesOrder(ByVal salesOrderHeader As SalesOrderHeader)
If (salesOrderHeader.EntityState = EntityState.Detached) Then
Me.ObjectContext.Attach(salesOrderHeader)
End If
Select Case salesOrderHeader.Status
Case 1 ' in process
Me.ObjectContext.DeleteObject(salesOrderHeader)
Case 2, 3, 4 ' approved, backordered, rejected
salesOrderHeader.Status = 6
Case 5 ' shipped
Throw New ValidationException("The order has been shipped and cannot be deleted.")
End Select
End Sub
End Class
[EnableClientAccess()]
public class OrderDomainService : LinqToEntitiesDomainService<AdventureWorksLT_DataEntities>
{
public IQueryable<SalesOrderHeader> GetSalesOrders()
{
return this.ObjectContext.SalesOrderHeaders.Include("SalesOrderDetails");
}
public void UpdateSalesOrder(SalesOrderHeader currentSalesOrderHeader)
{
SalesOrderHeader originalOrder = this.ChangeSet.GetOriginal(currentSalesOrderHeader);
if ((currentSalesOrderHeader.EntityState == EntityState.Detached))
{
if (originalOrder != null)
{
this.ObjectContext.AttachAsModified(currentSalesOrderHeader, this.ChangeSet.GetOriginal(currentSalesOrderHeader));
}
else
{
this.ObjectContext.Attach(currentSalesOrderHeader);
}
}
foreach (SalesOrderDetail detail in this.ChangeSet.GetAssociatedChanges(currentSalesOrderHeader, o => o.SalesOrderDetails))
{
ChangeOperation op = this.ChangeSet.GetChangeOperation(detail);
switch (op)
{
case ChangeOperation.Insert:
if ((detail.EntityState != EntityState.Added))
{
if ((detail.EntityState != EntityState.Detached))
{
this.ObjectContext.ObjectStateManager.ChangeObjectState(detail, EntityState.Added);
}
else
{
this.ObjectContext.AddToSalesOrderDetails(detail);
}
}
break;
case ChangeOperation.Update:
this.ObjectContext.AttachAsModified(detail, this.ChangeSet.GetOriginal(detail));
break;
case ChangeOperation.Delete:
if (detail.EntityState == EntityState.Detached)
{
this.ObjectContext.Attach(detail);
}
this.ObjectContext.DeleteObject(detail);
break;
case ChangeOperation.None:
break;
default:
break;
}
}
}
public void DeleteSalesOrder(SalesOrderHeader salesOrderHeader)
{
if ((salesOrderHeader.EntityState == EntityState.Detached))
{
this.ObjectContext.Attach(salesOrderHeader);
}
switch (salesOrderHeader.Status)
{
case 1: // in process
this.ObjectContext.DeleteObject(salesOrderHeader);
break;
case 2: // approved
case 3: // backordered
case 4: // rejected
salesOrderHeader.Status = 6;
break;
case 5: // shipped
throw new ValidationException("The order has been shipped and cannot be deleted.");
default:
break;
}
}
}