2012년 3월 12일 월요일

NHibernate ICriterion 의 장점

NHibernate 에서도 LINQ 를 지원하고, 상당한 범위의 질의 방법을 제공합니다.
다만, 기존 ICriterion 을 이용할 수 밖에 없는 상황이 아직도 많습니다. 특히 HQL을 사용하는 것도 난감한 상황이 많습니다.

예전에는 HQL과 Criteria 질의 방식에 대한 비교에 대해 글을 썼었는데, 이제는 LINQ 방식까지 도입해서 비교해야 보았습니다.

결론부터 말씀드리면, “Lambda Expression을 이용하여 ICriteria 를 빌드해주는 QueryOver가 가장 종은 해법이다.” 라고 말씀드릴 수 있겠습니다. (제가 LINQ로 복잡한 쿼리를 작성하는 방식에 공부를 하지 않는 것도 있습니다)

다음 코드는 기간 정보 (시작~완료 시각) 이 있는 엔티티에 대해, 검색을 수행하고자 할 때 다음과 같은 방식을 수행해야 합니다.

1. Entity 의 시작시각, 완료시각
2. 검색조건의 시작시각, 완료시각 

이 4개의 조건 값이 NULL 을 가질 수 있고, 특히 검색 조건에서 기간이 없을 때는 모든 기간, 시작시각만 있는 경우는 시작시각 이후 모두, 완료시각만 있는 경우는 완료시작까지… 뭐 이런 조건을 만들어 낼 수 있습니다.

다음 코드는 대상 기간과 검색 기간의 겹치는 기간이 있는지를 알 수 있는 Criterion을 빌드합니다.

/// <summary>
///
주어진 기간이 오버랩되는지를 파악하는 Criterion
/// </summary>
public static ICriterion IsOverlapCriterion(this ITimePeriod period, string loPropertyName, string hiPropertyName)
{
period.
ShouldNotBeNull("range");
Guard.Assert(period.IsAnytime == false, @"기간이 설정되어 있지 않습니다. 상하한 값 모두 없으므로, 질의어를 만들 필요가 없습니다.");
loPropertyName.
ShouldNotBeWhiteSpace("loProperty");
hiPropertyName.
ShouldNotBeWhiteSpace("hiProperty");

if(IsDebugEnabled)
log.Debug("Build IsOverlapCriterion... range={0}, loPropertyName={1}, hiPropertyName={2}",
period, loPropertyName, hiPropertyName);

if(period.HasStart && period.HasEnd)
{
return Restrictions.Disjunction()
.
Add(period.Start.IsInRangeCriterion(loPropertyName, hiPropertyName))
.
Add(period.End.IsInRangeCriterion(loPropertyName, hiPropertyName))
.
Add(loPropertyName.IsBetweenCriterion(period.Start, period.End))
.
Add(hiPropertyName.IsBetweenCriterion(period.Start, period.End));
}

if(period.HasStart)
{
return Restrictions.Disjunction()
.
Add(period.Start.IsInRangeCriterion(loPropertyName, hiPropertyName))
.
Add(Restrictions.Ge(loPropertyName, period.Start))
.
Add(Restrictions.Ge(hiPropertyName, period.Start));
}

if(period.HasEnd)
{
return Restrictions.Disjunction()
.
Add(period.End.IsInRangeCriterion(loPropertyName, hiPropertyName))
.
Add(Restrictions.Le(loPropertyName, period.End))
.
Add(Restrictions.Le(hiPropertyName, period.End));
}

throw new InvalidOperationException("기간이 Overlap되는지 판단하는 Criterion을 생성하기 위한 조건이 맞지 않습니다.");
}


다음으로는 특정 엔티티의 값에 대해 BETWEEN 조건을 주는 방식입니다. 검색 값의 NULL 여부에 따라 달라집니다.



/// <summary>
///
Between (상하한을 포함하는 구간의 값을 구한다. 상하한에 대한 구간 검증은 하지 않는다!!!)
/// </summary>
/// <param name="propertyName">
속성명</param>
/// <param name="lo">
하한</param>
/// <param name="hi">
상한</param>
/// <returns></returns>
public static ICriterion IsBetweenCriterion(this string propertyName, object lo, object hi)
{
propertyName.
ShouldNotBeWhiteSpace("propertyName");

if(lo == null && hi == null)
throw new InvalidOperationException("Between 을 사용할 상하한 값 모두 null이면 안됩니다.");

if(IsDebugEnabled)
log.Debug("Between Criteria를 빌드합니다... propertyName=[{0}], lo=[{1}], hi=[{2}]", propertyName, lo, hi);

if(lo != null && hi != null)
return Restrictions.Between(propertyName, lo, hi);

// lo, hi 값 중 하나가 없다면
var result = Restrictions.Conjunction();

if(lo != null)
result.
Add(Restrictions.Ge(propertyName, lo));

if(hi != null)
result.
Add(Restrictions.Le(propertyName, hi));

return result;
}



다음은 BETWEEN 과는 반대로 특정 값이 엔티티의 두 개의 속성값의 범위 내에 있는지 파악하는 질의어를 빌드하는 메소드입니다.


/// <summary>
///
지정한 값이 두 속성의 값 범위 안에 있을 때 ( Between 의 반대 개념 )
/// </summary>
/// <param name="value"></param>
/// <param name="loPropertyName"></param>
/// <param name="hiPropertyName"></param>
/// <returns></returns>
public static ICriterion IsInRangeCriterion(this object value, string loPropertyName, string hiPropertyName)
{
value.
ShouldNotBeNull("value");

if(IsDebugEnabled)
log.Debug("지정한 값이 두 속성의 값 범위 안에 있을 검색 조건을 빌드합니다. " +
@"value={0}, loPropertyName={1}, hiPropertyName={2}", value, loPropertyName, hiPropertyName);

return Restrictions.Conjunction()
.
Add(Restrictions.Disjunction()
.
Add(Restrictions.IsNull(loPropertyName))
.
Add(Restrictions.Le(loPropertyName, value)))
.
Add(Restrictions.Disjunction()
.
Add(Restrictions.IsNull(hiPropertyName))
.
Add(Restrictions.Ge(hiPropertyName, value)));
}




자 보시다시피, 질의를 위한 값의 NULL 여부에 땨라 상당히 복잡한 쿼리를 만들어 낼 수 있습니다. 이 걸 일반 쿼리문 빌더나 HQL 로는 절대로 못할 겁니다. LINQ로는요? 흠.. 글쎄요… IQueryable<TEntity> 이므로 불가능하지는 않겠지만…



이걸 QueryOver 를 이용하도록 변경한다면, Magic String 도 제거가 가능합니다.



/// <summary>

///
주어진 기간이 오버랩되는지를 파악하는 질의어를 빌드합니다. (모든 구간은 폐쇄구간일 필요는 없고, 개방 구간이라도 상관없습니다.

/// </summary>

/// <typeparam name="T">
엔티티 수형</typeparam>

/// <param name="period">
검사할 시간 구간</param>

/// <param name="loExpr">
하한값을 나타내는 속성</param>

/// <param name="hiExpr">
상한값을 나타내는 속성</param>

/// <returns></returns>


public static ICriterion IsOverlapCriterion<T>(this ITimePeriod period, Expression<Func<T, object>> loExpr, Expression<Func<T, object>> hiExpr)

{


    period.
ShouldNotBeNull("period");

   
Guard.Assert(period.IsAnytime == false, @"기간이 설정되어 있지 않습니다. 상하한 값 모두 없으므로, 질의어를 만들 필요가 없습니다.");




   
var loPropertyName = RetrievePropertyName(loExpr);

   
var hiPropertyName = RetrievePropertyName(hiExpr);




   
return CriteriaTool.IsOverlapCriterion(period, loPropertyName, hiPropertyName);

}





최종 사용자가 사용할 구간 Overlap 검색 질의어 빌드 메소드입니다.



public static QueryOver<TRoot, TSub> AddIsOverlap<TRoot, TSub>(
this QueryOver<TRoot, TSub> queryOver,
ITimePeriod period,
Expression<Func<TSub, object>> loExpr,
Expression<Func<TSub, object>> hiExpr)
{
queryOver.
ShouldNotBeNull("queryOver");

queryOver.
UnderlyingCriteria.Add(IsOverlapCriterion(period, loExpr, hiExpr));
return queryOver;
}


뭐 별 거 아닌 거 같지만, HQL이나 LINQ 로는 유연하면서 복잡한 질의어 생성이 불가능하거나 힘들 때는 QueryOver 를 추천합니다.



다만 성능을 생각하는 루틴한 질의는 HQL로, 쉽고 빠른 구현을 원한다면 LINQ 로 하시기 바랍니다.

위의 QueryOver 관련은 Domain Layer 개발자만 알면 되고, 일반 사용자에게는 API 로 제공되는 부분이므로 굳이 모든 사람이 공부할 필요는 없지요.

댓글 없음: