LINQ를 배울 때 저는 제목에서 볼 수 있는 데이터베이스 업데이트 작업이라는 어려움에 거의 쓰러질 뻔했습니다. 이제 나는 당신을 이 수렁 속으로 한걸음씩 데려가겠습니다. 벽돌과 침을 준비하십시오.
가장 간단한 사례부터 시작해 보겠습니다. Northwind 데이터베이스를 예로 들어 제품의 ProductName을 수정해야 하는 경우 클라이언트에서 직접 다음 코드를 작성할 수 있습니다.
// 목록 0NorthwindDataContext db = new NorthwindDataContext();
제품 product = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "차이가 변경되었습니다";
db.제출변경사항();
테스트해 보면 업데이트가 성공합니다. 그러나 나는 그러한 코드가 재사용이 불가능하기 때문에 귀하의 프로젝트에 나타나지 않을 것이라고 믿습니다. 좋아요, 리팩터링하여 메소드로 추출해 보겠습니다. 매개변수는 무엇이어야 합니까? 새 제품 이름과 업데이트할 제품 ID입니다. 글쎄요, 그런 것 같습니다.
공개 무효 UpdateProduct(int id, 문자열 productName)
{
NorthwindDataContext db = new NorthwindDataContext();
제품 product = db.Products.Single(p => p.ProductID == id);
product.ProductName = 제품이름;
db.제출변경사항();
}실제 프로젝트에서는 제품명만 수정할 수는 없습니다. 제품의 다른 필드도 수정될 수 있습니다. 그러면 UpdateProduct 메서드의 서명은 다음과 같습니다.
공개 무효 업데이트제품(int id,
문자열 제품 이름,
정수 공급업체 ID,
정수 카테고리 ID,
문자열 수량PerUnit,
십진수 단위가격,
짧은 단위재고,
짧은 단위 OnOrder,
short reorderLevel) 물론 이것은 단순한 데이터베이스일 뿐이며 실제 프로젝트에서는 20개, 30개, 심지어 수백 개의 필드를 갖는 것이 드문 일이 아닙니다. 누가 그런 방법을 용인할 수 있겠는가? 이렇게 작성하면 Product 개체는 무엇을 합니까?
맞습니다. Product를 메소드의 매개변수로 사용하고 클라이언트 코드에 성가신 할당 작업을 던지십시오. 동시에 Product 인스턴스를 얻기 위한 코드를 추출하여 GetProduct 메서드를 구성하고 데이터베이스 작업과 관련된 메서드를 데이터베이스 처리를 특별히 담당하는 ProductRepository 클래스에 넣었습니다. 아, 그래, SRP!
// 목록 1
// 제품 저장소
공개 제품 GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct(제품 제품)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(제품);
db.제출변경사항();
}
//클라이언트 코드
ProductRepository 저장소 = 새 ProductRepository();
제품 제품 = 저장소.GetProduct(1);
product.ProductName = "차이가 변경되었습니다";
저장소.UpdateProduct(제품);
여기서는 Attach 메서드를 사용하여 Product 인스턴스를 다른 DataContext에 연결합니다. 기본 Northwind 데이터베이스의 경우 결과는 다음과 같습니다.
// 예외 1 NotSupportException:
엔터티 연결 또는 추가를 시도했습니다. 해당 엔터티는 새 엔터티가 아니며 다른 DataContext에서 로드되었을 수 있습니다. 이 작업은 지원되지 않습니다.
새 항목이 아닌 항목을 연결하거나 추가하려는 시도가 있었습니다.
아마도 다른 DataContext에서 로드되었을 수 있습니다. 이는 지원되지 않습니다. MSDN을 보면 엔터티를 클라이언트에 직렬화할 때 이러한 엔터티가 원래 DataContext에서 분리된다는 것을 알고 있습니다. DataContext는 더 이상 이러한 엔터티의 변경 사항이나 다른 개체와의 연결을 추적하지 않습니다. 지금 데이터를 업데이트하거나 삭제하려면 SubmitChanges를 호출하기 전에 Attach 메서드를 사용하여 엔터티를 새 DataContext에 연결해야 합니다. 그렇지 않으면 위 예외가 발생합니다.
Northwind 데이터베이스에서 Product 클래스에는 세 가지 관련 클래스(예: 외래 키 연결)인 Order_Detail, Category 및 Supply가 포함되어 있습니다. 위의 예에서는 제품을 연결했지만 연결된 연결 클래스가 없으므로 NotSupportException이 발생합니다.
그렇다면 제품과 관련된 클래스를 어떻게 연관시킬 수 있을까요? Northwind와 같은 간단한 데이터베이스의 경우에도 이는 복잡해 보일 수 있습니다. 원본 Product와 관련된 Order_Detail, Category, Supply의 원본 클래스를 먼저 구해서 각각 현재 DataContext에 첨부해야 할 것 같지만, 실제로 이렇게 해도 NotSupportException이 발생하게 됩니다.
그렇다면 업데이트 작업을 구현하는 방법은 무엇입니까? 단순화를 위해 Northwind.dbml에서 다른 엔터티 클래스를 삭제하고 Product만 유지합니다. 이런 식으로 가장 간단한 경우부터 분석을 시작할 수 있습니다.
문제로 인해 다른 클래스를 삭제한 후 List 1의 코드를 다시 실행했지만 데이터베이스에서는 제품 이름이 변경되지 않았습니다. Attach 메서드의 오버로드된 버전을 살펴보면 문제를 쉽게 찾을 수 있습니다.
Attach(entity) 메서드는 기본적으로 Attach(entity, false) 오버로드를 호출하여 해당 엔터티를 수정되지 않은 상태로 연결합니다. Product 개체가 수정되지 않은 경우 이 오버로드된 버전을 호출하여 후속 작업을 위해 수정되지 않은 상태로 DataContext에 Product 개체를 연결해야 합니다. 이때 Product 객체의 상태는 "수정됨"이므로 Attach(entity, true) 메소드만 호출할 수 있습니다.
그래서 목록 1의 관련 코드를 Attach(product, true)로 변경하고 무슨 일이 일어났는지 살펴보겠습니다.
// 예외 2 InvalidOperationException:
엔터티가 버전 멤버를 선언하거나 업데이트 확인 정책이 없는 경우 원래 상태 없이 수정된 엔터티로만 첨부될 수 있습니다.
엔터티는 원래 상태 없이 수정된 형태로만 첨부될 수 있습니다.
버전 멤버를 선언하거나 업데이트 확인 정책이 없는 경우.
LINQ to SQL은 RowVersion 열을 사용하여 기본 낙관적 동시성 검사를 구현합니다. 그렇지 않으면 수정된 상태에서 DataContext에 엔터티를 연결할 때 위의 오류가 발생합니다. RowVersion 열을 구현하는 방법에는 두 가지가 있습니다. 하나는 데이터베이스 테이블에 대한 타임스탬프 유형 열을 정의하는 것이고, 다른 하나는 테이블의 기본 키에 해당하는 엔터티 속성에 IsVersion=true 속성을 정의하는 것입니다. TimeStamp 열과 IsVersion=true 특성을 동시에 가질 수 없습니다. 그렇지 않으면 InvalidOprationException이 발생합니다. "System.Data.Linq.Binary TimeStamp" 및 "Int32 ProductID" 멤버는 모두 행 버전으로 표시됩니다. 이 문서에서는 타임스탬프 열을 예로 사용합니다.
TimeStamp라는 열을 만들고 Products 테이블에 대한 timestamp를 입력한 후 다시 디자이너로 끌어서 목록 1의 코드를 실행합니다. 하느님 감사합니다. 마침내 성공했습니다.
이제 카테고리 테이블을 디자이너로 끌어 놓습니다. 이번에 교훈을 얻었고 먼저 카테고리 테이블에 타임스탬프 열을 추가했습니다. 테스트한 결과 다시 예외 1의 오류인 것으로 나타났습니다! 카테고리의 타임스탬프 열을 삭제한 후에도 문제가 남아 있습니다. 맙소사, 끔찍한 Attach 메소드에서는 정확히 어떤 작업이 수행됩니까?
아, 그런데 Attach 메서드의 오버로드된 버전이 있으니 시도해 보겠습니다.
public void UpdateProduct(제품 제품)
{
NorthwindDataContext db = new NorthwindDataContext();
제품 oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(제품, 기존제품);
db.제출변경사항();
} 또는 예외 1 오류!
나는 떨어질 것이다! 첨부, 첨부, 무슨 일이 있었나요?
LINQ to SQL 소스 코드를 탐색하기 위해 Reflector의 FileDisassembler 플러그인을 사용하여 System.Data.Linq.dll을 cs 코드로 디컴파일하고 프로젝트 파일을 생성합니다. 이는 Visual Studio에서 해당 파일을 찾고 찾는 데 도움이 됩니다.
예외 1은 언제 발생합니까?
먼저 System.Data.Linq.resx에서 예외 1에 설명된 정보를 찾고 "CannotAttachAddNonNewEntities" 키를 가져온 다음 System.Data.Linq.Error.CannotAttachAddNonNewEntities() 메서드를 찾고 이 메서드에 대한 모든 참조를 찾은 다음 이 메서드는 StandardChangeTracker.Track 메서드와 InitializeDeferredLoader 메서드의 세 위치에서 사용됩니다.
Table.Attach(entity, bool)의 코드를 다시 열면 예상대로 StandardChangeTracker.Track 메서드를 호출하는 것을 볼 수 있습니다(Attach(entity,entity) 메서드의 경우에도 마찬가지).
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); Track 메서드에서 다음 코드는 예외 1을 발생시킵니다.
if(trackedObject.HasDeferredLoaders)
{
던져 System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}그래서 우리는 StandardTrackedObject.HasDeferredLoaders 속성에 주목합니다.
내부 재정의 bool HasDeferredLoaders
{
얻다
{
foreach(this.Type.Associations의 MetaAssociation 연관)
{
if (this.HasDeferredLoader(association.ThisMember))
{
사실을 반환;
}
}
foreach(this.Type.PersistantDataMembers의 p의 MetaDataMember 멤버
여기서 p.IsDeferred && !p.IsAssociation
p)를 선택하세요
{
if (this.HasDeferredLoader(멤버))
{
사실을 반환;
}
}
거짓을 반환;
}
} 이를 통해 엔터티에 지연 로드된 항목이 있는 한 연결 작업에서 예외 1이 발생한다는 것을 대략적으로 추론할 수 있습니다. 이는 예외 1이 발생하는 시나리오와 정확히 일치합니다. 즉, Product 클래스에 지연 로드 항목이 포함되어 있습니다.
그런 다음 이 예외를 방지하는 방법이 나타났습니다. 제품에 로드하는 것을 지연해야 하는 항목을 제거하는 것입니다. 그것을 제거하는 방법? DataLoadOptions를 사용하여 즉시 로드하거나 지연 로딩이 필요한 항목을 null로 설정할 수 있습니다. 그런데 첫 번째 방법이 통하지 않아서 두 번째 방법을 이용하게 되었습니다.
// 목록 2
클래스 제품 저장소
{
공개 제품 GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
공개 제품 GetProductNoDeffered(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
//DataLoadOptions 옵션 = new DataLoadOptions();
//options.LoadWith<제품>(p => p.Category);
//db.LoadOptions = 옵션;
var product = db.Products.SingleOrDefault(p => p.ProductID == id);
제품.카테고리 = null;
제품 반품;
}
public void UpdateProduct(제품 제품)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(제품, 참);
db.제출변경사항();
}
}
//클라이언트 코드
ProductRepository 저장소 = 새 ProductRepository();
제품 제품 = 저장소.GetProductNoDeffered(1);
product.ProductName = "차이가 변경되었습니다";
저장소.UpdateProduct(제품);
예외 2는 언제 발생합니까?
이전 섹션의 방법에 따라 예외 2를 발생시킨 코드를 빠르게 찾았습니다. 다행히 전체 프로젝트에는 다음 코드만 있었습니다.
if (asModified && ((inheritanceType.VersionMember == null) && 상속Type.HasUpdateCheck))
{
throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
보시다시피 Attach의 두 번째 매개변수 asModified가 true이고 RowVersion 열(VersionMember=null)을 포함하지 않고 업데이트 확인 열(HasUpdateCheck)을 포함하는 경우 예외 2가 발생합니다. HasUpdateCheck의 코드는 다음과 같습니다.
공개 재정의 boHasUpdateCheck
{
얻다
{
foreach(this.PertantDataMembers의 MetaDataMember 멤버)
{
if (member.UpdateCheck != UpdateCheck.Never)
{
사실을 반환;
}
}
거짓을 반환;
}
}이는 우리의 시나리오와도 일치합니다. Products 테이블에는 RowVersion 열이 없으며 디자이너가 자동으로 생성한 코드에서 모든 필드의 UpdateCheck 속성은 기본값인 Always입니다. 즉, HasUpdateCheck 속성은 true입니다.
예외 2를 방지하는 방법은 더 간단합니다. 모든 테이블에 TimeStamp 열을 추가하거나 모든 테이블의 기본 키 필드에 IsVersion=true 필드를 설정하는 것입니다. 후자의 방법은 자동으로 생성된 클래스를 수정하고 언제든지 새로운 디자인으로 덮어쓸 수 있으므로 전자의 방법을 사용하는 것이 좋습니다.
Attach 메서드를 사용하는 방법은 무엇입니까?
위의 분석을 통해 Attach 메서드와 관련된 두 가지 조건, 즉 RowVersion 열이 있는지 여부와 외래 키 연결(즉, 지연 로드가 필요한 항목)이 있는지 확인할 수 있습니다. 이 두 가지 조건과 Attach의 여러 오버로드 사용법을 표로 정리했습니다. 아래 표를 보면 정신적으로 충분히 준비가 되어 있어야 합니다.
일련번호
부착방법
RowVersion 열에 연결된 설명이 있는지 여부
1 첨부(엔티티) 없음 없음 수정 없음
2 Attach(엔티티) 아니요 예 NotSupportException: 엔터티 연결 또는 추가가 시도되었습니다. 엔터티는 새 엔터티가 아니며 다른 DataContext에서 로드될 수 있습니다. 이 작업은 지원되지 않습니다.
3 첨부(엔티티) 수정사항 없음
4 Attach(엔티티)가 수정되지 않았습니다. 하위 집합에 RowVersion 열이 없으면 2와 동일합니다.
5 Attach(entity, true) 아니요 아니요 InvalidOperationException: 엔터티가 버전 멤버를 선언하거나 업데이트 확인 정책이 없는 경우 원래 상태 없이 수정된 엔터티로만 연결할 수 있습니다.
6 Attach(엔티티, true) 아니요 예 NotSupportException: 엔터티가 새 엔터티가 아니며 다른 DataContext에서 로드될 수 있습니다. 이 작업은 지원되지 않습니다.
7 Attach(entity, true) 수정 정상 여부 (RowVersion 컬럼을 강제로 수정하면 에러가 발생함)
8 Attach(엔티티, true) 예 NotSupportException: 엔터티 연결 또는 추가가 시도되었습니다. 엔터티는 새 엔터티가 아니며 다른 DataContext에서 로드될 수 있습니다. 이 작업은 지원되지 않습니다.
9 Attach(엔티티, 엔터티) 아니요 아니요 DuplicateKeyException: 키가 이미 사용 중인 엔터티를 추가할 수 없습니다.
10 Attach(엔티티, 엔터티) 아니요 예 NotSupportException: 엔터티가 새 엔터티가 아니며 다른 DataContext에서 로드될 수 있습니다. 이 작업은 지원되지 않습니다.
11 Attach(엔티티, 엔터티) DuplicateKeyException: 키가 이미 사용 중인 엔터티를 추가할 수 없습니다.
12 Attach(엔티티, 엔터티) 예 NotSupportException: 엔터티 연결 또는 추가가 시도되었습니다. 엔터티는 새 엔터티가 아니며 다른 DataContext에서 로드될 수 있습니다. 이 작업은 지원되지 않습니다.
첨부는 7번째 상황(RowVersion 열 포함 및 외래 키 연결 없음)에서만 정상적으로 업데이트될 수 있습니다! 데이터베이스 기반 시스템에서는 이런 상황이 거의 불가능합니다! 이것은 어떤 종류의 API입니까?
요약 이제 진정하고 요약을 시작하겠습니다.
List 0과 같이 UI에서 직접 LINQ to SQL 코드를 작성하면 불행한 일이 발생하지 않습니다. 그러나 별도의 데이터 액세스 계층을 추상화하려고 하면 재난이 발생합니다. 이는 LINQ to SQL이 다중 계층 아키텍처 개발에 적합하지 않다는 것을 의미합니까? 많은 사람들이 LINQ to SQL이 소규모 시스템 개발에 적합하다고 말하지만, 크기가 작다고 해서 계층화되지 않는다는 의미는 아닙니다. 이렇게 많은 예외를 피할 수 있는 방법이 있나요?
이 기사는 실제로 몇 가지 단서를 제공했습니다. 이 시리즈의 다음 에세이에서는 모든 사람이 선택할 수 있는 몇 가지 솔루션을 제공하려고 노력할 것입니다.