다음을 통해 공유


낙관적 동시성 구현(C#)

작성자 스콧 미첼

PDF 다운로드

여러 사용자가 데이터를 편집할 수 있는 웹 애플리케이션의 경우 두 사용자가 동시에 동일한 데이터를 편집할 위험이 있습니다. 이 자습서에서는 이 위험을 처리하기 위해 낙관적 동시성 제어를 구현합니다.

소개

사용자만 데이터를 볼 수 있도록 허용하는 웹 애플리케이션 또는 데이터를 수정할 수 있는 단일 사용자만 포함된 웹 애플리케이션의 경우 두 명의 동시 사용자가 실수로 서로의 변경 내용을 덮어쓸 위험이 없습니다. 그러나 여러 사용자가 데이터를 업데이트하거나 삭제할 수 있는 웹 애플리케이션의 경우 한 사용자의 수정 내용이 다른 동시 사용자와 충돌할 가능성이 있습니다. 동시성 정책이 없으면 두 사용자가 동시에 단일 레코드를 편집하는 경우 마지막으로 변경 내용을 커밋한 사용자가 첫 번째 변경 내용을 재정의합니다.

예를 들어 Jisun과 Sam이라는 두 사용자가 모두 방문자가 GridView 컨트롤을 통해 제품을 업데이트하고 삭제할 수 있는 애플리케이션의 페이지를 방문했다고 상상해 보세요. 둘 다 GridView에서 동시에 편집 단추를 클릭합니다. Jisun은 제품 이름을 "Chai Tea"로 변경하고 업데이트 단추를 클릭합니다. 순 결과는 데이터베이스로 전송되는 문입니다. 이 문은 UPDATE 제품의 모든 업데이트 가능한 필드를 설정합니다( Jisun이 하나의 필드 ProductName만 업데이트한 경우에도). 이 시점에서 데이터베이스에는 이 특정 제품에 대한 "차이 차", "음료" 범주, 공급업체 "이국적인 액체" 등이 있습니다. 그러나 Sam의 화면에 있는 GridView는 편집 가능한 GridView 행의 제품 이름을 "Chai"로 표시합니다. Jisun의 변경 내용이 커밋된 후 몇 초 후에 Sam은 범주를 Condiments로 업데이트하고 업데이트를 클릭합니다. 그러면 UPDATE 제품 이름을 "Chai"로 설정하고, CategoryID는 해당 음료 범주 ID로 설정하는 문이 데이터베이스로 전송됩니다. Jisun의 제품명 변경사항이 덮어쓰여졌습니다. 그림 1에서는 이 일련의 이벤트를 그래픽으로 보여 줍니다.

두 사용자가 동시에 레코드를 업데이트하는 경우 한 사용자가 다른 사용자를 덮어쓸 가능성이 있습니다.

그림 1: 두 사용자가 동시에 레코드를 업데이트할 때 한 사용자가 다른 사용자를 덮어쓸 가능성이 있습니다(전체 크기 이미지를 보려면 클릭).

마찬가지로 두 사용자가 페이지를 방문할 때 한 사용자가 다른 사용자가 삭제할 때 레코드를 업데이트하는 중일 수 있습니다. 또는 사용자가 페이지를 로드할 때와 삭제 단추를 클릭할 때 다른 사용자가 해당 레코드의 내용을 수정했을 수 있습니다.

다음 세 가지 동시성 제어 전략을 사용할 수 있습니다.

  • Do Nothing -if 동시 사용자가 동일한 레코드를 수정하고 있습니다. 마지막 커밋이 승리하도록 합니다(기본 동작).
  • 낙관적 동시성 - 때때로 동시성 충돌이 있을 수 있지만 대부분의 경우 이러한 충돌이 발생하지 않는다고 가정합니다. 따라서 충돌이 발생하는 경우 다른 사용자가 동일한 데이터를 수정했기 때문에 변경 내용을 저장할 수 없다는 사실을 사용자에게 알리기만 하면 됩니다.
  • 비관적 동시성 - 동시성 충돌이 일반적이며 다른 사용자의 동시 활동으로 인해 변경 내용이 저장되지 않았다는 말을 사용자가 용납하지 않는다고 가정합니다. 따라서 한 사용자가 레코드를 업데이트하기 시작하면 레코드를 잠그면 사용자가 수정 내용을 커밋할 때까지 다른 사용자가 해당 레코드를 편집하거나 삭제할 수 없습니다.

지금까지 모든 자습서는 기본 동시성 해결 전략을 사용했습니다. 즉, 마지막 쓰기가 성공하도록 했습니다. 이 자습서에서는 낙관적 동시성 제어를 구현하는 방법을 살펴보겠습니다.

비고

이 자습서 시리즈에서는 비관적 동시성 예제를 살펴보지 않겠습니다. 비관적 동시성은 이러한 잠금이 제대로 해제되지 않으면 다른 사용자가 데이터를 업데이트하지 못하게 막을 수 있기 때문에 거의 사용되지 않습니다. 예를 들어 사용자가 편집을 위해 레코드를 잠그고 잠금을 해제하기 전에 하루 동안 떠나는 경우 원래 사용자가 해당 업데이트를 반환하고 완료할 때까지 다른 사용자가 해당 레코드를 업데이트할 수 없습니다. 따라서 비관적 동시성이 사용되는 경우, 일반적으로 시간 제한이 도달하면 잠금을 취소합니다. 사용자가 주문 프로세스를 완료하는 동안 짧은 기간 동안 특정 좌석 위치를 잠그는 티켓 판매 웹 사이트는 비관적 동시성 제어의 예입니다.

1단계: 낙관적 동시성이 구현되는 방법 살펴보기

낙관적 동시성 제어는 업데이트 또는 삭제되는 레코드가 업데이트 또는 삭제 프로세스가 시작될 때와 동일한 값을 가지도록 하여 작동합니다. 예를 들어 편집 가능한 GridView에서 편집 단추를 클릭하면 레코드의 값이 데이터베이스에서 읽혀지고 TextBoxes 및 기타 웹 컨트롤에 표시됩니다. 이러한 원래 값은 GridView에 의해 저장됩니다. 나중에 사용자가 변경하고 업데이트 단추를 클릭하면 원래 값과 새 값이 비즈니스 논리 계층으로 전송된 다음 데이터 액세스 계층으로 전송됩니다. 데이터 액세스 계층은 사용자가 편집하기 시작한 원래 값이 데이터베이스의 값과 동일한 경우에만 레코드를 업데이트하는 SQL 문을 실행해야 합니다. 그림 2에서는 이 이벤트 시퀀스를 보여 줍니다.

업데이트 또는 삭제가 성공하려면 원래 값이 현재 데이터베이스 값과 같아야 합니다.

그림 2: 업데이트 또는 삭제가 성공하려면 원래 값이 현재 데이터베이스 값과 같아야 합니다(전체 크기 이미지를 보려면 클릭).

낙관적 동시성을 구현하는 다양한 방법이 있습니다(다양한 옵션을 간략하게 살펴보려면 Peter A. Bromberg낙관적 동시성 업데이트 논리 참조). ADO.NET 형식화된 데이터 세트는 확인란의 틱만으로 구성할 수 있는 하나의 구현을 제공합니다. Typed DataSet에서 TableAdapter에 대해 낙관적 동시성을 사용하도록 설정하면 TableAdapter의 UPDATEDELETE 문이 WHERE 절에 있는 모든 원래 값을 비교하도록 확장됩니다. 예를 들어 다음 UPDATE 문은 현재 데이터베이스 값이 GridView에서 레코드를 업데이트할 때 원래 검색된 값과 같은 경우에만 제품의 이름과 가격을 업데이트합니다. 및 @ProductName 매개 변수는 @UnitPrice 사용자가 입력한 새 값을 포함하는 반면 @original_ProductName@original_UnitPrice 편집 단추를 클릭할 때 원래 GridView에 로드된 값을 포함합니다.

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

비고

UPDATE 문은 가독성을 위해 간소화되었습니다. 실제로 UnitPrice 절에서 WHERE 체크는 UnitPrice s를 포함할 수 있는 NULL 때문에 더 복잡할 수 있으며, NULL = NULL가 항상 False를 반환하는지 여부를 확인해야 합니다(대신 IS NULL를 사용해야 합니다).

다른 기본 UPDATE 문을 사용하는 것 외에도 낙관적 동시성을 사용하도록 TableAdapter를 구성하면 DB 직접 메서드의 서명도 수정됩니다. 첫 번째 자습서인 데이터 액세스 계층 만들기에서 DB 직접 메서드는 스칼라 값 목록을 입력 매개 변수로 허용하는 메서드였습니다(강력한 형식의 DataRow 또는 DataTable 인스턴스가 아닌). 낙관적 동시성을 사용하는 경우 DB 직접 Update()Delete() 메서드에는 원래 값에 대한 입력 매개 변수도 포함됩니다. 또한 일괄 업데이트 패턴을 사용하기 위한 BLL의 코드( Update() 스칼라 값이 아닌 DataRows 및 DataTables를 허용하는 메서드 오버로드)도 변경해야 합니다.

낙관적 동시성을 사용하도록 기존 DAL의 TableAdapters를 확장하는 대신, 이를 수용하기 위해 BLL을 변경해야 하므로, 낙관적 동시성을 사용하는 NorthwindOptimisticConcurrency TableAdapter를 추가할 Products라는 새 형식화된 데이터 세트를 만들어 보겠습니다. 그런 다음 낙관적 동시성 DAL을 지원하기 위해 적절한 수정을 가한 비즈니스 로직 계층 클래스를 생성합니다. 이 기초가 마련되면 ASP.NET 페이지를 만들 준비가 됩니다.

2단계: 낙관적 동시성을 지원하는 데이터 액세스 계층 만들기

새 형식화된 DataSet을 만들려면 폴더 내의 DAL 폴더를 App_Code 마우스 오른쪽 단추로 클릭하고 명명 NorthwindOptimisticConcurrency된 새 DataSet을 추가합니다. 첫 번째 자습서에서 보았듯이 이렇게 하면 형식화된 데이터 세트에 새 TableAdapter가 추가되어 TableAdapter 구성 마법사가 자동으로 시작됩니다. 첫 번째 화면에서는 연결할 데이터베이스로 NORTHWNDConnectionString의 설정을 사용하여 Northwind 데이터베이스와 연결하라는 메시지가 표시됩니다.

동일한 Northwind 데이터베이스에 연결

그림 3: 동일한 Northwind 데이터베이스에 연결(전체 크기 이미지를 보려면 클릭)

다음으로 임시 SQL 문, 새 저장 프로시저 또는 기존 저장 프로시저를 통해 데이터를 쿼리하는 방법을 묻는 메시지가 표시됩니다. 원래 DAL에서 임시 SQL 쿼리를 사용했으므로 여기에서도 이 옵션을 사용합니다.

임시 SQL 문을 사용하여 검색할 데이터 지정

그림 4: 임시 SQL 문을 사용하여 검색할 데이터 지정(전체 크기 이미지를 보려면 클릭)

다음 화면에서 제품 정보를 검색하는 데 사용할 SQL 쿼리를 입력합니다. 제품의 공급업체 및 범주 이름과 함께 모든 열을 반환하는 원래 DAL의 Products TableAdapter에 사용되는 Product 것과 똑같은 SQL 쿼리를 사용하겠습니다.

SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

원래 DAL의 Products TableAdapter에서 동일한 SQL 쿼리 사용

그림 5: 원래 DAL의 TableAdapter에서 Products 동일한 SQL 쿼리 사용(전체 크기 이미지를 보려면 클릭)

다음 화면으로 이동하기 전에 고급 옵션 단추를 클릭합니다. 이 TableAdapter가 낙관적 동시성 제어를 사용하도록 하려면 "낙관적 동시성 사용" 확인란을 선택하기만 하면 됩니다.

그림 6: "낙관적 동시성 사용" 확인란을 선택하여 낙관적 동시성 제어 사용(전체 크기 이미지를 보려면 클릭)

마지막으로 TableAdapter가 DataTable을 채우고 DataTable을 반환하는 데이터 액세스 패턴을 사용해야 함을 나타냅니다. 또한 DB 직접 메서드를 만들어야 함을 나타냅니다. 원래 DAL에서 사용한 명명 규칙을 미러링하도록 DataTable 반환 패턴의 메서드 이름을 GetData에서 GetProducts로 변경합니다.

TableAdapter가 모든 데이터 액세스 패턴을 활용하게 합니다.

그림 7: TableAdapter가 모든 데이터 액세스 패턴을 활용하도록 합니다(전체 크기 이미지를 보려면 클릭).

마법사를 완료한 후, 데이터 세트 디자이너에는 강력하게 형식화된 Products DataTable과 TableAdapter가 포함됩니다. 데이터 테이블 Products의 이름을 ProductsOptimisticConcurrency으로 변경하려면 제목 표시줄을 마우스 오른쪽 버튼으로 클릭하고, 상황에 맞는 메뉴에서 '이름 바꾸기'를 선택하세요.

DataTable 및 TableAdapter가 형식화된 데이터 세트에 추가되었습니다.

그림 8: DataTable 및 TableAdapter가 형식화된 데이터 세트에 추가되었습니다(전체 크기 이미지를 보려면 클릭).

UPDATE TableAdapter(낙관적 동시성을 사용함)와 Products TableAdapter(그렇지 않음) 간의 DELETE 쿼리 차이점을 확인하려면, TableAdapter를 클릭하고 속성 창으로 이동하십시오. DeleteCommandUpdateCommand 속성의 CommandText 하위 속성에서 DAL의 업데이트 또는 삭제 관련 메서드가 호출될 때 데이터베이스로 전송되는 실제 SQL 구문을 볼 수 있습니다. TableAdapter의 ProductsOptimisticConcurrencyDELETE 경우 사용되는 문은 다음과 같습니다.

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

DELETE 반면, 원래 DAL의 Product TableAdapter에 대한 구문은 훨씬 더 간단합니다.

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

알 수 있듯이 낙관적 동시성을 사용하는 TableAdapter의 WHERE 쿼리 절에는 각 DELETE 테이블의 기존 열 값과 GridView(또는 DetailsView 또는 FormView)가 마지막으로 채워졌을 때의 원래 값 간의 비교가 포함됩니다. 다른 모든 필드는 ProductID 값을 가질 수 있으므로, ProductName 절에서 Discontinued 값을 정확하게 비교하기 위해 추가 매개 변수와 검사가 포함됩니다.

ASP.NET 페이지에서는 제품 정보 업데이트 및 삭제만 제공하므로 이 자습서에서는 낙관적 동시성 사용 데이터 세트에 DataTable을 추가하지 않습니다. 그러나 우리는 여전히 GetProductByProductID(productID) 메서드를 ProductsOptimisticConcurrency TableAdapter에 추가해야 합니다.

이렇게 하려면 TableAdapter의 제목 표시줄(바로 위 Fill 영역 및 GetProducts 메서드 이름)을 마우스 오른쪽 단추로 클릭하고 상황에 맞는 메뉴에서 쿼리 추가를 선택합니다. 그러면 TableAdapter 쿼리 구성 마법사가 시작됩니다. TableAdapter의 초기 구성과 마찬가지로 임시 SQL 문을 사용하여 메서드를 만들 GetProductByProductID(productID) 도록 선택합니다(그림 4 참조). 메서드는 GetProductByProductID(productID) 특정 제품에 대한 정보를 반환하므로 이 쿼리가 행을 SELECT 반환하는 쿼리 형식임을 나타냅니다.

쿼리 형식을

그림 9: 쿼리 형식을 "SELECT 행을 반환하는"으로 표시(전체 크기 이미지를 보려면 클릭)

다음 화면에서는 TableAdapter의 기본 쿼리가 미리 로드된 상태에서 사용할 SQL 쿼리를 묻는 메시지가 표시됩니다. 그림 10과 같이 기존 쿼리를 보강하여 절 WHERE ProductID = @ProductID을 포함합니다.

특정 제품 레코드를 반환하기 위해 미리 로드된 쿼리에 WHERE 절 추가

그림 10: 미리 로드된 쿼리에 절을 추가하여 WHERE 특정 제품 레코드를 반환합니다(전체 크기 이미지를 보려면 클릭).

마지막으로 생성된 메서드 이름을 다음과 같이 FillByProductID 변경합니다 GetProductByProductID.

메서드 이름을 FillByProductID 및 GetProductByProductID로 바꿉니다.

그림 11: 메서드 FillByProductID 이름 바꾸기 및 GetProductByProductID (전체 크기 이미지를 보려면 클릭)

이 마법사가 완료되면 TableAdapter에는 이제 데이터를 GetProducts()검색하는 두 가지 메서드, 즉 모든 제품을 반환하고 GetProductByProductID(productID)지정된 제품을 반환하는 두 가지 메서드가 포함됩니다.

3단계: 낙관적 Concurrency-Enabled DAL에 대한 비즈니스 논리 계층 만들기

기존 ProductsBLL 클래스에는 일괄 처리 업데이트와 DB 직접 패턴을 모두 사용하는 예제가 있습니다. AddProduct 메서드와 UpdateProduct 오버로드 모두 일괄 처리 업데이트 패턴을 사용하여 인스턴스를 ProductRow TableAdapter의 Update 메서드에 전달합니다. 반면에 이 메서드는 DeleteProduct TableAdapter의 Delete(productID) 메서드를 호출하는 DB 직접 패턴을 사용합니다.

ProductsOptimisticConcurrency TableAdapter를 사용하면 이제 DB 직접 메서드를 사용하려면 원래 값도 전달해야 합니다. 예를 들어 메서드는 Delete 이제 10개의 입력 매개 변수(원본ProductID, , ProductName, SupplierIDCategoryID, QuantityPerUnitUnitPrice, UnitsInStock, UnitsOnOrderReorderLevel및 )Discontinued를 예상합니다. 데이터베이스로 전송된 문의 절 WHERE 에서 DELETE 이러한 추가 입력 매개 변수 값을 사용하며, 데이터베이스의 현재 값이 원래 값에 매핑되는 경우에만 지정된 레코드를 삭제합니다.

일괄 업데이트 패턴에 사용되는 TableAdapter 메서드에 Update 대한 메서드 서명은 변경되지 않았지만 원래 값과 새 값을 기록하는 데 필요한 코드는 변경되었습니다. 따라서 낙관적 동시성 지원 DAL을 기존 ProductsBLL 클래스와 함께 사용하는 대신 새 DAL로 작업하기 위한 새 비즈니스 논리 계층 클래스를 만들어 보겠습니다.

ProductsOptimisticConcurrencyBLL 폴더 내의 BLL 폴더에 App_Code라는 클래스를 추가합니다.

BLL 폴더에 ProductsOptimisticConcurrencyBLL 클래스 추가

그림 12: BLL 폴더에 클래스 추가 ProductsOptimisticConcurrencyBLL

다음으로, 클래스에 다음 코드를 추가합니다 ProductsOptimisticConcurrencyBLL .

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

클래스 선언의 시작 위에 using NorthwindOptimisticConcurrencyTableAdapters 문을 적어 둡니다. 네임스페이스에는 NorthwindOptimisticConcurrencyTableAdapters DAL의 ProductsOptimisticConcurrencyTableAdapter 메서드를 제공하는 클래스가 포함되어 있습니다. 또한 클래스 선언 전에 Visual Studio에 ObjectDataSource 마법사의 드롭다운 목록에 이 클래스를 포함하도록 지시하는 특성을 찾을 System.ComponentModel.DataObject 수 있습니다.

'의 ProductsOptimisticConcurrencyBLL 속성은 Adapter클래스의 ProductsOptimisticConcurrencyTableAdapter 인스턴스에 대한 빠른 액세스를 제공하고 원래 BLL 클래스(ProductsBLLCategoriesBLL등)에서 사용되는 패턴을 따릅니다. 마지막으로, GetProducts() 메서드는 단순히 DAL의 GetProducts() 메서드를 호출하고, 데이터베이스에 있는 각 제품 레코드에 대한 ProductsOptimisticConcurrencyDataTable 인스턴스로 채워진 ProductsOptimisticConcurrencyRow 객체를 반환합니다.

낙관적 동시성과 함께 DB 직접 패턴을 사용하여 제품 삭제

낙관적 동시성을 사용하는 DAL에 대해 DB 직접 패턴을 사용하는 경우 메서드는 새 값과 원래 값을 전달해야 합니다. 삭제하려면 새 값이 없으므로 원래 값만 전달하면 됩니다. 그런 다음 BLL에서 모든 원래 매개 변수를 입력 매개 변수로 허용해야 합니다. DeleteProduct 클래스의 ProductsOptimisticConcurrencyBLL 메서드가 DB 직접 메서드를 사용하도록 하겠습니다. 즉, 이 메서드는 다음 코드와 같이 10개의 제품 데이터 필드를 모두 입력 매개 변수로 사용하고 DAL에 전달해야 합니다.

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

GridView(또는 DetailsView 또는 FormView)에 마지막으로 로드된 원래 값이 사용자가 삭제 단추를 WHERE 클릭할 때 데이터베이스의 값과 다른 경우 절은 데이터베이스 레코드와 일치하지 않으며 레코드는 영향을 받지 않습니다. 따라서 TableAdapter의 Delete 메서드가 반환 0 되고 BLL의 DeleteProduct 메서드가 반환 false됩니다.

낙관적 동시성을 사용한 배치 업데이트 패턴으로 제품 업데이트하기

앞에서 설명한 것처럼 일괄 업데이트 패턴에 대한 TableAdapter 메서드 Update 는 낙관적 동시성을 사용하는지 여부에 관계없이 동일한 메서드 시그니처를 가집니다. 즉, Update 메서드는 DataRow, DataRows 배열, DataTable 또는 Typed DataSet을 기대합니다. 원래 값을 지정하기 위한 추가 입력 매개 변수는 없습니다. 이는 DataTable이 해당 DataRow의 원래 값과 수정된 값을 추적하기 때문에 가능합니다. DAL이 문을 UPDATE@original_ColumnName 실행하면 매개 변수는 DataRow의 원래 값으로 채워지는 반면 @ColumnName 매개 변수는 DataRow의 수정된 값으로 채워집니다.

ProductsBLL 일괄 처리 업데이트 패턴을 사용하여 제품 정보를 업데이트하는 경우(원래의 낙관적이 아닌 동시성 DAL 사용) 클래스에서 코드는 다음 이벤트 시퀀스를 수행합니다.

  1. 현재 데이터베이스 제품 정보를 TableAdapter의 ProductRow 메서드를 사용하여 GetProductByProductID(productID) 인스턴스로 읽습니다.
  2. 1단계에서 인스턴스에 ProductRow 새 값 할당
  3. TableAdapter의 Update 메서드를 호출하여 인스턴스를 전달합니다 ProductRow .

그러나 이 단계 시퀀스는 1단계에서 채워진 값이 데이터베이스에서 직접 채워지므로 낙관적 동시성을 ProductRow 제대로 지원하지 않습니다. 즉, DataRow에서 사용하는 원래 값은 편집 프로세스 시작 시 GridView에 바인딩된 값이 아니라 현재 데이터베이스에 존재하는 값입니다. 대신 낙관적 동시성 지원 DAL을 사용하는 경우 다음 단계를 사용하도록 메서드 오버로드를 변경 UpdateProduct 해야 합니다.

  1. 현재 데이터베이스 제품 정보를 TableAdapter의 ProductsOptimisticConcurrencyRow 메서드를 사용하여 GetProductByProductID(productID) 인스턴스로 읽습니다.
  2. 1단계에서 인스턴스ProductsOptimisticConcurrencyRow 원래 값 할당
  3. 현재 값이 ProductsOptimisticConcurrencyRow "원래" 값임을 DataRow에 지시하는 인스턴스의 AcceptChanges() 메서드를 호출합니다.
  4. 인스턴스에 ProductsOptimisticConcurrencyRow 할당
  5. TableAdapter의 Update 메서드를 호출하여 인스턴스를 전달합니다 ProductsOptimisticConcurrencyRow .

1단계는 지정된 제품 레코드에 대한 모든 현재 데이터베이스 값을 읽습니다. 이 단계는 UpdateProduct 제품 열을 업데이트하는 오버로드에서는 불필요합니다. 이러한 값은 2단계에서 덮어쓰여지기 때문입니다. 그러나 열 값의 하위 집합만이 입력 매개 변수로 전달되는 오버로드에서는 필수입니다. 원래 값이 ProductsOptimisticConcurrencyRow 인스턴스에 할당되면 메서드 AcceptChanges()가 호출됩니다. 이 메서드는 현재 DataRow 값을 @original_ColumnName 문의 매개 변수 UPDATE 에 사용할 원래 값으로 표시합니다. 다음으로, 새 매개 변수 값이 할당 ProductsOptimisticConcurrencyRow 되고 마지막으로 메서드가 Update 호출되어 DataRow를 전달합니다.

다음 코드는 모든 제품 데이터 필드를 입력 매개 변수로 허용하는 UpdateProduct 오버로드를 보여줍니다. 여기에 ProductsOptimisticConcurrencyBLL 표시되지 않지만 이 자습서의 다운로드에 포함된 클래스에는 제품 이름과 가격만 입력 매개 변수로 허용하는 오버로드도 포함되어 UpdateProduct 있습니다.

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
    // STEP 2: Assign the original values to the product instance
    AssignAllProductValues(product, original_productName, original_supplierID,
        original_categoryID, original_quantityPerUnit, original_unitPrice,
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
        original_discontinued);
    // STEP 3: Accept the changes
    product.AcceptChanges();
    // STEP 4: Assign the new values to the product instance
    AssignAllProductValues(product, productName, supplierID, categoryID,
        quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
        discontinued);
    // STEP 5: Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

4단계: ASP.NET 페이지에서 BLL 메서드로 원본 및 새 값 전달

DAL 및 BLL이 완료되면 시스템에 기본 제공되는 낙관적 동시성 논리를 활용할 수 있는 ASP.NET 페이지를 만들어야 합니다. 특히 데이터 웹 컨트롤(GridView, DetailsView 또는 FormView)은 원래 값을 기억해야 하며 ObjectDataSource는 두 값 집합을 모두 비즈니스 논리 계층에 전달해야 합니다. 또한 동시성 위반을 정상적으로 처리하도록 ASP.NET 페이지를 구성해야 합니다.

먼저 폴더에서 OptimisticConcurrency.aspx 페이지를 열고 Designer에 EditInsertDelete GridView를 추가하고 해당 ID 속성을 ProductsGrid.로 설정합니다. GridView의 스마트 태그에서 새 ObjectDataSource를 ProductsOptimisticConcurrencyDataSource만들도록 선택합니다. 이 ObjectDataSource에서 낙관적 동시성을 지원하는 DAL을 사용하려면 개체를 ProductsOptimisticConcurrencyBLL 사용하도록 구성합니다.

ObjectDataSource에서 ProductsOptimisticConcurrencyBLL 개체를 사용하도록 합니다.

그림 13: ObjectDataSource에서 개체를 ProductsOptimisticConcurrencyBLL 사용하도록 합니다(전체 크기 이미지를 보려면 클릭).

마법사에서 드롭다운 목록의 GetProducts, UpdateProduct, 및 DeleteProduct 메서드를 선택합니다. UpdateProduct 메서드의 경우 제품의 모든 데이터 필드를 허용하는 오버로드를 사용합니다.

ObjectDataSource 컨트롤의 속성 구성

마법사를 완료한 후 ObjectDataSource의 선언적 태그는 다음과 같이 표시됩니다.

<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

확인하실 수 있듯이, DeleteParameters 컬렉션은 Parameter 클래스의 ProductsOptimisticConcurrencyBLL 메서드에 있는 10개의 입력 매개변수 각각에 대한 DeleteProduct 인스턴스를 포함합니다. 마찬가지로, UpdateParameters 안의 각 입력 매개 변수에 대해 컬렉션에는 Parameter에 있는 UpdateProduct 인스턴스가 포함됩니다.

데이터 수정과 관련된 이전 자습서의 경우 이 속성은 BLL 메서드가 이전(또는 원래) 값과 새 값이 전달될 것으로 예상한다는 것을 나타내기 때문에 이 시점에서 ObjectDataSource의 OldValuesParameterFormatString 속성을 제거합니다. 또한 이 속성 값은 원래 값의 입력 매개 변수 이름을 나타냅니다. 원래 값을 BLL에 전달하므로 이 속성을 제거 하지 마세요.

비고

속성 값 OldValuesParameterFormatString 은 원래 값을 예상하는 BLL의 입력 매개 변수 이름에 매핑되어야 합니다. original_productName, original_supplierID와 같은 이름으로 이러한 매개 변수를 지정했기 때문에 OldValuesParameterFormatString 속성 값을 original_{0} 그대로 둘 수 있습니다. 그러나 BLL 메서드의 입력 매개 변수에 이름(예: old_productName, old_supplierID 등)이 있는 경우, OldValuesParameterFormatString 속성을 old_{0}로 업데이트해야 합니다.

ObjectDataSource가 원래 값을 BLL 메서드에 올바르게 전달하려면 마지막으로 설정해야 하는 속성 설정이 하나 있습니다. ObjectDataSource에는 다음 두 값 중 하나에 할당할 수 있는 ConflictDetection 속성이 있습니다.

  • OverwriteChanges - 기본값; 에서는 원래 값을 BLL 메서드의 원래 입력 매개 변수로 보내지 않습니다.
  • CompareAllValues - 원래 값을 BLL 메서드로 보냅니다. 낙관적 동시성을 사용할 때 이 옵션 선택

잠시 시간을 내어 ConflictDetection 속성을 CompareAllValues로 설정하세요.

GridView의 속성 및 필드 구성

ObjectDataSource의 속성이 제대로 구성되었으므로 GridView를 설정하는 데 주의를 기울이겠습니다. 먼저 GridView에서 편집 및 삭제를 지원하도록 하려면 GridView의 스마트 태그에서 편집 사용 및 삭제 사용 확인란을 클릭합니다. 그러면 ShowEditButtonShowDeleteButton 모두 true로 설정된 CommandField가 추가됩니다.

ObjectDataSource에 ProductsOptimisticConcurrencyDataSource 바인딩된 경우 GridView에는 각 제품의 데이터 필드에 대한 필드가 포함됩니다. 이러한 GridView는 편집할 수 있지만 사용자 경험은 결코 허용되지 않습니다. CategoryIDSupplierID BoundFields는 TextBoxes로 렌더링되므로 사용자가 적절한 범주 및 공급자를 ID 번호로 입력해야 합니다. 숫자 필드에 대한 서식은 없으며 제품 이름이 제공되었고 단가, 재고 단위, 주문 단위 및 순서 변경 수준 값이 모두 적절한 숫자 값이고 0보다 크거나 같은지 확인하기 위한 유효성 검사 컨트롤이 없습니다.

편집 및 삽입 인터페이스에 유효성 검사 컨트롤 추가 및데이터 수정 인터페이스 사용자 지정 자습서에서 설명한 대로 BoundFields를 TemplateFields로 바꿔 사용자 인터페이스를 사용자 지정할 수 있습니다. 다음 방법으로 이 GridView 및 해당 편집 인터페이스를 수정했습니다.

  • ProductID, SupplierNameCategoryName BoundFields를 제거했습니다.
  • BoundField를 ProductName TemplateField로 변환하고 RequiredFieldValidation 컨트롤을 추가했습니다.
  • CategoryID SupplierID 및 BoundFields를 TemplateFields로 변환하고 TextBox가 아닌 DropDownLists를 사용하도록 편집 인터페이스를 조정했습니다. 이러한 TemplateFields' ItemTemplates에서는 CategoryName 데이터 필드와 SupplierName 데이터 필드가 표시됩니다.
  • UnitPrice, UnitsInStock, UnitsOnOrderReorderLevel BoundFields를 TemplateFields로 변환하고 CompareValidator 컨트롤을 추가했습니다.

이전 자습서에서 이러한 작업을 수행하는 방법을 이미 살펴보았으므로 여기에 최종 선언적 구문을 나열하고 구현을 그대로 둡니다.

<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

우리는 완전히 작동하는 예제를 갖는 것에 매우 가깝습니다. 그러나 몇 가지 미묘한 차이가 있어 문제를 일으킬 수 있습니다. 또한 동시성 위반이 발생했을 때 사용자에게 경고하는 일부 인터페이스가 여전히 필요합니다.

비고

데이터 웹 컨트롤이 원래 값을 ObjectDataSource에 올바르게 전달(BLL에 전달)하려면 GridView의 EnableViewState 속성을 기본값으로 true 설정하는 것이 중요합니다. 보기 상태를 사용하지 않도록 설정하면 포스트백 시 원래 값이 손실됩니다.

ObjectDataSource에 올바른 원래 값 전달

GridView가 구성된 방식에는 몇 가지 문제가 있습니다. ObjectDataSource의 ConflictDetection 속성이 우리 속성과 마찬가지로 설정된 CompareAllValues 경우, ObjectDataSource 또는 Update() 메서드가 GridView(또는 Delete() DetailsView 또는 FormView)에서 호출될 때 ObjectDataSource는 GridView의 원래 값을 적절한 Parameter 인스턴스로 복사하려고 시도합니다. 이 프로세스의 그래픽 표현은 그림 2를 다시 참조하세요.

특히 GridView의 원래 값에는 데이터가 GridView에 바인딩될 때마다 양방향 데이터 바인딩 문의 값이 할당됩니다. 따라서 필요한 원래 값은 모두 양방향 데이터 바인딩을 통해 캡처되고 변환 가능한 형식으로 제공되는 것이 중요합니다.

이것이 중요한 이유를 확인하려면 잠시 브라우저에서 페이지를 방문하세요. 예상대로 GridView는 왼쪽 열에 편집 및 삭제 단추가 있는 각 제품을 나열합니다.

제품은 GridView에 나열됩니다.

그림 14: 제품이 GridView에 나열됨(전체 크기 이미지를 보려면 클릭)

삭제 버튼을 클릭하면 모든 제품에 대해 FormatException가 던져집니다.

FormatException에서 제품 결과를 삭제하려고 시도

그림 15: 제품 결과 삭제 시도(FormatException전체 크기 이미지를 보려면 클릭)

ObjectDataSource가 원래의 FormatException 값을 읽으려고 시도할 때 UnitPrice가 발생합니다. ItemTemplateUnitPrice가 통화 형식(<%# Bind("UnitPrice", "{0:C}") %>)으로 되어 있어서 $19.95와 같은 통화 기호가 포함되어 있습니다. FormatException ObjectDataSource가 이 문자열decimal을 .로 변환하려고 할 때 발생합니다. 이 문제를 피하기 위해 다음과 같은 다양한 옵션이 있습니다.

  • ItemTemplate에서 통화 서식을 제거하십시오. 즉, <%# Bind("UnitPrice", "{0:C}") %>을 간단히 사용하여 <%# Bind("UnitPrice") %>을 대신합니다. 단점은 가격이 더 이상 형식화되지 않는다는 것입니다.
  • UnitPrice에서 ItemTemplate를 통화 형식으로 표시하되, 이를 위해 Eval 키워드를 사용합니다. Eval 단방향 데이터 바인딩을 수행하는 것을 기억하세요. 원래 값에 대한 UnitPrice 값을 제공해야 하므로 여전히 ItemTemplate에서 양방향 데이터 바인딩 문이 필요합니다. 그러나 이 문은 Visible으로 설정된 false 속성이 있는 Label Web 컨트롤에 배치할 수 있습니다. ItemTemplate에서 다음 태그를 사용할 수 있습니다.
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • ItemTemplate를 사용하여 <%# Bind("UnitPrice") %>의 통화 서식을 제거하세요. GridView의 RowDataBound 이벤트 처리기에서 값이 표시되는 레이블 웹 컨트롤 UnitPrice 에 프로그래밍 방식으로 액세스하고 해당 Text 속성을 서식이 지정된 버전으로 설정합니다.
  • 형식을 UnitPrice 통화 형식으로 유지하십시오. GridView의 RowDeleting 이벤트 처리기에서 기존 원래 UnitPrice 값($19.95)을 사용하여 Decimal.Parse실제 10진수 값으로 바꿉니다. ASP.NET 페이지 자습서에서 RowUpdatingBLL 처리 및 DAL-Level 예외 의 이벤트 처리기에서 비슷한 작업을 수행하는 방법을 알아보았습니다.

내 예제에서는 형식이 지정되지 않은 Text 값에 양방향 데이터 바인딩이 설정된 숨겨진 Label Web 컨트롤 UnitPrice을 추가하는 두 번째 방법을 선택했습니다.

이 문제를 해결한 후 제품의 삭제 단추를 다시 클릭해 보세요. 이번에는 ObjectDataSource가 BLL의 InvalidOperationException 메서드를 호출하려고 할 때 UpdateProduct가 표시됩니다.

ObjectDataSource에서 보내려는 입력 매개 변수가 있는 메서드를 찾을 수 없음

그림 16: ObjectDataSource에서 보내려는 입력 매개 변수가 있는 메서드를 찾을 수 없습니다(전체 크기 이미지를 보려면 클릭).

예외의 메시지를 보면 ObjectDataSource가 DeleteProduct 메서드를 호출하려 할 때 original_CategoryNameoriginal_SupplierName 입력 매개 변수를 포함한 BLL을 필요로 한다는 것이 분명합니다. 이는 현재 ItemTemplateCategoryID TemplateFields가 SupplierIDCategoryName 데이터 필드와 양방향 바인드 문을 포함하고 있기 SupplierName 때문입니다. 대신, Bind 문장을 CategoryID, SupplierID 데이터 필드와 함께 포함해야 합니다. 이렇게 하려면 기존 Bind 문을 Eval 문으로 바꾸고, 다음과 같이 숨겨진 Label 컨트롤을 추가합니다. 즉, Text 속성이 CategoryIDSupplierID 데이터 필드에 양방향 데이터 바인딩되도록 설정합니다.

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

이러한 변경 내용으로 이제 제품 정보를 삭제하고 편집할 수 있습니다. 5단계에서는 동시성 위반이 검색되는지 확인하는 방법을 살펴보겠습니다. 그러나 지금은 몇 가지 레코드를 업데이트하고 삭제하여 단일 사용자에 대한 업데이트 및 삭제가 예상대로 작동하는지 확인하는 데 몇 분 정도 걸립니다.

5단계: 낙관적 동시성 지원 기능 테스트

동시성 위반이 감지되고 있는지 확인하려면(데이터를 맹목적으로 덮어쓰는 것이 아니라) 이 페이지에 두 개의 브라우저 창을 열어야 합니다. 두 브라우저 인스턴스에서 Chai에 대한 편집 단추를 클릭합니다. 그런 다음 브라우저 중 하나에서 이름을 "Chai Tea"로 변경하고 업데이트를 클릭합니다. 업데이트가 성공적으로 완료되면, GridView는 새 제품 이름이 'Chai Tea'로 사전 편집 상태로 되돌아갑니다.

그러나 다른 브라우저 창 인스턴스에서 제품 이름 TextBox는 여전히 "Chai"를 표시합니다. 이 두 번째 브라우저 창에서 UnitPrice25.00로 업데이트합니다. 낙관적 동시성 지원이 없으면 두 번째 브라우저 인스턴스에서 업데이트를 클릭하면 제품 이름이 다시 "Chai"로 변경되어 첫 번째 브라우저 인스턴스에서 변경한 내용을 덮어씁니다. 그러나 낙관적 동시성이 사용되면 두 번째 브라우저 인스턴스에서 업데이트 단추를 클릭하면 DBConcurrencyException이 발생합니다.

동시성 위반이 감지되면 DBConcurrencyException이 throw됩니다.

그림 17: 동시성 위반이 감지되면 DBConcurrencyException 예외가 발생함(전체 크기 이미지를 보려면 클릭)

DAL DBConcurrencyException 의 일괄 업데이트 패턴을 사용하는 경우에만 throw됩니다. DB 직접 패턴은 예외를 발생시키지 않으며 단지 영향을 받은 행이 없음을 나타냅니다. 이를 설명하기 위해 두 브라우저 인스턴스의 GridView를 사전 편집 상태로 반환합니다. 그런 다음 첫 번째 브라우저 인스턴스에서 편집 단추를 클릭하고 제품 이름을 "Chai Tea"에서 "Chai"로 변경하고 업데이트를 클릭합니다. 두 번째 브라우저 창에서 Chai에 대한 삭제 단추를 클릭합니다.

삭제를 클릭하면 페이지가 다시 게시되고 GridView는 ObjectDataSource의 Delete() 메서드를 호출하고 ObjectDataSource는 클래스의 ProductsOptimisticConcurrencyBLL 메서드를 호출하여 DeleteProduct 원래 값을 전달합니다. 두 번째 브라우저 인스턴스의 원래 ProductName 값은 데이터베이스의 현재 ProductName 값과 일치하지 않는 "Chai Tea"입니다. 따라서 데이터베이스에 발행된 DELETE 문은 WHERE 절을 만족하는 레코드가 데이터베이스에 없으므로 0행에 영향을 미칩니다. DeleteProduct 메서드는 false을(를) 반환하고, ObjectDataSource의 데이터가 GridView에 다시 바인딩됩니다.

최종 사용자의 관점에서 두 번째 브라우저 창에서 Chai Tea에 대한 삭제 단추를 클릭하면 화면이 깜박이고, 돌아오면 제품이 여전히 존재하지만 지금은 "Chai"(첫 번째 브라우저 인스턴스에서 만든 제품 이름 변경)로 나열됩니다. 사용자가 삭제 단추를 다시 클릭하면 GridView의 원래 ProductName 값("Chai")이 데이터베이스의 값과 일치하므로 삭제가 성공합니다.

두 경우 모두 사용자 환경은 이상과는 거리가 멀다. 일괄 업데이트 패턴을 사용할 때 사용자에게 예외의 DBConcurrencyException 핵심 세부 정보를 표시하지 않으려는 것이 분명합니다. 그리고 DB 직접 패턴을 사용할 때의 동작은 사용자 명령이 실패할 때 다소 혼란스럽지만 그 이유를 정확하게 알 수는 없었습니다.

이러한 두 가지 문제를 해결하기 위해 업데이트 또는 삭제가 실패한 이유에 대한 설명을 제공하는 레이블 웹 컨트롤을 페이지에 만들 수 있습니다. 일괄 업데이트 패턴의 경우 필요에 따라 경고 레이블을 DBConcurrencyException 표시하는 GridView의 사후 수준 이벤트 처리기에서 예외가 발생했는지 여부를 확인할 수 있습니다. DB 직접 접근 방법의 경우, BLL 메서드의 반환 값을 검사하여 한 행이 영향을 받으면 true, 그렇지 않으면 false를 반환합니다. 이를 통해 필요에 따라 정보 메시지를 표시할 수 있습니다.

6단계: 정보 메시지 추가 및 동시성 위반 발생 시 메시지 표시

동시성 위반이 발생하면 표시되는 동작은 DAL의 일괄 업데이트 또는 DB 직접 패턴이 사용되었는지 여부에 따라 달라집니다. 이 자습서에서는 업데이트에 사용되는 일괄 처리 업데이트 패턴과 삭제에 사용되는 DB 직접 패턴과 함께 두 패턴을 모두 사용합니다. 시작하려면 데이터를 삭제하거나 업데이트하려고 할 때 동시성 위반이 발생했음을 설명하는 두 개의 레이블 웹 컨트롤을 페이지에 추가해 보겠습니다. 레이블 컨트롤의 VisibleEnableViewState 속성을 false로 설정합니다. 그러면 Visible 속성이 프로그래밍 방식으로 true로 설정되는 특정 페이지 방문을 제외하고 각 페이지 방문 시 숨겨집니다.

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

해당 Visible 속성을 EnabledViewState로 설정하고, Text, CssClass, 및 Warning 속성을 설정했습니다. 이러한 설정으로 인해 레이블이 크고, 빨간색, 기울어진 굵은 글꼴로 표시됩니다. 이 CSS Warning 클래스는 삽입, 업데이트 및 삭제 자습서와 관련된 이벤트 검사 자습서에서 이전에 정의되고 Styles.css에 추가되었습니다.

이러한 레이블을 추가한 후 Visual Studio의 디자이너는 그림 18과 유사해야 합니다.

페이지에 두 개의 레이블 컨트롤이 추가되었습니다.

그림 18: 두 개의 레이블 컨트롤이 페이지에 추가되었습니다(전체 크기 이미지를 보려면 클릭).

이러한 레이블 웹 컨트롤을 사용하여 동시성 위반이 발생한 시기를 확인하는 방법을 검토할 준비가 되었습니다. 이때 적절한 레이블의 Visible 속성을 설정하여 정보 메시지를 표시할 true수 있습니다.

업데이트할 때 동시성 위반 처리

일괄 처리 업데이트 패턴을 사용할 때 동시성 위반을 처리하는 방법을 먼저 살펴보겠습니다. 일괄 업데이트 패턴을 DBConcurrencyException 위반하면 예외가 발생하므로, 우리는 업데이트 프로세스 중에 DBConcurrencyException 예외가 발생했는지 여부를 확인하기 위해 ASP.NET 페이지에 코드를 추가해야 합니다. 그렇다면 다른 사용자가 레코드 편집을 시작할 때와 업데이트 단추를 클릭할 때 사이에 동일한 데이터를 수정했기 때문에 변경 내용이 저장되지 않았다는 메시지를 사용자에게 표시해야 합니다.

ASP.NET 페이지 자습서의 BLL 및 DAL-Level 예외 처리에서 살본 것처럼 데이터 웹 컨트롤의 사후 수준 이벤트 처리기에서 이러한 예외를 검색하고 표시하지 않을 수 있습니다. 따라서 GridView의 RowUpdated 이벤트에 대한 이벤트 처리기를 만들어야 하며, 이는 DBConcurrencyException 예외가 발생했는지를 확인합니다. 이 이벤트 처리기는 아래 이벤트 처리기 코드와 같이 업데이트 프로세스 중에 발생한 예외에 대한 참조를 전달합니다.

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

예외가 발생할 DBConcurrencyException 경우, 이 이벤트 처리기는 UpdateConflictMessage 레이블 컨트롤을 표시하고 예외가 처리되었음을 나타냅니다. 이 코드를 사용하면 레코드를 업데이트할 때 동시성 위반이 발생하면 동시에 다른 사용자의 수정 내용을 덮어쓰게 되므로 사용자의 변경 내용이 손실됩니다. 특히 GridView는 사전 편집 상태로 반환되고 현재 데이터베이스 데이터에 바인딩됩니다. 그러면 GridView 행이 이전에 표시되지 않았던 다른 사용자의 변경 내용으로 업데이트됩니다. 또한 레이블 컨트롤은 UpdateConflictMessage 사용자에게 방금 발생한 일을 설명합니다. 이 이벤트 시퀀스는 그림 19에 자세히 설명되어 있습니다.

동시성 위반 시 사용자 업데이트가 손실됨

그림 19: 동시성 위반으로 인해 사용자의 업데이트가 손실됩니다 (전체 크기 이미지를 보려면 클릭)

비고

또는 GridView를 사전 편집 상태로 되돌리는 대신 전달된 KeepInEditMode 개체의 속성을 true로 설정 GridViewUpdatedEventArgs 하여 GridView를 편집 상태로 둘 수 있습니다. 그러나 이 방법을 사용하는 경우 다른 사용자의 값이 편집 인터페이스에 로드되도록(메서드 DataBind() 를 호출하여) GridView에 데이터를 다시 바인딩해야 합니다. 이 자습서에서 다운로드할 수 있는 코드에는 이벤트 처리기에서 RowUpdated 주석으로 처리된 두 줄의 코드가 있습니다. 동시성 위반 후 GridView가 편집 모드로 유지되도록 이러한 코드 줄의 주석 처리를 제거하기만 하면 됩니다.

삭제 시 동시성 위반 문제 해결 방법

DB 직접 패턴에서는 동시성 위반에 직면했을 때 예외가 발생하지 않습니다. 대신 WHERE 절이 레코드와 일치하지 않으므로 데이터베이스 문은 레코드에 영향을 주지 않습니다. BLL에서 만든 모든 데이터 수정 메서드는 정확히 하나의 레코드에 영향을 주었는지 여부를 나타내는 부울 값을 반환하도록 설계되었습니다. 따라서 레코드를 삭제할 때 동시성 위반이 발생했는지 확인하기 위해 BLL DeleteProduct 메서드의 반환 값을 검사할 수 있습니다.

BLL 메서드의 반환 값은 ObjectDataSource의 사후 수준 이벤트 처리기 내에서 이벤트 처리기에 전달된 ReturnValue 개체의 ObjectDataSourceStatusEventArgs 속성을 통해 검사할 수 있습니다. 메서드에서 DeleteProduct 반환 값을 결정하는 데 관심이 있으므로 ObjectDataSource 이벤트에 Deleted 대한 이벤트 처리기를 만들어야 합니다. 속성 ReturnValueobject 형식이며, 예외가 발생하여 메서드가 중단된 경우 null일 수 있습니다. 따라서 먼저 ReturnValue 속성이 null되지 않고 부울 값인지 확인해야 합니다. 이 확인이 통과하면 레이블 컨트롤(있는 DeleteConflictMessage 경우 ReturnValuefalse)이 표시됩니다. 이 작업은 다음 코드를 사용하여 수행할 수 있습니다.

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

동시성 위반이 발생하면 사용자의 삭제 요청이 취소됩니다. GridView가 새로 고쳐져 사용자가 페이지를 로드한 시간과 삭제 단추를 클릭한 시점 사이에 해당 레코드에 대해 발생한 변경 내용이 표시됩니다. 이러한 위반이 발생하면 레이블이 DeleteConflictMessage 표시되어 방금 발생한 일을 설명합니다(그림 20 참조).

동시성 위반 시 사용자 삭제가 취소됨

그림 20: 동시성 위반 상황에서 사용자 삭제가 취소됩니다(전체 크기 이미지를 보려면 클릭)

요약

동시성 위반의 기회는 여러 동시 사용자가 데이터를 업데이트하거나 삭제할 수 있는 모든 애플리케이션에 존재합니다. 이러한 위반이 고려되지 않으면, 두 사용자가 동시에 동일한 데이터를 업데이트할 때, 마지막으로 데이터를 쓴 사용자가 "승리"하게 되어 다른 사용자의 변경 사항이 덮어쓰여지게 됩니다. 또는 개발자는 낙관적 또는 비관적 동시성 제어를 구현할 수 있습니다. 낙관적 동시성 제어는 동시성 위반이 드물다고 가정하고 동시성 위반을 구성하는 업데이트 또는 삭제 명령을 허용하지 않습니다. 비관적 동시성 제어는 동시성 위반이 빈번하고 단순히 한 사용자의 업데이트 또는 삭제 명령을 거부하는 것은 허용되지 않는다고 가정합니다. 비관적 동시성 제어를 사용하면 레코드를 잠그면 다른 사용자가 레코드가 잠겨 있는 동안 레코드를 수정하거나 삭제하지 못하게 됩니다.

.NET의 Typed DataSet은 낙관적 동시성 제어를 지원하는 기능을 제공합니다. 특히 UPDATE 데이터베이스에 발급된 문 및 DELETE 문에는 테이블의 모든 열이 포함되므로 레코드의 현재 데이터가 업데이트 또는 삭제를 수행할 때 사용자가 가지고 있던 원래 데이터와 일치하는 경우에만 업데이트 또는 삭제가 발생합니다. 낙관적 동시성을 지원하도록 DAL이 구성되면 BLL 메서드를 업데이트해야 합니다. 또한 BLL을 호출하는 ASP.NET 페이지는 ObjectDataSource가 데이터 웹 컨트롤에서 원래 값을 검색하여 BLL로 전달하도록 구성해야 합니다.

이 자습서에서 살본 것처럼 ASP.NET 웹 애플리케이션에서 낙관적 동시성 제어를 구현하려면 DAL 및 BLL을 업데이트하고 ASP.NET 페이지에서 지원을 추가하는 작업이 포함됩니다. 이 추가 작업이 시간과 노력의 현명한 투자인지 여부는 애플리케이션에 따라 달라집니다. 데이터를 업데이트하는 동시 사용자가 자주 없거나 업데이트하는 데이터가 서로 다른 경우 동시성 제어는 중요한 문제가 아닙니다. 그러나 사이트에 여러 사용자가 동일한 데이터로 작업하는 경우 동시성 제어를 통해 한 사용자의 업데이트 또는 삭제가 무의식적으로 다른 사용자의 업데이트를 덮어쓰는 것을 방지할 수 있습니다.

행복한 프로그래밍!

작성자 정보

7개의 ASP/ASP.NET 책의 저자이자 4GuysFromRolla.com 창립자인 Scott Mitchell은 1998년부터 Microsoft 웹 기술을 연구해 왔습니다. Scott은 독립 컨설턴트, 트레이너 및 작가로 일합니다. 그의 최신 책은 Sams Teach Yourself 24시간 안에 ASP.NET 2.0입니다. 그는 mitchell@4GuysFromRolla.com로 연락할 수 있습니다.