티스토리 뷰
이번에는 쿼리를 이용하여 원격 개체 탐색을 하는 방법에 대해서 알아보겠습니다. 이 파트는 마치 LINQ To SQL 과 비슷하긴 하지만, 원격 개체라는 것의 대상을 SQL 서버에만 두는 것이 아니라는 것을 명심하셔야 합니다. 이번 예제는 SQL 서버를 이용하여 쿼리를 탐색하는 것이지만, 이 다음 파트인 LINQ To Naver Open API 를 보시면, 다양한 원격 개체에 접근 할 수 있다는 것을 알 수 있을 것입니다.
SampleContext 에 쿼리 Log 프로퍼티 추가
SampleContext 의 소스는 2회차의 소스와 똑같습니다. 다만, 원격 탐색을 하기 위해 질의식을 어떻게 만들었는지 알 수 있도록 Log 프로퍼티를 추가합니다. 원격 서버에 원하는 데이터를 가져올 수 있도록, 질의를 해야 하는데, 그것이 SQL 서버면, SQL 쿼리식이 될 것이고, 또는 WMI 통한다라고 하면, WMI 쿼리식이 될 것입니다.
public class SampleContext : IQueryable<Person>
{
// 생략…
public string Log
{
get { return this.provider.sbLog.ToString(); }
}
} |
SampleProvider 의 프로퍼티 추가와 Visitor 클래스 만들기
아래의 StringBuilder 는 쿼리식을 만들기 위한 객체입니다.
public class SampleProvider : IQueryProvider
{
// 생략…
public StringBuilder sbLog = new StringBuilder();
} |
그리고 IProvidor 의 Execute<T> 메서드의 내용을 변경하고자 합니다. 우선 테스트용으로 이렇게 작성하였고, 실제 원격 개체에 연결하기 위해 이후에 다시 이 메서드의 코드는 변경할 예정입니다.
public TResult Execute<TResult>(Expression expression)
{
var exp = expression as MethodCallExpression;
var func = (exp.Arguments[1] as UnaryExpression).Operand as Expression<Func<Person, bool>>;
var lambda = Expression.Lambda<Func<Person, bool>>(func.Body, func.Parameters[0]);
var r = context.DataSource.Where(lambda.Compile());
TranslateExpression trans = new TranslateExpression();
sbLog.Append( trans.Translate(expression) );
return (TResult)r.GetEnumerator();
} |
이전 소스와 비교해 보았을 때, 약간 틀린 점이 있습니다.
TranslateExpression trans = new TranslateExpression();
sbLog.Append( trans.Translate(expression) );
바로 이 부분인데, TranslateExpression 클래스는 C# 3.0 의 쿼리식을 실제 원격 서버에서 질의 할 수 있는 쿼리식으로 변경하는 클래스입니다. Translate() 메서드를 통해 LINQ 식을 텍스트로 변환하는 것입니다.
Visitor 패턴이란?
패턴을 구분할 때 Visitor 패턴은 행위 패턴에 속합니다. 간접적으로 클래스에 다형적인 기능을 추가합니다. 즉, 새로운 클래스를 많이 추가하기를 원치 안을 경우 선택하면 좋은 대안이 될 수 있습니다. |
TranslateExpression 클래스
소스코드를 전체로 보면 좋겠지만, LINQ 의 내부를 살펴보는 기초적인 포스팅이니, 메서드별로 구분하여 설명 드리고자 합니다.
TranslateExpression 클래스의 맴버와 생성자는 다음과 같습니다. StringBuilder 의 sb 맴버는 하나하나의 식을 분석하여 쿼리를 만들 것입니다. 그리고 생성자의 expression 은 LINQ 의 표현식이겠죠?
생성자
public class TranslateExpression
{
private StringBuilder sb = new StringBuilder();
public string Translate(Expression expression)
{
this.Visit(expression);
return sb.ToString();
}
} |
Visit 메서드
Visit 메서드는 이 클래스에서 상당히 중요한 부분입니다. 위에 말씀드린 Visitor 패턴을 구현하고 있지요. Visitor 패턴은 다른 패턴과 유사한 점이 많기 때문에, 이것은 Interpreter 패턴처럼 보일 수도 있고, 확장한다면 Composite 패턴과도 같아 보일 수 있습니다. 하지만 패턴은 코드의 내용 보다는 관점을 어떻게 보느냐가 더 중요합니다.
Expression 클래스는 ExpressionType 의 열거형 맴버를 가지고 있습니다. 현재의 Expression 이 어떤 표현식을 가지고 있는지 명확히 나타내고 있습니다. 적당히 쿼리가 만들어 질 수 있는 정도만을 구현하였기 때문에 JOIN 이나 GROUPING 기능은 수행할 수 없답니다.
protected Expression Visit(Expression expression)
{
switch (expression.NodeType)
{
case ExpressionType.Call:
return this.VisitCall((MethodCallExpression)expression);
case ExpressionType.Constant:
return this.VisitConstant((ConstantExpression)expression);
case ExpressionType.Lambda:
return this.VisitLambda((LambdaExpression)expression);
case ExpressionType.MemberAccess:
return this.VisitMember((MemberExpression)expression);
default:
throw new Exception( string.Format("{0} 은지원하지않습니다", expression.NodeType));
}
} |
VisitCall 메서드
이 메서드는 MethodCallExpression 표현식을 분석합니다. LINQ 식의 모든 C# 메서드는 이것의 대상이 되는 것입니다.
MethodCallExpression 의 메서드는 하나 이상의 상수나 변수를 포함하거나, 리턴 타입이 있어야 합니다. 즉, void 형의 C# 메서드는 LINQ 식에 포함이 될 수 없습니다.
protected virtual Expression VisitCall(MethodCallExpression mce)
{
switch (mce.Method.Name)
{
case "Where":
sb.Append("SELECT * FROM").Append( Environment.NewLine );
this.Visit(mce.Arguments[0]);
sb.Append(" AS T WHERE ");
UnaryExpression ue = mce.Arguments[1] as UnaryExpression;
LambdaExpression le = ue.Operand as LambdaExpression;
BinaryExpression be = le.Body as BinaryExpression;
if (be != null)
this.VisitBinary(be);
break;
case "StartsWith":
this.Visit(mce.Object);
sb.AppendFormat(" LIKE '{0}%'", mce.Arguments[0].ToString().Replace("\"",""));
break;
}
return mce;
} |
mce.Arguments[1]
를 디버깅 해보면,
{r => (((r.Age >= 20) && (r.Age <= 30)) && r.Name.StartsWith("엄"))}
위와 같이 마치 람다식과 같이 생겼습니다. 하지만, 이것의 NodeType 은 Lambda 가 아닌 Quote 입니다. Quote 는 상수값이 포함된 표현식입니다. 이것의 피연산자를 람다표현으로 바꾸는 구문이
UnaryExpression ue = mce.Arguments[1] as UnaryExpression;
LambdaExpression le = ue.Operand as LambdaExpression;
이렇게 되고, BinaryExpression 으로 다시 표현이 가능할 경우, BinaryVisit 을 수행하게 됩니다.
그리고,
case "StartsWith":
this.Visit(mce.Object);
sb.AppendFormat(" LIKE '{0}%'", mce.Arguments[0].ToString().Replace("\"",""));
break;
위 코드는 StartsWith 의 메서드를 SQL 쿼리식과 같이 LIKE ‘xxx%’ 처럼 바꾸는 역할을 하게 됩니다.
VisitLambda 메서드
VisitLambda 메서드는 LambdaExpression 을 분석합니다. LambdaExpression 은 Body 속성이 있으며, 이 Body 는 여러가지의 NodeType 이 올 수 있습니다. 현재 소스에서는 특별한 기능을 하지 않습니다.
protected virtual Expression VisitLambda(LambdaExpression le)
{
return le;
} |
VisitConstant 메서드
이 메서드는 ConstantExpression 을 분석합니다. 신기하게도 잘 살펴보면 이 ConstantExpression.Value 는 SampleContext 를 참조하고 있습니다. 이놈은 테이블을 참조 하고 있지만, Constant 로 가장하고 있습니다. 이 ConstantExpression 의 ElementType 을 가져와서 매핑되는 테이블로 변환해 줍니다.
protected virtual Expression VisitConstant(ConstantExpression ce)
{
IQueryable q = ce.Value as IQueryable;
if (q is IQueryable)
{
sb.AppendFormat(" ( SELECT * FROM {0} ) ", q.ElementType.Name)
.Append( Environment.NewLine );
}
else
{
sb.AppendFormat(" {0} ", ce.Value);
}
return ce;
} |
그래서 ConstantExpression 은 IQueryable 로 가장하고 있지 않을 경우, 일반적인 상수값으로 취급할 수 있습니다.
VisitMember 메서드
이 메서드는 MemberExpression 의 표현을 분석합니다. LINQ 쿼리식의 맴버는 모두 여기에 해당됩니다.
protected virtual Expression VisitMember(MemberExpression me)
{
sb.Append(me.Member.Name);
return me;
} |
예를 들어,
var result = from r in context
where r.Age >= 20 && r.Age <= 30 && r.Name.StartsWith("엄")
select r;
와 같은 식의 MethodCallExpression 은 Where 절이 될 것이고, 이곳의 람다 표현식으로 각각의 연산식을 분석해 보면,
각각의 BinaryExpression 은
r.Age >= 20
r.Age <= 30
r.Name.StartsWith("엄")
이 됩니다. 이 BinaryExpression.Left 는 r.Age 로 표현이 되지만, 우리가 변환할 쿼리식에서 “r.Age” 의 “r.” 은 필요가 없습니다. 때문에, MemberExpression 의 Member.Name 을 통해 “Age” 와 같이 오직 맴버 이름만을 표현하도록 하고 있습니다.
VisitBinary 메서드 ( Update 2008/03/27 - 오타 수정 )
이 메서드는 BinaryExpression 의 표현을 분석합니다. BinaryExpression 의 Left/Right 를 구현하고 있으며, 이 두 피연산자를 쪼개어내어 결합하는 기능을 하고 있습니다.
중요한 것은 BinaryExpressoin 의 Left/Right 는 그 안에 또 다른 BinaryExpression 을 포함할 수 있습니다.
var result = from r in context
where r.Age >= 20 && r.Age <= 30 && r.Name.StartsWith("엄")
select r;
와 같은 식의 BinaryExpression 은
be.Left = {((r.Age >= 20) && (r.Age <= 30))}
be.Right = {r.Name.StartsWith("엄")}
가 될 수 있습니다. 때문에, 위의 각각의 Left/Right 대한 VisitBinary 를 수행해야 합니다. 즉,재귀호출과도 같죠.
protected virtual Expression VisitBinary(BinaryExpression be)
{
if (be.Left is BinaryExpression)
this.VisitBinary((BinaryExpression)be.Left);
else
{
this.Visit(be.Left);
}
switch (be.NodeType)
{
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
case ExpressionType.LessThan :
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.And:
case ExpressionType.AndAlso:
sb.Append(" AND " );
break;
case ExpressionType.Or:
sb.Append(" OR ");
break;
default:
throw new Exception( string.Format("{0} 형식은지원하지않습니다", be.NodeType) );
}
if (be.Right is BinaryExpression)
this.VisitBinary((BinaryExpression)be.Right);
else
{
this.Visit(be.Right);
}
return be;
} |
BinaryExpression.NodeType 은 기본적인 덧셈(+),뺄셈(-) 외에도 &&, ||, 또는 비트연상 등의 연산도 포함될 수 있습니다. 특히, &&, || 와 같은 조건식을 AND,OR 로 변환해 주어야 할 필요가 있습니다.
한번 프로그램을 실행해 볼까요?
다음과 같은 코드입니다.
class Program
{
static void Main(string[] args)
{
SampleContext context = new SampleContext() ;
context.DataSource = new List<Person> {
new Person { Name="엄준일", Age=29},
new Person { Name="엄호희(내동생)", Age=26},
new Person { Name="엄혜진(울누나)", Age=31},
new Person { Name="멍멍이", Age=6},
new Person { Name="발발이", Age=5}
};
var result = from r in context
where r.Age >= 20 && r.Age <= 30 && r.Name.StartsWith("엄")
select r;
result.ToList().ForEach( o=>Console.WriteLine(o.Name ));
Console.WriteLine("----------------------------");
Console.WriteLine( context.Log );
} |
결과는 다음과 같습니다.
엄준일
엄호희(내동생)
----------------------------
SELECT * FROM
( SELECT * FROM Person ) AS T
WHERE Age >= 20 AND Age <= 30 AND Name LIKE '엄%'
계속하려면 아무 키나 누르십시오 . . . |
어떻습니까? 제법 쓸만한 QueryProvider 가 되었지요?
원격 서버에 연결
우리는 원격 서버를 MS-SQL 서버로 실습을 할 것입니다. 실습을 위해 테이블을 만들고, 테스트 데이터를 만들도록 하겠습니다.
Person 클래스와 같은 스키마와 실습 데이터를 넣었습니다.
CREATE TABLE Person
(
[Name] VARCHAR(50) NOT NULL,
[Age] INT NOT NULL
)
INSERT INTO Person(Name,Age) VALUES ('엄준일',29)
INSERT INTO Person(Name,Age) VALUES ('엄호희(내동생)',26)
INSERT INTO Person(Name,Age) VALUES ('엄혜진(울누나)',31)
INSERT INTO Person(Name,Age) VALUES ('멍멍이',6)
INSERT INTO Person(Name,Age) VALUES ('발발이',5) |
그리고 SampleContext 의 GetEnumerator<Person> 메서드를 다음과 같이 수정하였습니다.
public IEnumerator<Person> GetEnumerator()
{
provider.Execute<IEnumerator<Person>>(this.expression);
SqlConnection cn = new SqlConnection("Server=xxxx.kr;DataBase=xxxx;UID=xxxx;PWD=xxxx");
SqlCommand cm = new SqlCommand( Log, cn);
cm.CommandType = System.Data.CommandType.Text;
cn.Open();
Console.WriteLine("DataBase Connection!");
SqlDataReader reader = cm.ExecuteReader();
List<Person> list = new List<Person>();
while (reader.Read())
{
list.Add( new Person {
Name = reader["Name"].ToString(),
Age = Convert.ToInt32(reader["Age"])
});
}
reader.Close();
cn.Close();
return list.GetEnumerator();
} |
위와 같이 실제 데이터베이스를 연결하여 쿼리를 수행하도록 하였습니다.
결과는 다음과 같습니다.
DataBase Connection!
엄준일
엄호희(내동생)
----------------------------
SELECT * FROM ( SELECT * FROM Person ) AS T WHERE Age >= 20 AND Age <= 30 A
ND Name LIKE '엄%'
계속하려면아무키나누르십시오 . . . |
이 과정에서 변경된 소스는 특별히 자세한 설명이 필요 없을 것 같아서 첨부된 소스코드를 참고 하시기 바랍니다.
'.NET > C#' 카테고리의 다른 글
Language Server Protocol, OmniSharp-Roslyn 빌드 오류 해결 (0) | 2017.01.23 |
---|---|
Custom LINQ Provider - [5]. LINQ To Naver Open API (0) | 2008.03.30 |
Custom LINQ Provider - [3]. Custom LINQ Provider 만들기 (IQueryProvider) (0) | 2008.03.17 |
Custom LINQ Provider - [2]. Custom LINQ Provider 만들기 (IQueryable) (0) | 2008.03.13 |
Custom LINQ Provider - [1]. 소개 (1) | 2008.03.10 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
- ***** MY SOCIAL *****
- [SOCIAL] 페이스북
- [SOCIAL] 팀 블로그 트위터
- .
- ***** MY OPEN SOURCE *****
- [GITHUB] POWERUMC
- .
- ***** MY PUBLISH *****
- [MSDN] e-Book 백서
- .
- ***** MY TOOLS *****
- [VSX] VSGesture for VS2005,200…
- [VSX] VSGesture for VS2010,201…
- [VSX] Comment Helper for VS200…
- [VSX] VSExplorer for VS2005,20…
- [VSX] VSCmd for VS2005,2008
- .
- ***** MY FAVORITES *****
- MSDN 포럼
- MSDN 라이브러리
- Mono Project
- STEN
- 일본 ATMARKIT
- C++ 빌더 포럼
- .
TAG
- testing
- monodevelop
- 땡초
- POWERUMC
- github
- .NET
- mono
- Visual Studio
- MEF
- TFS 2010
- ASP.NET
- ALM
- 엄준일
- Visual Studio 2008
- umc
- Team Foundation Server 2010
- 비주얼 스튜디오
- Team Foundation Server
- 팀 파운데이션 서버
- test
- Visual Studio 2010
- LINQ
- Managed Extensibility Framework
- Visual Studio 11
- c#
- 비주얼 스튜디오 2010
- Silverlight
- Windows 8
- TFS
- .NET Framework 4.0