다음을 통해 공유


구성 계층 구조

WCF RIA Services를 사용하면 포함하는 개체(전체 또는 상위)가 포함되는 개체(부분 또는 하위)의 만들기 및 수명을 제어하는 "포함" 관계로 연결된 클래스가 들어 있는 구성 계층 구조에 속하는 데이터 클래스에 대한 응용 프로그램 논리를 만들 수 있습니다. 예를 들어 SalesOrderHeader 엔터티에는 주문과 관련된 세부 정보가 주문의 일부로만 존재하므로 SalesOrderDetail 엔터티가 포함됩니다. 클래스 구성은 보다 일반적인 유형(탈것)에 세부 정보를 추가하여 보다 구체적인 유형(자동차)을 만드는 과정으로 구성되는 클래스의 하위 형식 지정과 비교할 수 있습니다. 그 결과 자세한(파생된) 클래스는 일반적인(기본) 형식과 동일하게 처리될 수 있는 상속 계층 구조를 얻을 수 있습니다. 예를 들어 자동차는 "여전히" 탈것이기도 합니다.

연관된 클래스 간의 구성적인 관계를 정의한 다음에는 이를 별개의 엔터티로 처리하는 대신 단일 단위로 처리하는 데이터 수정 작업을 엔터티에서 수행할 수 있습니다. 이렇게 하면 각 엔터티에 대해 적용하기 위해 논리를 분할하고 데이터 작업 중에 분할된 논리를 조정하는 대신 전체 계층 구조에 대한 응용 프로그램 논리를 작성할 수 있으므로 중간 계층 논리가 간소화됩니다.

구성 계층 구조 이해

엔터티의 계층 구조에서 한 엔터티를 부모 엔터티라고 하고 다른 관련 엔터티를 하위 엔터티라고 합니다. 부모 엔터티는 하위 엔터티의 데이터에 대한 단일 루트인 데이터를 나타내는 클래스입니다. 예를 들어 SalesOrderHeader 엔터티는 부모 엔터티고 SalesOrderDetail은 하위 엔터티입니다. SalesOrderHeader 엔터티의 단일 레코드를 SalesOrderDetail 엔터티의 여러 레코드에 연결할 수 있습니다.

계층적 관계의 일부인 데이터 클래스는 대개 다음과 같은 특징을 가지고 있습니다.

  • 여러 하위 엔터티가 하나의 부모 엔터티에 연결된 트리로 엔터티 간의 관계를 나타낼 수 있습니다. 하위 엔터티를 원하는 수준만큼 확장할 수 있습니다.

  • 하위 엔터티의 수명이 부모 엔터티의 수명 내에 포함되어 있습니다.

  • 부모 엔터티의 컨텍스트 외부에서 의미 있는 ID가 하위 엔터티에 없습니다.

  • 여러 엔터티에 대한 데이터 작업을 위해 엔터티가 하나의 단위로 취급되어야 합니다. 예를 들어, 하위 엔터티의 레코드를 추가, 삭제 또는 업데이트하려면 부모 엔터티에서도 동일하게 변경되어야 합니다.

구성 관계 정의

엔터티 간의 연결을 나타내는 속성에 CompositionAttribute 특성을 적용하여 엔터티 간의 구성 관계를 정의합니다. 다음 예제에서는 메타데이터 클래스를 사용하여 SalesOrderHeaderSalesOrderDetail 간의 구성 관계를 정의하는 방법을 보여 줍니다. System.ComponentModel.DataAnnotations 네임스페이스에 CompositionAttribute 특성이 있습니다. 다음 코드와 같이 특성을 적용하기 위해 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 특성을 적용하고 쿼리 메서드에 하위 엔터티를 포함해야 합니다. 다음 단원 끝부분의 예제에는 쿼리 메서드에 하위 엔터티를 포함하는 방법이 나와 있습니다.

구성 계층 구조와의 도메인 서비스 작업

구성 계층 구조를 정의할 때 상위 및 하위 엔터티와 상호 작용하는 방식을 변경해야 합니다. 도메인 서비스에 포함하는 논리로 엔터티 간의 링크가 설명되어야 합니다. 일반적으로 부모 엔터티에 대한 도메인 서비스 메서드를 통해 계층 구조에 대한 논리를 정의합니다. 부모 엔터티에 대한 도메인 서비스 작업에서는 부모 엔터티와 하위 엔터티에 대한 수정 사항을 처리합니다.

구성 관계가 있는 엔터티에 대한 도메인 서비스 작업에는 다음 규칙이 적용됩니다.

  • 부모 또는 하위 엔터티에 대한 쿼리 메서드도 허용되지만 부모 엔터티의 컨텍스트에서 하위 엔터티를 검색하는 것이 좋습니다. 부모 엔터티가 없이 로드된 하위 엔터티를 수정할 경우 예외가 throw됩니다.

  • 하위 엔터티에 데이터 수정 작업을 추가할 수 있지만 하위 엔터티에 대해 허용되는 작업은 부모 엔터티에 대해 허용되는 작업의 영향을 받습니다.

    • 부모 엔터티에 대해 업데이트가 허용되는 경우 하위 엔터티에 대해 업데이트, 삽입 및 삭제가 허용됩니다.

    • 부모 엔터티에 명명된 업데이트 메서드가 있으면 모든 하위 엔터티에서 업데이트를 사용할 수 있어야 합니다.

    • 부모 엔터티에 대해 삽입이나 삭제가 허용되는 경우 하위 엔터티에 대해서도 재귀적으로 해당 작업이 허용됩니다.

클라이언트 프로젝트 내에서 구성 관계가 있는 엔터티를 사용하는 데 다음 규칙이 적용됩니다.

  • 하위 엔터티에 변경 사항이 있을 때 변경 알림이 부모 엔터티에 전파됩니다. 부모 엔터티에 대한 HasChanges 속성은 true로 설정됩니다.

  • 부모 엔터티가 수정되면 변경되지 않은 하위 엔터티를 비롯한 모든 해당 하위 엔터티가 변경 집합에 포함됩니다.

  • 도메인 컨텍스트의 공용 EntitySet은 하위 엔터티에 대한 클라이언트에 생성되지 않습니다. 부모 엔터티를 통해 하위 엔터티에 액세스해야 합니다.

  • 하위 엔터티는 동일한 수준의 여러 상위 항목으로 정의할 수 있지만 단일 상위 항목의 컨텍스트 내에서만 로드되어야 합니다.

다음 규칙에 따라 데이터 수정 작업이 실행됩니다.

  • 하위 엔터티에 대해 재귀적으로 데이터 수정 작업을 실행하기 전에 먼저 부모 엔터티에 대해 업데이트, 삽입 또는 삭제 작업이 실행됩니다.

  • 필요한 데이터 작업이 하위 엔터티에 없을 경우 재귀 실행이 중지됩니다.

  • 부모 엔터티를 업데이트할 때 하위 항목에 대한 데이터 작업의 실행 순서는 지정되지 않습니다.

다음 예제에서는 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;
        }
       
    }
}