상세 컨텐츠

본문 제목

ASP.NET 3.0 - LINQ

Web Development/ASP.NET

by thankee 2008. 11. 27. 19:34

본문

LINQ는 SQL을 사용하는 데이터베이스 뿐만아니라 XML, DataSet, Object등 다양한 데이터 원본으로 부터 데이터를 입출력 할 수 있도록 해줍니다. 다른 데이터 원본을 연결하더라도 같은 문법을 사용하실 수 있습니다. LINQ는 ASP.NET에서만 사용할 수 있는 것이아니라, 커맨드 라인 프로그램, 윈도 어플리케이션 등 모든 .NET 프로그램에서 사용가능합니다.

LINQ는 향상된 디버깅, 리팩토링, Intellisense의 지원으로 안정적이고 빠른 개발이 가능하게 합니다. 트랜젝션, 뷰, 저장프로시저 외 다양한 새로운 기능들을 지원하며, 비지니스로직과 유효성 검사 코드의 분리를 통해 보다 깔끔하고 체계적인 코드작성이 가능합니다. 성능에 있어서도 동일하거나 더 빠른 속도를 보여주며, LINQ가 제공하는 향상된 기능과 방법들을 사용하면 더욱더 빠른 동작속도를 보장합니다.

LINQ to SQL 시작하기
LINQ to SQL Classes 추가

LINQ to SQL을 시작하기 위해서는 프로젝트에 LINQ to SQL Classes를 추가해야합니다. LINQ to SQL을 추가하면 dbml파일 및 기타 파일들이 시스템에 추가가 됩니다. 그리고 LINQ to SQL에 추가할 데이터베이스를 Server Explorer로 연결이 필요합니다.

LINQ to SQL을 이용한 모델링

LINQ to SQL은 데이터베이스를 쉽게 쉽각화 해주고, 모델링할 수 있도록 하는 LINQ to SQL Designer를 제공합니다.  디자이너 창에 Server Explorer로 나타나는 데이터베이스의 테이블을 끌어다 놓는 것 만으로도 테이블은 클래스화 되며, 각 테이블의 관계에 따라 Designer에 나타나는 클래스들의 관계도 화살표로 표현됩니다. 저장프로시저 또한 Designer페이지에 추가하여 사용할 수 있습니다. Designer페이지에 추가된 테이블 개체들은 Entity Class라고 하며 해당 테이블의 한 행(Row)를 담는 그릇과 같은 역할을 하게 됩니다. Entity Class의 속성들은 실제 DB의 테이블 컬럼과 동일하게 설정됩니다.

Custom Data Context Class

clip_image002[6] clip_image004

LINQ to SQL Designer페이지에 필요한 데이터베이스의 테이블을 끌어다 놓는 등, 모델링 작업이 끝나고 난 다음, SAVE버튼을 클릭하면 모델링한 모든 클래스와 관계, 저장프로시저들이 Custom Data Context Class로서 솔루션에 추가가 됩니다. 추가된 클래스명은 dbml파일명+DataContext이며 ClassView를 통해 추가된 클래스를 확인 하실 수 있습니다. 또한 dbml파일을 열어보면 선언된 클래스들과 이벤트들이 사용자가 쉽게 확장 가능한 partial 클래스와 메서드로 정의되어있습니다. 추가된 Entity 클래스를 통해서 실제 데이터베이스의 데이터를 참조하실 수 있습니다. 하지만 모든 데이터베이스의 테이블들이 Custom Data Context Class로 참조할 수 있는 것이 아니라, Designer페이지에 추가된 테이블들만 Custom Data Context Class을 이용해 참조할 수 있습니다.

Designer 페이지에 추가된 개체들의의 속성을 보시면 Data Source라는 항목이 있습니다. 이 항목의 속성값은 해당 Entity Class에 해당하는 DB상의 테이블 명입니다. 이 항목을 통해 Custom Data Context가 실제 데이터베이스의 테이블을 참조하게 됩니다.

Entity Class

clip_image002[8]

Designer페이지에 모델링된 각 테이블들 역시 각각 클래스화 됩니다. 이것을 Entity Class라고 부르며 테이블의 한 Row값을 담기위한 틀의 역할을 하게됩니다. 즉 Entity Class로 생성된 인스턴스는 테이블의 Row와 같습니다. Custom Data Context Class로 참조하는 DB상의 테이블도 사실은 이 Entity Class의 집합(컬렉션)입니다. Entity Class를 하나 생성한다는 것은 빈 Row를 생성한다는 것이며, 생성된 Entity Class의 인스턴스에 데이터를 할당하여 Data Context Class의 테이블에 추가한다는 것은, 실제 DB에 행을 추가하는 것입니다.

Entity Class의 Property들은 실제 데이터베이스 테이블의 컬럼들이 가지는 모든 특징을 가지게됩니다. 같은 데이터 타입과 제약조건이 각 Property에 적용되며, 따라서 사용자는 Entity Class에 데이터를 입력할 때 데이터 타입과 제약조건에 유의해야합니다.

Naming Classes

Entity Class와 Custom Data Context의 테이블 이름이 약간다르다는 것을 알 수 있습니다. Designer에 테이블들을 모델링할 때, Visual Studio는 DB상의 테이블명을 참조하여 Custom Data Context Class와 Entity Class를 등록하게 되는데, Entity Class는 단수로, Custom Data Context Class에는 복수형으로 이름이 등록됩니다. Region, Products 테이블을 Designer에 등록하면, Entity 클래스는 Region, Product가 되며 Custom Data Context Class에는 Regions, Products로 작명됩니다. Custom Data Context Class에 등록된 이름은 변경이 불가능하지만, Entity Class에 등록된 이름은 Designer 페이지에서 in-line으로 또는 속성창을 통해서 사용자가 수정이가능합니다.

LINQ to SQL에서 실제 DB의 테이블 이름과 Visual Studio에서 사용자에게 제공하는 이름이 다름으로서 발생하는 이익은 다음과 같습니다.

  1. 데이터베이스 종류나 개체들의 이름, 구조를 일부 변경해도 LINQ to SQL 소스코드에 영향을 미치지 않거나, 최소한의 수정만을 요구
  2. 개발에 부적절한 테이블명, 속성 명을 개발에 편한 이름으로 정하여 개발할 수 있음

Relationship Associations

LINQ to SQL에서는 데이터베이스의 기본키/외래키의 관계를 파악하여 자동으로 각 관계들을 처리합니다. Designer에서는 화살표로 부모가 되는 테이블(참조 당하는 테이블)에서 자식 테이블(참조하는 테이블)로 화살표의 방향이 설정되어 표시됩니다. 그리고 부모 테이블의 클래스에는  자신을 참조하는 자식 컬렉션이 추가(자신을 참조하는 자식들을 관리할 수 있음)되며, 자식 테이블의 클래스에는 부모테이블 테이블 클래스의 레퍼런스가 속성으로 추가됩니다.

만약 Designer가 생성한 이름이나 관계들이 마음에 들지 않는다면, 해당 관계의 속성을 통해 이름을 변경하거나 삭제, 수정이 가능합니다.

Delay/Lazy Loading
clip_image002[10]

LINQ to SQL은 각 테이블의 데이터를 사용할 때마다 읽는 것이 아니라, 한번에 미리 읽어서 처리 속도를 향상시킵니다. 하지만 사용자는 특정 컬럼의 데이터가 미리 읽혀질지, 아니면 필요할 때 읽혀질지 결정할 수 있습니다. 만약 이미지 데이터가 저장되는 컬럼이 있는 경우, 이 컬럼은 사용되는 순간 읽혀지도록 하는 것이 더 유리합니다.  따라서 해당 컬럼에 해당하는 속성을 클릭하고 Delay Loaded를 True로 설정함으로서 미리읽기를 끌 수 있습니다.

Stored Procedure
clip_image002[12]

저장프로시저를 LINQ to SQL에 추가함으로서 DataContext 상에서 메서드로서 사용할 수 있습니다. 저장 프로시저를 LINQ에 등록해서 사용하면, 먼저 DataContext를 통해 손쉽게 사용할 수 있다는 점과 엄격한 데이터 타입과 제약조건이 반영되며, 저장프로시저를 통해서 반환된 결과에 변경이 가해져도 LINQ에 의해 추적되고 SubmitChanges()에 의해 DB에 반영됩니다. 물론 LINQ에 정의된 Validation식을 거쳐 변화가 DB에 반영되게됩니다.

저장 프로시저를 Designer에 Drag and Drop할 때, Method Pane에 직접 추가할 경우 반환 데이터 형이 자동으로 결정되는 형태로 생성되며(Variant), 특정 Entity Class 위로 Drag and Drop 할 경우 반환 데이터형은 해당 Entity Class로 결정됩니다. 단, 저장프로시저가 해당 Entity Class의 모든 Property를 반환해야 가능합니다.

보통 LINQ 쿼리문을 이용하여 입력, 수정, 삭제를 할 경우 자동으로 시스템에서 SQL문을 생성하여 실행하게 되는데, 사용자가 지정한 저장 프로시저를 대신에 사용할 수 있습니다. Designer에서 Entity Class를 선택한 뒤, 속성창에서 Delete, Insert, Update의 "..."버튼을 클릭하면 자동으로 생성된 SQL문을 쓸 것인지, 사용자가 지정한 저장 프로시저를 쓸 것인지 결정할 수 있습니다.

SubmitChanges()

LINQ to SQL로 데이터의 일부를 변경하거나 추가했을 때, 그 변경사항은 메모리상에 저장되고 LINQ to SQL에 의해 추적되어 CustomDataContext.SubmitChanges()가 호출 될 때, DB에 반영됩니다. 모든 변경사항은 내부적인 연산을 거처 적절한 SQL문을 생성/실행하여 결과를 DB에 반영시킵니다. 물론 사용자가 데이터를 수정했더라도, 실제 DB의 데이터와 비교했을 때 차이가 없는 경우, 해당 구문은 실행되지 않습니다.

Paging

Skip(), Take() 함수를 사용하여 특정 개수 만큼의 컬럼을 건너뛰거나 특정 개수만큼의 컬럼을 가져 올 수 있습니다. LINQ to SQL은 실제 필요한 부분만을 DB로 부터 전송받기위해  해당 페이징 데이터가 사용될때 DB에 최적화된 SQL코드를 이용하여 데이터를 조회하게 됩니다.

dbNorthwindDataContext db = new dbNorthwindDataContext();
var pro = from p in db.Products
          where p.ProductName.Contains("Toy")
          select p;

db.Products.DeleteAllOnSubmit(pro);
db.SubmitChanges();
showProduct();
//Skip(), Take() 단독으로도 사용할 수 있음

MS Sql 2005을 사용하면  LINQ to SQL은 ROW_NUMBER() SQL 함수를 페이징에 사용하여 성능을 향상시킵니다.

/* MS-SQL Server에서 ROW_NUMBER()을 이용한 간단한 페이징 */
create procedure product_paging
    @startNum int,
    @endNum int
As
    SELECT *
    FROM
        (SELECT ProductName,ROW_NUMBER() OVER (order by ProductID) AS RowNumber
         FROM Products ) AS Product
    WHERE RowNumber BETWEEN @startNum AND @endNum --RowNum을 Between으로 제한
Go

/* 기존의 TOP-N을 이용한 페이징 처리 */
SELECT * --3. 역순으로 반환된 테이블 정렬
FROM (SELECT TOP takeNum * --2. 30~20행을 반환
           FROM (SELECT TOP (takeNum * pageNum) * --1. 3페이지면 1~30행을 반환
                      FROM products
                      ORDER BY 1 ASC) AS product
          ORDER BY 1 DESC) AS descendingProduct
ORDER BY 1 ASC

Enumerable

LINQ to SQL은 IEnumerable 인터페이스를 구현하고 있습니다. 단일 행은 Entity Class로 담게 되지만, 복수 행은 IEnummerable<Table> 컬렉션에 담게됩니다. 그리고 .NET 모든 서버컨트롤은 IEnumerable를 데이터바인딩에 지원하기에 LINQ to SQL의 결과를 서버컨트롤에 쉽게 바인딩 할 수 있습니다.

LINQ to SQL에 구현된 IEnumerable의 주요 멤버함수

- All : 모든 시퀀스가 해당 조건을 만족하는지 반환. All(p => p.UnitsInStock < 100)
- Any : 시퀀스 중 하나라도 조건을 만족하는지 반환. Any(p => p.UnitsInStock == 0)
- Average : 표현식으로 지정된 시퀀스의 평균을 반환
- Count : 시퀀스의 총 개수를 반환.
- Distinct : 시퀀스에서 중복을 제거한 총 개수를 반환
- Concat : 두 시퀀스를 하나로 합칩니다. Concat(NewProduct)
- Except : 두 시퀀스의 차집합을 구합니다. Except(Product2)
- First, Last ; 시퀀스의 첫 번째 혹은 마지막 요소를 반환합니다.
- Where : 사용자가 지정한 조건에 맞는 시퀀스 반환. Where(p => p.ProductId == 1)
- GroupBy : 시퀀스의 요소를 그룹화
- Max, Min : 시퀀스의 지정된 데이터 중 최대값/최소값 반환
- Select : 시퀀스의 요소를 새 폼에 투영. Select(p => p.UnitPrice * p.UnitsInStock)
- OrderBy, OrderByDescending : 시퀀스를 정렬.
- Range : 지정된 범위의 정수 시퀀스 반환. Range(1, 10);
- Reverse : 시퀀스를 반전
- Single : 조건에 맞는 단일 요소 반환. 조건의 결과가 다수일 경우 예외 발생.
- Sum : 시퀀스의 합. Sum(p => p.UnitPrice)
- Skip : 지정한 수 만큼 건너뛴 시퀀스 반환. Skip(10)
- Take : 지정한 수의 시퀀스 반환. Take(10)

LINQ to SQL Query

SELECT : LINQ to SQL을 이용하여 데이터 조회하기

  1. Lambda 식 사용 : 실제 SQL문의 실행 순서대로 입력.(Select문은 항상 마지막)
        dbNorthwindDataContext db = new dbNorthwindDataContext();
        var products = from p in db.Products
                              where p.Category.CategoryName == "Beverages"
                              orderby p.ProductID descending
                              select p;
  2. IEnumerable 멤버함수 사용하기
        dbNorthwindDataContext db = new dbNorthwindDataContext();
        var products = db.Products.Where(p => p.Category.CategoryName == "Beverages");

 

UPDATE : CustomDataContext의 인스턴스를 변경하고, SubmitChanges() 호출

dbNorthwindDataContext db = new dbNorthwindDataContext();
Product product = db.Products.Single(p => p.ProductName == "Toy 1"); 
product.UnitPrice = 99; 
product.UnitsInStock = 5;
db.SubmitChanges();

 

INSERT : EntityClass를 생성하고, 데이터를 입력한 다음 CustomDataContext의 인스턴스에 추가 또는 InsertOnSubmit(), InsertAllOnSubmit()함수를 이용하여 데이터 추가

  1. dbNorthwindDataContext db = new dbNorthwindDataContext(); //DB선언
    Category category = new Category(); //Entity Class 인스턴스 생성
    category.CategoryName = "Scott's Toys"//데이터 입력
    db.Categories.InsertOnSubmit(category); //Insert
    db.SubmitChanges(); //작업 저장

 

DELETE : 삭제할 데이터를 CustomDataContext에서 선택하여, DeleteAllOnSubmit나 DeleteOnSubmit()의 인자로 전달하여 데이터 삭제

dbNorthwindDataContext db = new dbNorthwindDataContext();
var products = db.Products.Where(p => p.Category.CategoryName == "Beverages"); 
db.Products.DeleteAllOnSubmit(products); //매개변수로 전달받은 Entity Classes를 삭제 
db.SubmitChanges();

 

Subset of the result : New 식을 이용하여 결과의 부분 집합을 구할 수 있습니다.

var product = from p in db.Products 
                     where p.Category.CategoryName == "Beverages"
                     select new
                     { 
                         ID = p.ProductID,
                         Name = p.ProductName, 
                         Orders = p.Order_Details.Count, 
                         Revenue = p.Order_Details.Sum(o => o.UnitPrice * o.Quantity) 
                     };

 

Primary Key/Foreign Key : 외래키 관계가 있는 Property에는 원하는 Entity Class의 인스턴스를 할당하면 됩니다. 할당된 Entity Class가 새롭게 만들어진 것이더라도 LINQ to SQL이 자동으로 DB에 반영합니다.

var product = from p in db.Products 
                      where p.Category.CategoryName == "Beverages"
                      select p;
//외래키에 기존의 Category 할당
product.Category = db.Category.Single(c => c.CategoryName == “Seafood”);
//외래키에 새로운 Category 할당
Category category = new Category(); //생성
category.CategoryName = “Software”;
product.Category=category; //할당

사용자 쿼리문 실행

- ExecuteCommand를 이용하여 사용자 DML문 수행 가능
- 영향을 받은 행의 숫자를 반환, 에러 발생시 -1 반환
- DML이 아닌 Select, Create, Drop, Alter 문 등 수행 불가.
db.ExecuteCommand("INSERT INTO products(productName) VALUES('EasyBiz')");

Querying Database

여러 행이 담겨있는 IQueryable<Table> 타입을 검색하는 방법은 다음과 같습니다.

IQueryable<Product> products = db.Products; 
foreach (Product p in products) 
       Response.Write(p.ProductName + "<br>");

Transactions

LINQ to SQL이 SQL문장을 실행 함에 있어서 Transaction을 적용하기 때문에, 모든 변경이 DB에 반영되거나 아니면 모두 반영되지 않습니다. .NET 2.0부터 사용가능한 TransactionScope를 이용, 사용자가 지정한 트랜젝션 범위에 LINQ to SQL이 포함될 수 있습니다. 즉 프로그래머가 지정한 트랜젝션을 사용하여 기존에 데이터 엑세스 코드와 LINQ to SQL간의 트랜젝션을 쉽게 통합할 수 있습니다.

//.NET 2.0에서 제공하는 트랜젝션

using(TransactionScope ts = new TransactionScope())
{
try
{
//각종 데이터 처리 구문
//LINQ 코드 기존의 Transaction에 포함되면, 해당 트렌젝션에 따라 동작합니다.
ts.Complete();
}
catch (DbException ex)
{
conn.Close();
ts.Dispose();
}
}

Validation
clip_image002[14]

개발에 있어서 코드 반복, 통합된 코드 작성은 피해야할 일입니다. LINQ to SQL 역시 유효성 검사 코드와 비지니스 로직을 분리하는 방법을 제공합니다.
  1. Base Validation : 데이터베이스의 테이블이 Designer에 모델링 될 때, 데이터베이스의 각 제약사항과 데이터타입이 모두 LINQ to SQL에 적용됩니다. 따라서 기본적인 데이터 형이나 제약조건(Unique, Nullable, identity 포함)에 대한 검사는 컴파일러에 의해서 자동적으로 이루어집니다. 이 제약조건은 Designer페이지에서 확인할 수 있으며, 사용자가 사용하길 원하지 않는 다면 삭제할 수 도 있습니다.
  2. SQL Injection : 유효성 검사를 하는 중요한 목적은 여러 불안정한 상황에서 벗어나기 위해서입니다. 널리 알려진 SQL Injection을 피하기 위해서 LINQ to SQL은 자동적으로 SQL에 저장될 데이터를 Escape 시킵니다.
  3. Property Validation : 테이블의 각 컬럼값이 변경 될 때마다 발생되는 이벤트를 사용자가 구현할 수 있습니다. 해당 dbml파일에 사용자가 재정의할 수 있는 이벤트들이 정의되어 있습니다. 이 이벤트를 다음과 같이 재정의하여 속성들 각각에 대한 유효성검사 코드를 추가하시면 됩니다. dbml파일안에있는 Partial로 작성된 해당 Entity클래스의 해당 Partial Method를 구현하면됩니다.
    public partial class Customer
    {
       partial void OnPhoneChanging(string value)
        {
            Regex phoeNumber = new Regex(@"0\d{2}-\d{3-4}-\d{4}$");

            if (phoeNumber.IsMatch(value) == false)
            {
                throw new Exception("유효한 연락처(전화/휴대폰)가 아닙니다.");
            }
        }
    }

  4. Entity Object Validation : 각 Entity의 속성 하나하나 검사하는 이벤트를 작성하기 번거롭다면, 다음과 같이 Entity의 유효성 검사 이벤트를 작성하시면 됩니다. dbml파일안에있는 Entity 클래스의 partial void OnValidate()를 구현하면됩니다.
    public partial class Customer
    {
        partial void OnValidate(System.Data.Linq.ChangeAction action)
        {
            Regex phoeNumber = new Regex(@"0\d{2}-\d{3-4}-\d{4}$");
            if (phoeNumber.IsMatch(Phone) == false)
            {
                throw new Exception("유효한 연락처(전화/휴대폰)가 아닙니다.");
            }
            if (phoeNumber.IsMatch(Fax) == false)
            {
                throw new Exception("유효한 팩스번호가 아닙니다.");
            }
        }
    }
  5. Entity Insert/Update/Delete Method Validation : 각 Insert, Update, Delete에 해당하는 이벤트가 Entity에서 발생하면 유효성 검사 코드가 실행되도록 할 수 있습니다. dbml파일에 있는 해당 DataContext클래스의 각 Partial 함수를 구현하면됩니다.

    public partial class NorthwindDataContext
    {
        partial void InsertCustomer(Customer instance)
        {
            this.ExecuteDynamicInsert(instance);
        }
       partial void UpdateCustomer(Customer instance)
        {
            this.ExecuteDynamicUpdate(instance);
        }
        partial void DeleteCustomer(Customer instance)
        {
            this.ExecuteDynamicDelete(instance);
        }
    }

Optimistic concurrency

LINQ to SQL은 동시성 제어를 위해서 Locking를 사용하지 않는 방법인 optimistic concurrency를 제공합니다. 다음과 같은 절차로 충돌을 처리하게 됩니다.

  1. DB서버로부터 값을 얻어 그 값을 클라이언트에 저장하여 그것을 통한 작업을 합니다.
  2. 클라이언트에서 해당 데이터를 통합 작업이 끝난 뒤, DB에 반영하기전에 해당 데이터가 DB에서 변경이 있었는지 검증 작업을 가집니다.
  3. 그 동안에 해당 데이터가 변경되었거나 변경 중에 있다면 충돌을 사용자에게 알립니다.
  4. 충돌 리스트에서 충돌을 확인하고, 충돌을 사용자에게 알려주고 사용자의 선택을 받거나, 작업을 취소하는 등의 처리를 하게됩니다.
  5. 충돌이 없었다면 정상적으로 작업이 커밋됩니다.

<asp:LinqDataSource>
clip_image002[16]

.NET에서 제공하는 여러 DataSource컨트롤과 같이 LINQ에 대해서도 DataSource컨트롤을 제공합니다. 이는 다른 컨트롤에 대한 LINQ의 바인딩을 보다 간단하게 합니다. SqlDataSource 등 다른 DataSource 컨트롤을 사용할 때 보다 얻을 수 있는 장점은, LINQ에서 제공하는 Validation 및 다른 장점들을 활용할 수 있다는 점이 되겠습니다.

실제 SQL쿼리문

clip_image002[18]

실제로 수행되는 SQL쿼리문을 확인하는 방법은 breakpoint를 걸고, 프로그램을 실행하여 디버그 모드로 들어가는 것입니다. 디버그 모드에서 해당 LINQ to SQL 표현식을 보면 실제 수행되는 쿼리문이 나타나는 것을 확인할 수 있습니다.

LINQ to SQL Visualizer

SQL문을 더욱더 효율적으로 확인하고 검사까지 할 수 있는 플러그인이 제공되는데, http://www.scottgu.com/blogposts/linqquery/SqlServerQueryVisualizer.zip에서 다운 받은 다음, 압축을 풀고 \bin\debug\ SqlServerQueryVisualizer.dll 파일을 \Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers\ 폴더에 넣고 비주얼 스튜디오를 재시작 하시면 됩니다. 디버거 모드에서 SQL문 확인 및 평가를 더욱 더 쉽게 할 수 있습니다.

관련글 더보기