작성자 스콧 미첼
여러 사용자가 데이터를 편집할 수 있는 웹 애플리케이션의 경우 두 사용자가 동시에 동일한 데이터를 편집할 위험이 있습니다. 이 자습서에서는 이 위험을 처리하기 위해 낙관적 동시성 제어를 구현합니다.
소개
사용자만 데이터를 볼 수 있도록 허용하는 웹 애플리케이션 또는 데이터를 수정할 수 있는 단일 사용자만 포함된 웹 애플리케이션의 경우 두 명의 동시 사용자가 실수로 서로의 변경 내용을 덮어쓸 위험이 없습니다. 그러나 여러 사용자가 데이터를 업데이트하거나 삭제할 수 있는 웹 애플리케이션의 경우 한 사용자의 수정 내용이 다른 동시 사용자와 충돌할 가능성이 있습니다. 동시성 정책이 없으면 두 사용자가 동시에 단일 레코드를 편집하는 경우 마지막으로 변경 내용을 커밋한 사용자가 첫 번째 변경 내용을 재정의합니다.
예를 들어 Jisun과 Sam이라는 두 사용자가 모두 방문자가 GridView 컨트롤을 통해 제품을 업데이트하고 삭제할 수 있는 애플리케이션의 페이지를 방문했다고 상상해 보세요. 둘 다 GridView에서 동시에 편집 단추를 클릭합니다. Jisun은 제품 이름을 "Chai Tea"로 변경하고 업데이트 단추를 클릭합니다. 순 결과는 데이터베이스로 전송되는 문입니다. 이 문은 UPDATE
제품의 모든 업데이트 가능한 필드를 설정합니다( Jisun이 하나의 필드 ProductName
만 업데이트한 경우에도). 현재 데이터베이스에는 이 제품에 대한 값으로 "차이 티", 음료 카테고리, 공급업체 Exotic Liquids 등이 기록되어 있습니다. 그러나 Sam의 화면에 있는 GridView는 편집 가능한 GridView 행의 제품 이름을 "Chai"로 표시합니다. Jisun의 변경 내용이 커밋된 후 몇 초 후에 Sam은 범주를 Condiments로 업데이트하고 업데이트를 클릭합니다. 제품 이름을 "Chai"로 설정하고 UPDATE
를 해당 음료 범주 ID로 설정하는 CategoryID
명령문이 데이터베이스로 전송됩니다. 그리고 나머지 항목도 동일하게 처리됩니다. 제품 이름에 대한 Jisun의 변경 내용이 덮어쓰여졌습니다. 그림 1에서는 이 일련의 이벤트를 그래픽으로 보여 줍니다.
그림 1: 두 사용자가 동시에 레코드를 업데이트할 때 한 사용자가 다른 사용자를 덮어쓰도록 변경될 가능성이 있습니다(전체 크기 이미지를 보려면 클릭).
마찬가지로 두 사용자가 페이지를 방문할 때 한 사용자가 다른 사용자가 삭제할 때 레코드를 업데이트하는 중일 수 있습니다. 또는 사용자가 페이지를 로드할 때와 삭제 단추를 클릭할 때 다른 사용자가 해당 레코드의 내용을 수정했을 수 있습니다.
다음 세 가지 동시성 제어 전략을 사용할 수 있습니다.
- Do Nothing -if 동시 사용자가 동일한 레코드를 수정하고 있습니다. 마지막 커밋이 승리하도록 합니다(기본 동작).
- 낙관적 동시성 - 때때로 동시성 충돌이 있을 수 있지만 대부분의 경우 이러한 충돌이 발생하지 않는다고 가정합니다. 따라서 충돌이 발생하는 경우 다른 사용자가 동일한 데이터를 수정했기 때문에 변경 내용을 저장할 수 없다는 사실을 사용자에게 알리기만 하면 됩니다.
- 비관적 동시성 - 동시성 충돌이 일반적이며 다른 사용자의 동시 활동으로 인해 변경 내용이 저장되지 않았다는 말을 사용자가 용납하지 않는다고 가정합니다. 따라서 한 사용자가 레코드를 업데이트하기 시작하면 레코드를 잠그면 사용자가 수정 내용을 커밋할 때까지 다른 사용자가 해당 레코드를 편집하거나 삭제할 수 없습니다.
지금까지 모든 자습서는 기본 동시성 해결 전략을 사용했습니다. 즉, 마지막 쓰기가 성공하도록 했습니다. 이 자습서에서는 낙관적 동시성 제어를 구현하는 방법을 살펴보겠습니다.
비고
이 자습서 시리즈에서는 비관적 동시성 예제를 살펴보지 않을 것입니다. 비관적 동시성은 이러한 잠금이 적절하게 해제되지 않으면 다른 사용자가 데이터를 업데이트하는 것을 방지할 수 있기 때문에 거의 사용되지 않습니다. 예를 들어 사용자가 편집을 위해 레코드를 잠그고 잠금을 해제하기 전에 하루 동안 떠나는 경우 원래 사용자가 해당 업데이트를 반환하고 완료할 때까지 다른 사용자가 해당 레코드를 업데이트할 수 없습니다. 따라서 비관적 동시성이 사용되는 경우 일반적으로 잠금을 취소하는 시간 제한이 있습니다. 사용자가 주문 프로세스를 완료하는 동안 짧은 기간 동안 특정 좌석 위치를 잠그는 티켓 판매 웹 사이트는 비관적 동시성 제어의 예입니다.
1단계: 낙관적 동시성이 구현되는 방법 살펴보기
낙관적 동시성 제어는 업데이트 또는 삭제되는 레코드가 업데이트 또는 삭제 프로세스가 시작될 때와 동일한 값을 가지도록 하여 작동합니다. 예를 들어 편집 가능한 GridView에서 편집 단추를 클릭하면 레코드의 값이 데이터베이스에서 읽혀지고 TextBoxes 및 기타 웹 컨트롤에 표시됩니다. 이러한 원래 값은 GridView에 의해 저장됩니다. 나중에 사용자가 변경하고 업데이트 단추를 클릭하면 원래 값과 새 값이 비즈니스 논리 계층으로 전송된 다음 데이터 액세스 계층으로 전송됩니다. 데이터 액세스 계층은 사용자가 편집하기 시작한 원래 값이 데이터베이스의 값과 동일한 경우에만 레코드를 업데이트하는 SQL 문을 실행해야 합니다. 그림 2에서는 이 이벤트 시퀀스를 보여 줍니다.
그림 2: 업데이트 또는 삭제가 성공하려면 원래 값이 현재 데이터베이스 값과 같아야 합니다(전체 크기 이미지를 보려면 클릭).
낙관적 동시성을 구현하는 다양한 방법이 있습니다(다양한 옵션을 간략하게 살펴보려면 Peter A. Bromberg의 낙관적 동시성 업데이트 논리 참조). ADO.NET 형식화된 데이터 세트는 확인란의 틱만으로 구성할 수 있는 하나의 구현을 제공합니다. Typed DataSet에서 TableAdapter의 낙관적 동시성을 활성화하면 TableAdapter의 UPDATE
및 DELETE
문이 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
가 NULL
를 포함할 수 있으며, NULL = NULL
가 항상 False를 반환하는지 확인해야 하기 때문입니다(대신 IS NULL
를 사용해야 합니다).
다른 기본 UPDATE
문을 사용하는 것 외에도 낙관적 동시성을 사용하도록 TableAdapter를 구성하면 DB 직접 메서드의 서명도 수정됩니다. 첫 번째 자습서인 데이터 액세스 계층 만들기에서 DB 직접 메서드는 스칼라 값 목록을 입력 매개 변수로 허용하는 메서드였습니다(강력한 형식의 DataRow 또는 DataTable 인스턴스가 아닌). 낙관적 동시성을 사용하는 경우 DB 직접 Update()
및 Delete()
메서드에는 원래 값에 대한 입력 매개 변수도 포함됩니다. 또한 일괄 업데이트 패턴을 사용하기 위한 BLL의 코드( Update()
스칼라 값이 아닌 DataRows 및 DataTables를 허용하는 메서드 오버로드)도 변경해야 합니다.
기존 DAL의 TableAdapter를 낙관적 동시성을 사용하도록 확장하는 대신(BLL을 조정해야 함), 우리는 낙관적 동시성을 사용하는 NorthwindOptimisticConcurrency
TableAdapter를 추가할 Products
라는 새 형식화된 데이터 세트를 만들어 보겠습니다. 낙관적 동시성 DAL을 지원하기 위해 적절한 수정이 가해진 비즈니스 논리 계층 클래스를 다음 단계에서 만듭니다. 이 기초가 마련되면 ASP.NET 페이지를 만들 준비가 됩니다.
2단계: 낙관적 동시성을 지원하는 데이터 액세스 계층 만들기
새 형식화된 DataSet을 만들려면 폴더 내의 DAL
폴더를 App_Code
마우스 오른쪽 단추로 클릭하고 명명 NorthwindOptimisticConcurrency
된 새 DataSet을 추가합니다. 첫 번째 자습서에서 보았듯이 이렇게 하면 형식화된 데이터 세트에 새 TableAdapter가 추가되어 TableAdapter 구성 마법사가 자동으로 시작됩니다. 첫 번째 화면에서는 NORTHWNDConnectionString
의 Web.config
설정을 사용하여 동일한 Northwind 데이터베이스에 연결하도록 요청받습니다.
그림 3: 동일한 Northwind 데이터베이스에 연결(전체 크기 이미지를 보려면 클릭)
다음으로 임시 SQL 문, 새 저장 프로시저 또는 기존 저장 프로시저를 통해 데이터를 쿼리하는 방법을 묻는 메시지가 표시됩니다. 원래 DAL에서 임시 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
그림 5: 원래 DAL의 TableAdapter에서 Products
동일한 SQL 쿼리 사용(전체 크기 이미지를 보려면 클릭)
다음 화면으로 이동하기 전에 고급 옵션 단추를 클릭합니다. 이 TableAdapter가 낙관적 동시성 제어를 사용하도록 하려면 "낙관적 동시성 사용" 확인란을 선택하기만 하면 됩니다.
그림 6: "낙관적 동시성 사용" 확인란을 선택하여 낙관적 동시성 제어 사용(전체 크기 이미지를 보려면 클릭)
마지막으로 TableAdapter가 DataTable을 채우고 DataTable을 반환하는 데이터 액세스 패턴을 사용해야 함을 나타냅니다. 또한 DB 직접 메서드를 만들어야 함을 나타냅니다. 원래 DAL에서 사용한 명명 규칙을 미러링하도록 DataTable 반환 패턴의 메서드 이름을 GetData에서 GetProducts로 변경합니다.
그림 7: TableAdapter가 모든 데이터 액세스 패턴을 활용하도록 합니다(전체 크기 이미지를 보려면 클릭).
마법사를 완료한 후 데이터 세트 디자이너에는 엄격하게 형식화된 Products
DataTable과 TableAdapter가 포함됩니다. DataTable의 이름을 Products
에서 ProductsOptimisticConcurrency
로 변경하려면, DataTable의 제목 표시줄을 마우스 오른쪽 버튼으로 클릭하고 나타나는 메뉴에서 '이름 바꾸기'를 선택하세요.
그림 8: DataTable 및 TableAdapter가 형식화된 데이터 세트에 추가되었습니다(전체 크기 이미지를 보려면 클릭).
TableAdapter(낙관적 동시성을 사용하는 UPDATE
)와 Products TableAdapter(DELETE
(그렇지 않음) 사이의 ProductsOptimisticConcurrency
쿼리의 차이점을 확인하려면 TableAdapter를 클릭하고 속성 창으로 이동합니다.
DeleteCommand
및 UpdateCommand
속성의 CommandText
하위 속성에서 DAL의 업데이트 또는 삭제 관련 메서드가 호출될 때 데이터베이스로 전송되는 실제 SQL 구문을 볼 수 있습니다. TableAdapter의 ProductsOptimisticConcurrency
DELETE
경우 사용되는 문은 다음과 같습니다.
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
테이블 열의 현재 값과 그리드뷰(또는 디테일뷰 또는 폼뷰)가 마지막으로 채워졌을 당시의 원래 값 간의 비교가 포함됩니다.
ProductID
, ProductName
, Discontinued
필드를 제외한 모든 필드는 NULL
값을 가질 수 있으므로, 절의 NULL
에서 WHERE
값을 올바르게 비교하기 위해 추가적인 매개 변수와 검사가 포함됩니다.
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
을 포함합니다.
그림 10: 미리 로드된 쿼리에 절을 추가하여 WHERE
특정 제품 레코드를 반환합니다(전체 크기 이미지를 보려면 클릭).
마지막으로 생성된 메서드 이름을 다음과 같이 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
, SupplierID
CategoryID
, QuantityPerUnit
UnitPrice
, UnitsInStock
, UnitsOnOrder
ReorderLevel
및 )Discontinued
를 예상합니다. 데이터베이스로 전송된 문의 절 WHERE
에서 DELETE
이러한 추가 입력 매개 변수 값을 사용하며, 데이터베이스의 현재 값이 원래 값에 매핑되는 경우에만 지정된 레코드를 삭제합니다.
일괄 업데이트 패턴에 사용되는 TableAdapter 메서드에 Update
대한 메서드 서명은 변경되지 않았지만 원래 값과 새 값을 기록하는 데 필요한 코드는 변경되었습니다. 따라서 낙관적 동시성 지원 DAL을 기존 ProductsBLL
클래스와 함께 사용하는 대신 새 DAL로 작업하기 위한 새 비즈니스 논리 계층 클래스를 만들어 보겠습니다.
ProductsOptimisticConcurrencyBLL
폴더 내의 BLL
폴더에 App_Code
라는 클래스 추가하기.
그림 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
속성은 Adapter
클래스의 ProductsOptimisticConcurrencyTableAdapter
인스턴스에 대한 빠른 액세스를 제공하고 원래 BLL 클래스(ProductsBLL
CategoriesBLL
등)에서 사용되는 패턴을 따릅니다. 마지막으로, GetProducts()
메서드는 단순히 DAL의 GetProducts()
메서드를 호출하여 데이터베이스의 각 제품 기록에 대해 ProductsOptimisticConcurrencyDataTable
인스턴스로 채워진 ProductsOptimisticConcurrencyRow
객체를 반환합니다.
낙관적 동시성과 함께 DB 직접 패턴을 사용하여 제품 삭제
낙관적 동시성을 사용하는 DAL에 대해 DB 직접 패턴을 사용하는 경우 메서드는 새 값과 원래 값을 전달해야 합니다. 삭제하려면 새 값이 없으므로 원래 값만 전달하면 됩니다. 그런 다음 BLL에서 모든 원래 매개 변수를 입력 매개 변수로 허용해야 합니다.
DeleteProduct
클래스의 ProductsOptimisticConcurrencyBLL
메서드가 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
됩니다.
낙관적 동시성을 사용한 배치 업데이트 패턴으로 제품 업데이트하기
앞에서 설명한 것처럼 일괄 업데이트 패턴에 대한 TableAdapter 메서드 Update
는 낙관적 동시성을 사용하는지 여부에 관계없이 동일한 메서드 시그니처를 가집니다. 즉, Update
메서드는 DataRow, DataRow 배열, DataTable 또는 Typed DataSet을 기대합니다. 원래 값을 지정하기 위한 추가 입력 매개 변수는 없습니다. 이는 DataTable이 해당 DataRow의 원래 값과 수정된 값을 추적하기 때문에 가능합니다. DAL이 문을 UPDATE
@original_ColumnName
실행하면 매개 변수는 DataRow의 원래 값으로 채워지는 반면 @ColumnName
매개 변수는 DataRow의 수정된 값으로 채워집니다.
ProductsBLL
일괄 처리 업데이트 패턴을 사용하여 제품 정보를 업데이트하는 경우(원래의 낙관적이 아닌 동시성 DAL 사용) 클래스에서 코드는 다음 이벤트 시퀀스를 수행합니다.
- TableAdapter의
ProductRow
메서드를 사용하여 현재 데이터베이스 제품 정보를GetProductByProductID(productID)
인스턴스로 읽습니다. - 1단계에서 인스턴스에
ProductRow
새 값 할당 - TableAdapter의
Update
메서드를 호출하여 인스턴스를 전달합니다ProductRow
.
그러나 이 단계 시퀀스는 1단계에서 채워진 값이 데이터베이스에서 직접 채워지므로 낙관적 동시성을 ProductRow
제대로 지원하지 않습니다. 즉, DataRow에서 사용하는 원래 값은 편집 프로세스 시작 시 GridView에 바인딩된 값이 아니라 현재 데이터베이스에 존재하는 값입니다. 대신 낙관적 동시성 지원 DAL을 사용하는 경우 다음 단계를 사용하도록 메서드 오버로드를 변경 UpdateProduct
해야 합니다.
- TableAdapter의
ProductsOptimisticConcurrencyRow
메서드를 사용하여 현재 데이터베이스 제품 정보를GetProductByProductID(productID)
인스턴스로 읽습니다. - 1단계에서 인스턴스 에
ProductsOptimisticConcurrencyRow
원래 값 할당 - 현재 값이
ProductsOptimisticConcurrencyRow
"원래" 값임을 DataRow에 지시하는 인스턴스의AcceptChanges()
메서드를 호출합니다. - 인스턴스에 새 값
ProductsOptimisticConcurrencyRow
할당 - TableAdapter의
Update
메서드를 호출하여 인스턴스를 전달합니다ProductsOptimisticConcurrencyRow
.
1단계는 지정된 제품 레코드에 대한 모든 현재 데이터베이스 값을 읽습니다. 이 단계는 모든 제품 열을 업데이트하는 UpdateProduct
오버로드에서는 이러한 값들이 2단계에서 덮어쓰기 때문에 불필요하지만, 열 값의 하위 집합만 입력 매개 변수로 전달되는 오버로드에서는 필수적입니다. 원래 값이 인스턴스 ProductsOptimisticConcurrencyRow
에 할당되면 AcceptChanges()
메서드가 호출됩니다. 이 메서드는 현재 DataRow 값을 @original_ColumnName
문장 내의 매개 변수 UPDATE
에 사용할 원래 값으로 표시합니다. 다음으로, 새 매개 변수 값이 할당 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.aspx
페이지를 열고 Designer에 EditInsertDelete
GridView를 추가하고 해당 ID
속성을 ProductsGrid
.로 설정합니다. GridView의 스마트 태그에서 새 ObjectDataSource를 ProductsOptimisticConcurrencyDataSource
만들도록 선택합니다. 이 ObjectDataSource에서 낙관적 동시성을 지원하는 DAL을 사용하려면 개체를 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의 스마트 태그에서 편집 사용 및 삭제 사용 확인란을 클릭합니다. 이렇게 하면 각각 ShowEditButton
로 설정된 ShowDeleteButton
및 true
를 포함하는 CommandField가 추가됩니다.
ObjectDataSource에 ProductsOptimisticConcurrencyDataSource
바인딩된 경우 GridView에는 각 제품의 데이터 필드에 대한 필드가 포함됩니다. 이러한 GridView는 편집할 수 있지만 사용자 경험은 결코 만족스럽지 않습니다.
CategoryID
및 SupplierID
BoundFields는 TextBoxes로 렌더링되므로 사용자가 적절한 범주 및 공급자를 ID 번호로 입력해야 합니다. 숫자 필드에 대한 서식은 없으며 제품 이름이 제공되었고 단가, 재고 단위, 주문 단위 및 순서 변경 수준 값이 모두 적절한 숫자 값이고 0보다 크거나 같은지 확인하기 위한 유효성 검사 컨트롤이 없습니다.
편집 및 삽입 인터페이스에 유효성 검사 컨트롤 추가 및데이터 수정 인터페이스 사용자 지정 자습서에서 설명한 대로 BoundFields를 TemplateFields로 바꿔 사용자 인터페이스를 사용자 지정할 수 있습니다. 다음 방법으로 이 GridView 및 해당 편집 인터페이스를 수정했습니다.
-
ProductID
,SupplierName
및CategoryName
BoundFields를 제거했습니다. - BoundField를
ProductName
TemplateField로 변환하고 RequiredFieldValidation 컨트롤을 추가했습니다. -
CategoryID
SupplierID
및 BoundFields를 TemplateFields로 변환하고 TextBox가 아닌 DropDownLists를 사용하도록 편집 인터페이스를 조정했습니다. 이러한 TemplateFields'ItemTemplates
에서는CategoryName
데이터 필드와SupplierName
데이터 필드가 표시됩니다. -
UnitPrice
,UnitsInStock
,UnitsOnOrder
및ReorderLevel
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는 왼쪽 열에 편집 및 삭제 단추가 있는 각 제품을 나열합니다.
그림 14: 제품이 GridView에 나열됨(전체 크기 이미지를 보려면 클릭)
모든 제품에 대한 삭제 단추를 클릭하면 FormatException
예외가 발생합니다.
그림 15: 제품 결과 삭제 시도(FormatException
전체 크기 이미지를 보려면 클릭)
ObjectDataSource가 원래 FormatException
값을 읽으려 할 때 UnitPrice
오류가 발생합니다.
ItemTemplate
는 UnitPrice
통화로 형식화되어 있기 때문에 $19.95와 같은 통화 기호가 포함됩니다.
FormatException
ObjectDataSource가 이 문자열decimal
을 .로 변환하려고 할 때 발생합니다. 이 문제를 피하기 위해 다음과 같은 다양한 옵션이 있습니다.
-
ItemTemplate
에서 통화 서식을 제거합니다. 즉,<%# Bind("UnitPrice", "{0:C}") %>
를 사용하지 말고 간단히<%# Bind("UnitPrice") %>
를 사용하십시오. 단점은 가격이 더 이상 형식화되지 않는다는 것입니다. -
UnitPrice
을(를)ItemTemplate
에 통화 형식으로 표시하되, 이를 위해Eval
키워드를 사용합니다.Eval
단방향 데이터 바인딩을 수행하는 것을 기억하세요. 원래 값에 대해UnitPrice
값을 제공해야 하므로ItemTemplate
에서는 여전히 양방향 데이터 바인딩이 필요합니다. 그러나 이 문은 속성이Visible
로 설정된 Label Web 컨트롤의false
에 배치될 수 있습니다. 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 페이지 자습서에서RowUpdating
BLL 처리 및 DAL-Level 예외 의 이벤트 처리기에서 비슷한 작업을 수행하는 방법을 알아보았습니다.
내 예제에서는 두 번째 방법을 선택했으며, 숨겨진 Label Web 컨트롤의 Text
속성을 형식이 지정되지 않은 UnitPrice
값에 양방향 데이터 바인딩하여 추가했습니다.
이 문제를 해결한 후 제품의 삭제 단추를 다시 클릭해 보세요. 이번에는 ObjectDataSource가 BLL의 InvalidOperationException
메서드를 호출하려고 할 때 UpdateProduct
오류가 발생합니다.
그림 16: ObjectDataSource에서 보내려는 입력 매개 변수가 있는 메서드를 찾을 수 없습니다(전체 크기 이미지를 보려면 클릭).
예외 메시지를 보면 ObjectDataSource가 입력 매개변수 DeleteProduct
및 original_CategoryName
을 포함하는 original_SupplierName
BLL 메서드를 호출한다는 것이 분명합니다. 이는 ItemTemplate
및 CategoryID
TemplateFields에 서로 양방향 Bind 문이 SupplierID
및 CategoryName
데이터 필드를 포함하고 있기 때문입니다. 대신, 우리는 Bind
문장을 CategoryID
및 SupplierID
데이터 필드와 함께 포함시켜야 합니다. 이렇게 하려면 기존의 Bind 문을 Eval
문으로 바꾸고, 양방향 데이터 바인딩을 사용하여 Text
속성이 CategoryID
및 SupplierID
데이터 필드에 바인딩된 숨겨진 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"로 변경하고 업데이트를 클릭합니다. 업데이트가 성공하면 "Chai Tea"라는 새로운 제품 이름을 가진 GridView가 사전 편집 상태로 복원됩니다.
그러나 다른 브라우저 창 인스턴스에서 제품 이름 TextBox는 여전히 "Chai"를 표시합니다. 이 두 번째 브라우저 창에서 UnitPrice
를 25.00
로 업데이트합니다. 낙관적 동시성 지원이 없으면 두 번째 브라우저 인스턴스에서 업데이트를 클릭하면 제품 이름이 다시 "Chai"로 변경되어 첫 번째 브라우저 인스턴스에서 변경한 내용을 덮어씁니다. 그러나 낙관적 동시성이 사용되면 두 번째 브라우저 인스턴스에서 업데이트 단추를 클릭하면 DBConcurrencyException이 발생합니다.
그림 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
을(를) 반환하고, ObjectDataSource의 데이터가 GridView에 다시 바인딩됩니다.
최종 사용자의 관점에서 두 번째 브라우저 창에서 Chai Tea에 대한 삭제 단추를 클릭하면 화면이 깜박이고, 돌아오면 제품이 여전히 존재하지만 지금은 "Chai"(첫 번째 브라우저 인스턴스에서 만든 제품 이름 변경)로 나열됩니다. 사용자가 삭제 단추를 다시 클릭하면 GridView의 원래 ProductName
값("Chai")이 데이터베이스의 값과 일치하므로 삭제가 성공합니다.
두 경우 모두 사용자 환경은 이상과는 거리가 멀다. 일괄 업데이트 패턴을 사용할 때 사용자에게 예외의 DBConcurrencyException
핵심 세부 정보를 표시하지 않으려는 것이 분명합니다. 그리고 DB 직접 패턴을 사용할 때의 동작은 사용자 명령이 실패할 때 다소 혼란스럽지만 그 이유를 정확하게 알 수는 없었습니다.
이러한 두 가지 문제를 해결하기 위해 업데이트 또는 삭제가 실패한 이유에 대한 설명을 제공하는 레이블 웹 컨트롤을 페이지에 만들 수 있습니다. 일괄 업데이트 패턴의 경우 필요에 따라 경고 레이블을 DBConcurrencyException
표시하는 GridView의 사후 수준 이벤트 처리기에서 예외가 발생했는지 여부를 확인할 수 있습니다. DB 직접 메서드를 사용할 경우 BLL 메서드의 반환 값(true
이 한 행이 영향을 받은 경우, false
그 외 경우)을 검사하여 필요 시 정보 메시지를 표시할 수 있습니다.
6단계: 정보 메시지 추가 및 동시성 위반 발생 시 메시지 표시
동시성 위반이 발생하면 표시되는 동작은 DAL의 일괄 업데이트 또는 DB 직접 패턴이 사용되었는지 여부에 따라 달라집니다. 이 자습서에서는 업데이트에 사용되는 일괄 처리 업데이트 패턴과 삭제에 사용되는 DB 직접 패턴과 함께 두 패턴을 모두 사용합니다. 시작하려면 데이터를 삭제하거나 업데이트하려고 할 때 동시성 위반이 발생했음을 설명하는 두 개의 레이블 웹 컨트롤을 페이지에 추가해 보겠습니다. 레이블 컨트롤의 Visible
및 EnableViewState
속성을 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
예외가 throw되었는지 확인하는 이벤트 핸들러를 만들어야 합니다. 이 이벤트 처리기는 아래 이벤트 처리기 코드와 같이 업데이트 프로세스 중에 발생한 예외에 대한 참조를 전달합니다.
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
경우 이 이벤트 처리기는 레이블 컨트롤을 UpdateConflictMessage
표시하고 예외가 처리되었음을 나타냅니다. 이 코드를 사용하면 레코드를 업데이트할 때 동시성 위반이 발생하면 동시에 다른 사용자의 수정 내용을 덮어쓰게 되므로 사용자의 변경 내용이 손실됩니다. 특히 GridView는 사전 편집 상태로 반환되고 현재 데이터베이스 데이터에 바인딩됩니다. 그러면 GridView 행이 이전에 표시되지 않았던 다른 사용자의 변경 내용으로 업데이트됩니다. 또한 레이블 컨트롤은 UpdateConflictMessage
사용자에게 방금 발생한 일을 설명합니다. 이 이벤트 시퀀스는 그림 19에 자세히 설명되어 있습니다.
그림 19: 동시성 위반이 발생할 때 사용자 업데이트가 손실됨 (이미지를 전체 크기로 보려면 클릭)
비고
또는 GridView를 사전 편집 상태로 되돌리는 대신 전달된 KeepInEditMode
개체의 속성을 true로 설정 GridViewUpdatedEventArgs
하여 GridView를 편집 상태로 둘 수 있습니다. 그러나 이 방법을 사용하는 경우 다른 사용자의 값이 편집 인터페이스에 로드되도록(메서드 DataBind()
를 호출하여) GridView에 데이터를 다시 바인딩해야 합니다. 이 자습서에서 다운로드할 수 있는 코드에는 이벤트 처리기에서 RowUpdated
주석으로 처리된 두 줄의 코드가 있습니다. 동시성 위반 후 GridView가 편집 모드로 유지되도록 이러한 코드 줄의 주석 처리를 제거하기만 하면 됩니다.
삭제 작업 중 동시성 위반에 대한 대응
DB 직접 패턴에서는 동시성 위반에 직면했을 때 예외가 발생하지 않습니다. 대신 WHERE 절이 어떤 레코드와도 일치하지 않기 때문에, 데이터베이스 문이 아무런 레코드에도 영향을 주지 않습니다. BLL에서 만든 모든 데이터 수정 메서드는 정확히 하나의 레코드에 영향을 주었는지 여부를 나타내는 부울 값을 반환하도록 설계되었습니다. 따라서 레코드를 삭제할 때 동시성 위반이 발생했는지 확인하기 위해 BLL DeleteProduct
메서드의 반환 값을 검사할 수 있습니다.
BLL 메서드의 반환 값은 이벤트 처리기에 전달된 ReturnValue
개체의 ObjectDataSourceStatusEventArgs
속성을 통해 ObjectDataSource의 사후 수준 이벤트 처리기에서 검사할 수 있습니다. 메서드에서 DeleteProduct
반환 값을 결정하는 데 관심이 있으므로 ObjectDataSource 이벤트에 Deleted
대한 이벤트 처리기를 만들어야 합니다. 속성 ReturnValue
은 형식 object
이며, 예외가 발생했을 경우 메서드가 값 반환 전에 중단될 수 있습니다. 따라서 먼저 속성이 ReturnValue
가 아니고 부울 값인지 확인해야 합니다. 이 검사가 통과되면, DeleteConflictMessage
이 ReturnValue
일 경우 false
레이블 컨트롤이 표시됩니다. 이 작업은 다음 코드를 사용하여 수행할 수 있습니다.
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: 동시성 위반이 발생한 경우 사용자 삭제가 취소됨(전체 크기 이미지를 보려면 클릭)
요약
동시성 위반의 기회는 여러 동시 사용자가 데이터를 업데이트하거나 삭제할 수 있는 모든 애플리케이션에 존재합니다. 위반 사항이 고려되지 않으면, 두 사용자가 동일한 데이터를 동시에 업데이트할 때 마지막으로 데이터를 쓴 쪽이 변경 내용을 덮어쓰게 되어 결과적으로 다른 사용자의 변경 사항을 무시하게 됩니다. 또는 개발자는 낙관적 또는 비관적 동시성 제어를 구현할 수 있습니다. 낙관적 동시성 제어는 동시성 위반이 드물다고 가정하고 동시성 위반을 구성하는 업데이트 또는 삭제 명령을 허용하지 않습니다. 비관적 동시성 제어는 동시성 위반이 빈번하고 단순히 한 사용자의 업데이트 또는 삭제 명령을 거부하는 것은 허용되지 않는다고 가정합니다. 비관적 동시성 제어를 사용하면 레코드를 잠그면 다른 사용자가 레코드가 잠겨 있는 동안 레코드를 수정하거나 삭제하지 못하게 됩니다.
.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.com로 연락할 수 있습니다.