백문이 불여일타라고 실제로 DataContext를 이용해서 LINQ를 이용해서 데이터를 조작하는 것을 정리해본다.
Visual Studio 2008에 있는 서버 탐색기 > 데이터 연결을 이용해서 ORM을 이용해 보겠다.
1. DB 구성
CREATE TABLE [dbo].[TableEx](
[Seq] [int] IDENTITY(1,1) NOT NULL,
[Col1] [varchar](50) NULL,
[Col2] [varchar](50) NULL,
[Col3] [varchar](50) NULL,
CONSTRAINT [PK_TableEx] PRIMARY KEY CLUSTERED (
[Seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]
생성된 테이블을 표로 나타내면 아래와 같다.
2. Visual Studio 프로젝트 생성
- Windows Form이던, Console이던 상관은 없지만 편의를 위해 Console 프로젝트를 생성한다.
파일 > 새로 만들기 > 프로젝트 > Visual C# > 콘솔 응용 프로그램
이름: HelloDataContext
위치: 본인이 편한데로.. 나는 그냥 Desktop을 선택했다.
솔루션용 디렉터리 만들기 : 체크해제 (솔루션에 프로젝트 하나만..)
3. 서버 탐색기 > 데이터 연결 생성
서버 탐색기 창의 데이터 연결에서 '연결 추가'를 이용해서 새 연결을 만든다.
이 창이 안보인다면, 보기 > 다른 창 > 서버 탐색기 를 선택해서 보이도록 한다.
데이터 소스 변경 창
데이터 소스: Microsoft SQL Server
데이터 공급자: .NET Framework Data Provider for SQL Server
아래와 비슷하게 입력하고 "연결 테스트"를 했을 때, "테스트 연결에 성공했습니다."라는 메시지가 나오면 성공이다.
확인을 누르면 아래와 같이 '서버 탐색기'에 연결이 된 것이 나온다.
4. LINQ to SQL 클래스 추가
프로젝트를 오른쪽 클릭해서, 추가 > 새 항목 > Visual C# 항목 > LINQ to SQL 클래스 를 선택 후,
이름을 TableEx.dbml 이라고 고치고 추가 버튼을 누른다.
아래와 같은 레이아웃이 된다.
5. 데이터 클래스 작성
바로 위의 그림처럼 서버 탐색기의 TableEx를 드래그해서 TableEx.dbml에 드롭을 한다.
연결 문자열에 대한 질문이 나오는데, 암호를 프로그램 구성 파일에 저장할 것이냐는 것이다. "예"를 선택한다.
- 참고로 연결 문자열 정보는 app.config와 Settings.settings 두 군데에 저장이 된다.
- 보안상 암호가 노출이 될 수도 있으므로 런타임시 암호를 입력 받는 것이 좋을 수도 있고, 암호화를 하는 것도 고려해 볼 만하다.
자동 생성된 코드를 보려면 TableEx.dbml의 하위의 TableEx.designer.cs를 오른쪽 클릭 > 코드보기를 선택하면 된다.
보면 TableExDataContext 라는 partial 클래스가 생겼음을 알 수 있다.
[System.Data.Linq.Mapping.DatabaseAttribute(Name="Test")]
public partial class TableExDataContext : System.Data.Linq.DataContext
이 클래스를 이용해 DB의 접근이 가능하다.
또한 데이블에 대한 클래스가 중간 정도 위치에 생겼다.
[Table(Name="dbo.TableEx")]
public partial class TableEx
6. 데이터 조작
기본적으로 만들어져 있는 Program.cs에 코드를 추가하도록 하겠다.
아래와 같이 using을 이용해서 TableExDataContext 인스턴스를 생성했다.
namespace HelloDataContext {
class Program {
static void Main(string[] args) {
using (var db = new TableExDataContext())
{
db.Log = Console.Out;
}
}
}
}
리소스 해지를 위해 자동적으로 Dispose가 호출되도록 하기 위해서 using 블록으로 구성했고,
DataContext의 내용을 알기 쉽도록 db.Log를 콘솔의 출력으로 지정을 하였다.
(개발을 위해서이고 실제로는 성능을 떨어 뜨릴 수 있으며로 로깅은 끄도록 하자)
INSERT
원래 SQL에서 가장 많이 하는 것은 SELECT이지만 데이터가 하나도 없기에 우선 넣어봐야 겠다.
using (var db = new TableExDataContext())
{
db.Log = Console.Out;
// 입력할 데이터
var record = new TableEx() {
Col1 = "data1", Col2 = "data2", Col3 = "data3",
};
db.TableEx.InsertOnSubmit(record);
db.SubmitChanges(); // 입력 반영
}
이게 끝이다! 수행하고 나면 레코드가 하나가 추가되었음을 알 수 있다.
Seq라는 필드가 있지만 이것은 자동입력 필드라 자동으로 DB에서 생성이 된다.
위에서 db.Log = Console.Out 라고 설정을 했는데, 콘솔에서는 수행한 쿼리들이 보인다.
INSERT INTO [dbo].[TableEx]([Col1], [Col2], [Col3]) VALUES (@p0, @p1, @p2)
SELECT CONVERT(Int,SCOPE_IDENTITY()) AS [value]
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data1]
-- @p1: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data2]
-- @p2: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data3]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
재미있는 것은 INSERT 뿐 아니라 SELECT까지 자동으로 수행됨을 알 수 있다.
SubmitChanges() 수행 이후에 record.Seq 값을 찍어보면 방금전 입력한 레코드의 Seq 필드의 값이 자동으로 갱신되었음을 알 수 있다
...
db.SubmitChanges(); // 입력 반영
Console.WriteLine("Insert record seq: {0}", record.Seq);
}
SELECT
using (var db = new TableExDataContext())
{
var data = db.TableEx.Where(x => (x.Seq == 1)).Select(x => x);
if (data.Count() > 0)
{
var record = data.Single();
Console.WriteLine("Seq:{0}\nCol1:{1}\nCol2:{2}\nCol3:{3}"
, record.Seq, record.Col1, record.Col2, record.Col3);
}
}
UPDATE
위에서 SELECT를 했으므로 SELECT를 한 레코드의 값을 수정해보자.
using (var db = new TableExDataContext())
{
var data = db.TableEx.Where(x => x.Seq == 1).Select(x => x);
if (data.Count() > 0)
{
var record = data.Single();
record.Col1 = "new data1";
record.Col2 = null;
db.SubmitChanges();
}
}
값을 바꾸고 SubmitChanges() 를 호출한 것이 전부이다!
쿼리는 아까 SELECT했을 때 두 번의 SELECT에 UPDATE하나가 추가가 되었다.
UPDATE [dbo].[TableEx] SET [Col1] = @p4, [Col2] = @p5
WHERE ([Seq] = @p0) AND ([Col1] = @p1) AND ([Col2] = @p2) AND ([Col3] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data1]
-- @p2: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data2]
-- @p3: Input VarChar (Size = 5; Prec = 0; Scale = 0) [data3]
-- @p4: Input VarChar (Size = 9; Prec = 0; Scale = 0) [new data1]
-- @p5: Input VarChar (Size = 0; Prec = 0; Scale = 0) [Null]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
데이터는 아래와 같이 바뀌었다.
ChangeConflictException 예외
주의할점!
INSERT할 경우에는 그럴일이 없겠지만, UPDATE일 경우에는 값을 SELECT하고 난 이후에 값이 바뀌었을 가능성이 있다.
LINQ에서는 이런 상황을 충돌이 발생했다고 하고, System.Data.Linq.ChangeConflictException 예외를 발생시킨다.
처리되지 않은 'System.Data.Linq.ChangeConflictException' 형식의 예외가 System.Data.Linq.dll에서 발생했습니다.
추가 정보: 행이 없거나 변경되었습니다.
아래와 같이 예외처리를 해주는 것을 미리 고려를 하는 것이 좋다.
// Change
try
{
db.SubmitChanges();
}
catch (System.Data.Linq.ChangeConflictException)
{
db.ChangeConflicts.ResolveAll(System.Data.Linq.RefreshMode.KeepChanges);
db.SubmitChanges();
}
참고로 RefreshMode 열거형은 아래와 같이 3가지 타입이 있다.
namespace System.Data.Linq
{
public enum RefreshMode
{
KeepCurrentValues = 0,
KeepChanges = 1,
OverwriteCurrentValues = 2,
}
}
최초 데이터가 아래와 같다고 하고,
LINQ에서 데이터(레코드)를 가져간 이후에, 외부 업데이트에 의해 Col1과 Col2가 'UPD'로 수정되었다고 하자.
LINQ에서는 업데이트하려는 컬럼은 Col2와 Col3이다. 참고로 충돌이 겹치는 부분은 Col2이다.
RefreshMode에 따른 결과는 아래와 같다.
1. KeepCurrentValues는 현재 데이터를 그대로 업데이트 시킨다.
SELECT [t0].[Seq], [t0].[Col1], [t0].[Col2], [t0].[Col3] FROM [dbo].[TableEx] AS [t0] WHERE [t0].[Seq] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
UPDATE [dbo].[TableEx] SET [Col1] = @p4, [Col2] = @p5, [Col3] = @p6
WHERE ([Seq] = @p0) AND ([Col1] = @p1) AND ([Col2] = @p2) AND ([Col3] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input VarChar (Size = 3; Prec = 0; Scale = 0) [UPD]
-- @p2: Input VarChar (Size = 3; Prec = 0; Scale = 0) [UPD]
-- @p3: Input VarChar (Size = 3; Prec = 0; Scale = 0) [ORI]
-- @p4: Input VarChar (Size = 3; Prec = 0; Scale = 0) [ORI]
-- @p5: Input VarChar (Size = 4; Prec = 0; Scale = 0) [LINQ]
-- @p6: Input VarChar (Size = 4; Prec = 0; Scale = 0) [LINQ]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
2. KeepChanges는 가져왔던 데이터에 현재 수정하려는 것을 업데이트를 한다.
SELECT [t0].[Seq], [t0].[Col1], [t0].[Col2], [t0].[Col3] FROM [dbo].[TableEx] AS [t0] WHERE [t0].[Seq] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
UPDATE [dbo].[TableEx] SET [Col2] = @p4, [Col3] = @p5
WHERE ([Seq] = @p0) AND ([Col1] = @p1) AND ([Col2] = @p2) AND ([Col3] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- @p1: Input VarChar (Size = 3; Prec = 0; Scale = 0) [UPD]
-- @p2: Input VarChar (Size = 3; Prec = 0; Scale = 0) [UPD]
-- @p3: Input VarChar (Size = 3; Prec = 0; Scale = 0) [ORI]
-- @p4: Input VarChar (Size = 4; Prec = 0; Scale = 0) [LINQ]
-- @p5: Input VarChar (Size = 4; Prec = 0; Scale = 0) [LINQ]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
3. OverwriteCurrentValues는 현재 바뀐 데이터를 현재 인스턴스에 덮어씌우는 동작을 한다. 결국 DB업데이트는 없다.
SELECT [t0].[Seq], [t0].[Col1], [t0].[Col2], [t0].[Col3] FROM [dbo].[TableEx] AS [t0] WHERE [t0].[Seq] = @p0
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [3]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 3.5.30729.5420
ChangeConflictException를 테스트 했던 코드이다.
using (var db = new TableExDataContext())
{
int seq = 0;
RefreshMode mode = (RefreshMode)seq;
seq++;
var data = db.TableEx.Where(x => (x.Seq == seq)).Select(x => x);
while (data.Count() > 0)
{
var record = data.Single();
record.Col2 = "LINQ";
record.Col3 = "LINQ";
// Wait
Console.WriteLine("Press Any key...");
Console.ReadKey(true);
// Change
try
{
db.SubmitChanges();
}
catch (ChangeConflictException)
{
Console.WriteLine("RefreshMode : {0}", mode);
db.Log = Console.Out;
db.ChangeConflicts.ResolveAll(mode);
db.SubmitChanges();
db.Log = null;
}
mode = (RefreshMode)seq;
seq++;
data = db.TableEx.Where(x => (x.Seq == seq)).Select(x => x);
}
}
SQL Server management studio에서는 아래의 쿼리를 수행했다.
1. 최초 테이블 초기화
update TableEx set col1='ORI',col2='ORI',col3='ORI';
2. 첫 번째 정지시
update TableEx set col1='UPD',col2='UPD' where seq=1;
3. 두 번째 정지시
update TableEx set col1='UPD',col2='UPD' where seq=2;
4. 세 번째 정지시
update TableEx set col1='UPD',col2='UPD' where seq=3;
그렇다면, update하기 전에 무조건 ResolveAll를 수행하는 것은?
상관없다. 데이터가 바뀌지 않았다면 ResolveAll()을 수행해도 SELECT를 수행하지 않는다.
'Programing > 닷넷' 카테고리의 다른 글
윈폼 다국어(i18n) 개발하기 - Best Practice (1) | 2013.01.09 |
---|---|
C#프로그래밍가이드 - 주석 (0) | 2013.01.08 |
자주쓰는 LINQ 정리 (0) | 2012.12.20 |
ADO.NET 데이터 프로바이더들(Data Providers) (0) | 2012.12.19 |
SQLite in LINQ (0) | 2012.12.13 |