본문 바로가기

Programing/닷넷

DataContext Step by step

백문이 불여일타라고 실제로 DataContext를 이용해서 LINQ를 이용해서 데이터를 조작하는 것을 정리해본다.

Visual Studio 2008에 있는 서버 탐색기 > 데이터 연결을 이용해서 ORM을 이용해 보겠다.


1. DB 구성

DB: SQL Server 2008 Standard를 사용합니다. 아래 스크립트로 샘플용 테이블을 생성한다.
주의해야 할 점은 테이블에는 기본키(Primary Key)가 꼭 있어야 한다는 것이다.
그렇지 않으면 Create, Update, Delete작업시에 아래와 같이 에러가 난다.
처리되지 않은 예외: System.InvalidOperationException: 'Table(TableEx)'에 기본 키가 없으므로 Create, Update 또는 Delete 작업을 수행할 수 없습니다.
   위치: System.Data.Linq.Table`1.CheckReadOnly()
   위치: System.Data.Linq.Table`1.InsertOnSubmit(TEntity entity)

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);

}

다시 수행을 해보면 아래와 같은 내용이 화면의 맨 아래에 나온다.
Insert record seq: 2

SELECT

여기까지 수행했다면 2개의 레코드가 들어가 있을 것이다. 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);

    }

}

참고로 위에서는 LINQ 표현식을 이용해서 SELECT를 했다. 쿼리식으로 바꾼다면
 var data = db.TableEx.Where(x => (x.Seq == 1)).Select(x => x);
 var data = from x in db.TableEx where x.Seq == 1 select x;
로 바뀌면 된다. 난 개인적으로 쿼리식이 좀더 보기 편한 것 같다. (두 문장 모두 동일한 IL코드를 만든다.)

위의 코드가 수행한 SQL을 로깅을 통해 보면 아래와 같다.
SELECT COUNT(*) AS [value] 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

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
쿼리를 보면 LINQ가 지연수행을 한다는 것을 알 수 있다. 지연수행이란 실제적으로 데이터가 필요할 때까지 쿼리를 실행하지 않는 다는 것이다. Select를 한 이후에 실제 Count()로 개수로 가져오고 나서야 SELECT COUNT(*) ... 이 수행됨을 알 수 있고, Single()로 데이터 하나를 가져왔다.

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