낙관적 동시성 구현(VB)

작성자 : Scott Mitchell

PDF 다운로드

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

소개

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

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

두 사용자가 동시에 레코드를 업데이트할 때 한 사용자의 변경 내용이 다른 사용자를 덮어쓸 가능성이 있습니다.

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

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

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

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

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

참고

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

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

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

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

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

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

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

참고

UPDATE 문은 가독성을 위해 간소화되었습니다. 실제로 절의 검사 가 포함 NULL 되고 항상 False를 반환하는지 NULL = NULL 확인합니다(대신 을 사용해야 IS NULL합니다UnitPrice).WHEREUnitPrice

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

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

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

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

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

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

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

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

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

다음 화면에서 제품 정보를 검색하는 데 사용할 SQL 쿼리를 입력합니다. 제품의 공급자 및 범주 이름과 함께 모든 열을 반환하는 원래 DAL의 Product TableAdapter에 사용되는 Products 것과 똑같은 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에서 모든 데이터 액세스 패턴을 활용하도록 설정(전체 크기 이미지를 보려면 클릭)

마법사를 완료한 후 DataSet Designer 강력한 형식 Products 의 DataTable 및 TableAdapter가 포함됩니다. 잠시 시간을 내어 DataTable의 제목 표시줄을 마우스 오른쪽 단추로 ProductsOptimisticConcurrency클릭하고 상황에 맞는 메뉴에서 이름 바꾸기를 선택하여 DataTable의 이름을 에서 Products 로 바꿉니다.

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

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

TableAdapter(낙관적 동시성을 사용함)와 DELETE Products TableAdapter(그렇지 않음) 간의 ProductsOptimisticConcurrency 및 쿼리 간의 UPDATE 차이점을 확인하려면 TableAdapter를 클릭하고 속성 창 이동합니다. 및 UpdateCommand 속성의 DeleteCommandCommandText 하위 속성에서 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))

WHERE 수 있듯이 낙관적 동시성을 사용하는 TableAdapter에 대한 문의 절 DELETE 에는 GridView(또는 DetailsView 또는 FormView)가 마지막으로 채워진 시점의 각 Product 테이블의 기존 열 값과 원래 값 간의 비교가 포함됩니다. , 및 DiscontinuedProductName이외의 ProductID모든 필드에는 값이 있을 NULL 수 있으므로 절의 WHERE 값을 올바르게 비교 NULL 하기 위해 추가 매개 변수 및 검사가 포함됩니다.

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

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

쿼리 형식을

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

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

미리 로드된 쿼리에 WHERE 절을 추가하여 특정 제품 레코드 반환

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

마지막으로 생성된 메서드 이름을 및 GetProductByProductIDFillByProductID 변경합니다.

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

그림 11: 메서드 이름을 및 GetProductByProductIDFillByProductID 바꿉니다(전체 크기 이미지를 보려면 클릭).

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

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

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

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

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

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

BLL 폴더에 ProductsOptimisticConcurrencyBLL 클래스 추가

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

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

Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
    Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
            End If
            Return _productsAdapter
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
        Return Adapter.GetProducts()
    End Function
End Class

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

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

낙관적 동시성이 있는 DB 직접 패턴을 사용하여 제품 삭제

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

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = 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
End Function

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

낙관적 동시성을 사용하여 Batch 업데이트 패턴을 사용하여 제품 업데이트

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

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

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

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

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

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

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

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = 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
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

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

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

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

ObjectDataSource ProductsOptimisticConcurrencyBLL 개체 사용

그림 13: ObjectDataSource 개체 사용 ProductsOptimisticConcurrencyBLL (전체 크기 이미지를 보려면 클릭)

마법사의 GetProducts드롭다운 목록에서 , UpdateProductDeleteProduct 메서드를 선택합니다. 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>

보듯이 컬렉션에는 클래스 DeleteProductDeleteParameters 메서드에 있는 Parameter 10개의 입력 매개 변수 각각에 ProductsOptimisticConcurrencyBLL 대한 instance 포함되어 있습니다. 마찬가지로 컬렉션에는 의 UpdateParameters 각 입력 매개 변수UpdateProduct에 대한 instance 포함 Parameter 됩니다.

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

참고

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

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

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

잠시 시간을 내어 속성을 CompareAllValuesConflictDetection 설정합니다.

GridView의 속성 및 필드 구성

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

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

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

  • ProductID, SupplierNameCategoryName BoundFields를 제거했습니다.
  • BoundField를 ProductName TemplateField로 변환하고 RequiredFieldValidation 컨트롤을 추가했습니다.
  • CategoryIDSupplierID BoundFields를 TemplateFields로 변환하고 TextBox가 아닌 DropDownLists를 사용하도록 편집 인터페이스를 조정했습니다. 이 TemplateFields의 ItemTemplates에서 CategoryNameSupplierName 데이터 필드가 표시됩니다.
  • 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 (있는 그대로) GridView(또는 DetailsView 또는 FormView)에서 ObjectDataSource 또는 Delete()Update() 메서드를 호출할 때 ObjectDataSource는 GridView의 원래 값을 적절한 Parameter 인스턴스에 복사하려고 시도합니다. 이 프로세스의 그래픽 표현은 그림 2를 다시 참조하세요.

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

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

제품은 GridView에 나열됩니다.

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

모든 제품에 대해 삭제 단추를 클릭하면 이 FormatException throw됩니다.

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

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

FormatException ObjectDataSource가 원래 UnitPrice 값에서 읽으려고 할 때 가 발생합니다. ItemTemplate 에는 UnitPrice 통화(<%# Bind("UnitPrice", "{0:C}") %>)로 형식이 지정되므로 $19.95와 같은 통화 기호가 포함됩니다. FormatException ObjectDataSource가 이 문자열을 로 decimal변환하려고 할 때 이 발생합니다. 이 문제를 우회하기 위해 다음과 같은 여러 가지 옵션이 있습니다.

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

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

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

ObjectDataSource에서 보내려는 입력 매개 변수를 사용하여 메서드를 찾을 수 없음

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

예외의 메시지를 살펴보면 ObjectDataSource가 및 original_SupplierName 입력 매개 변수를 포함하는 original_CategoryName BLL DeleteProduct 메서드를 호출하려고 한다는 것이 분명합니다. 및 TemplateFields의 에 CategoryID 현재 및 SupplierIDSupplierName 데이터 필드가 있는 양방향 Bind 문이 CategoryName 포함되어 있기 ItemTemplate 때문입니다. 대신 및 SupplierID 데이터 필드가 있는 CategoryID 문을 포함 Bind 해야 합니다. 이렇게 하려면 기존 Bind 문을 문으로 Eval 바꾼 다음 아래와 같이 양방향 데이터 바인딩을 사용하여 속성이 TextSupplierID 데이터 필드에 바인딩 CategoryID 된 숨겨진 Label 컨트롤을 추가합니다.

<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"와 함께 사전 편집 상태로 되돌립니다.

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

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

그림 17: 동시성 위반이 감지되면 DBConcurrencyException 이 throw됨(전체 크기 이미지를 보려면 클릭)

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

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

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

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

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

6단계: 정보 메시지 추가 및 동시성 위반의 얼굴에 메시지 표시

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

<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." />

, VisibleEnabledViewStateText 속성을 설정하는 것 외에도 속성을 Warning로 설정 CssClass 하여 Label이 크고 빨간색, 기울임꼴, 굵은 글꼴로 표시됩니다. 이 CSS Warning 클래스는 삽입, 업데이트 및 삭제와 관련된 이벤트 검사 자습서에서 다시 Styles.css 정의되고 추가되었습니다.

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

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

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

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

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

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

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

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

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

동시성 위반의 얼굴에서 사용자의 업데이트 손실됩니다.

그림 19: 동시성 위반의 얼굴에서 사용자 업데이트 손실됨(전체 크기 이미지를 보려면 클릭)

참고

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

삭제 시 동시성 위반에 대응

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

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

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

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

동시성 위반의 얼굴에서 사용자 삭제가 취소됨

그림 20: 동시성 위반의 얼굴에서 사용자 삭제가 취소됨(전체 크기 이미지를 보려면 클릭)

요약

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

.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은 독립 컨설턴트, 트레이너 및 작가로 일합니다. 그의 최신 책은 샘스 자신을 가르친다 ASP.NET 2.0 24 시간. 그는 에서 찾을 수있는 그의 블로그를 통해 또는 에 mitchell@4GuysFromRolla.comhttp://ScottOnWriting.NET도달 할 수 있습니다.