레이블이 NHibernate인 게시물을 표시합니다. 모든 게시물 표시
레이블이 NHibernate인 게시물을 표시합니다. 모든 게시물 표시

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 로 제공되는 부분이므로 굳이 모든 사람이 공부할 필요는 없지요.

2012년 2월 16일 목요일

ORM (Object Relational Mapping) 을 왜 쓸까?

NHibernate 를 어언 4년 가까이 공부하고, 사용하면서, 처음 접했을 때의 매력(?) 에 대해 곰곰히 생각해 보게 된다. 지금이야 뭐 당연히 ORM을 쓰는게 너무나 당연하고, 왠만한 경우에도 ORM을 사용하고자 하는 관성에 젖어버려… 처음 ORM을 접하는 사람들에게 “ORM 을 공부하시고, 써 보세요. 신세계가 열립니다” 라고 말하는 것 조차 무의미하게 느껴지게 되었습니다. – 매너리즘에 젖어버렸나요?

어쨌든 다시 한번 생각해 봅시다. 아니 처음 접하시는 분들도 왜 남들이 ORM 에 많은 관심을 가지고, 개발하고, 사용하려고 하는지 생각해 봅시다.

여기 글 중에는 Microsoft 계열 즉 .NET 계열 중심으로 설명할 생각입니다. 주제가 NHibnerate 위주로 설명할 것이고, 제 경험도 Microsoft 계열에 많이 치중되어 있었기에 설명하기가 더 편합니다.

본론으로 들어가서, ORM (Object Relational Mapping) 을 쓰는 이유는 뭘까요?  우선 ORM 정의를 보면 쉽게 알 수도 있겠죠?

Object-relational mapping' (ORM, O/RM, and O/R mapping) in computer software is a programming technique for converting data between incompatible type systems in object-oriented programming languages. This creates, in effect, a "virtual object database" that can be used from within the programming language. There are both free and commercial packages available that perform object-relational mapping, although some programmers opt to create their own ORM tools.”

대강 해석하면 “OOP 언어와 데이터를 다루는 RDBMS 와의 상이한 시스템을 매핑하여, 쉽게 데이터 관련 OOP 프로그래밍을 쉽게 하도록 하기 위한 기술이다” 라고 할 수 있겠습니다.

아시다시피 OOP의 class 구조 와 object-graph 는 RDBMS의 table 구조와 relation 으로 표현이 가능합니다. 다만, 이 두 가지 표현 방식에는 상당한 차이점이 있어 매핑을 통해 변환을 해주어야 한다는 점입니다.

즉 서로 다른 두 지역에서 정보 처리 방식이 전혀 다른데, 이를 중간에서 소통하게 해주는 통역기가 있다면, 둘 다 알 필요 없이, 한 지역의 정보 처리 방식만을 알면 됩니다.

그동안 OOP 언어의 현장에서의 많은 실패의 원인이 이런 데이터를 다루는 부분의 생산성의 문제에 기인한 경우가 많았습니다.

그럼 ORM 이 이렇게 통역기 역할을 해주면 뭐가 좋아지는가? 결론부터 말하면, 개발자가 OOPCBD에 의한 개발에만 집중할 수 있고, RDBMS 관련 부분은 아주 쬐금만 고려하면 된다는 점입니다.

.NET 계열의 대표적인 ORM 툴인 NHibernate 의 개발 모토에도 개발에서“95%의 Database 작업을 제거하는 것이다.” 라고 했습니다. 

이 말은 ORM은 OOP 언어나 CBD 개발 방법론에서 Class 나 Component 설계 및 개발에서 이질적인 RDBMS 와 관련된 부분을 최소화하고, 원 제품의 로직 구현에 충실하고자 하는 의도에서 나온 산물이다 라고 말할 수 있습니다.

그럼 실제 ORM 툴을 도입하여 제품을 개발 시에, 개발자들이 어떻게 변화하는 가를 보게 되면, DataSet과 DataTable,  SQL 문을 중심으로 한 Data 와 ASP.NET Control 이나 HTML, JavaScript의 Presentation Layer 만 있는 단순 어플리케이션이 IList<Entity> 와 Repository, Validator, Interceptor 등 OOP의 다양한 기술을 동반한 한층 업그래이드된 제품으로 격상하게 됨을 느낄 수 있습니다.

특히나 RDBMS 를 전혀 몰라도 웹 개발자가 원하는 개발을 수행할 수 있다는 의미는 Data Layer와 Presentation Layer 를 자연스럽게 분리해주고, Role을 명확히 하고, 단위테스트 등 분산 개발이 가능하게 해 주는 부가 혜택도 있습니다. (물론 이 경우는 iBATIS, Dapper 같은 SQL Mapping Tool 도 제공하는 기능입니다)

자 어느정도 ORM을 왜 쓰는지에 대한 제 의견에 공감을 하시는지요? 땡기시는지요?
만약 아직 땡기시지 않는다면 앞으로 구체적으로 NHibernate 의 예를 들어가면서 ORM 의 세계에 빠져보시기 바랍니다.

물론 제가 NHibernate 를 아주 잘하는 건 아니니, 다른 견해에 대해서는 기탄없이 말씀해주시기 바랍니다. 강호에는 숨은 고수가 많으신데, 잘 나타나시질 않더군요…

2012년 2월 7일 화요일

NHibernate DTO를 AutoMapper로 구현하기

NHibernate 엔티티를 외부에 서비스하기 위해서는 전통적 방식 중에 하나가 DTO (Data Transfer Object) 패턴을 사용하는 것입니다. 물론 다른 ORM도 마찮가지구요.
다만 문제는 NHibernate 의 Association 이 유지되느냐의 문제인데, 유지를 원할 수도 있고, 안 원할 수도 있습니다.


Entity Framework 처럼 WCF DataService를 이용하면, Domain Layer의 모든 정보를 간단하게 외부에 서비스 (노출) 할 수 있습니다. 정말 환상이긴 합니다만, OOP 의 객체 안정성을 위해, 대부분의 정보를 은폐하는 것을 추천하듯이, 있는 그대로 노출하는 것보다 DTO 패턴을 사용하여, 원하는 내용만 서비스(노출) 하는 것을 더 추천하고 싶습니다.

이 예를 DB 간의 Data 공유 시에 Table 들을 OPEN 해주는 것과 VIEW 를 만들어서 제공하는 것은 다른 의미가 있습니다. 실제 Data를 쓸 수 없다하더라도, 구조를 알게되면 보안에 문제가 생기겠죠.
또 한가지 문제점은 서비스마다 VIEW 를 만든다던가, 공통의 큰 VIEW 하나를 만든다면, 유지보수나 성능상에 문제가 큽니다.

여기 NHibernate 엔티티를 DTO로 만들 때, 단순 엔티티가 아니라 Association 된 부분도 같이 자동으로 Mapping 해주는 기능을 가진 AutoMapper 를 사용해 봅시다.

원문 : AutoMapper 의 Nested Mappings

원문을 보시면 아 하시겠지만, NHIbernate의 Lazy Loading 을 위한 Proxy에 대해서도 과연 될까요? 그럼 함 해보지요^^

 

우선 NHibernate 엔티티로서 Parent-Child 를 구성했습니다.

[Serializable]
public class Parent : DataEntityBase<Guid>, IParent
{
public Parent()
{
Id = Guid.NewGuid();
Version = -1;
}

public virtual int Version { get; set; }
public virtual int Age { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }

private IList<Child> _children;

public virtual IList<Child> Children
{
get { return _children ?? (_children = new List<Child>()); }
protected set { _children = value; }
}

public override int GetHashCode()
{
if(IsSaved)
return base.GetHashCode();

return HashTool.Compute(Id, Version);
}
}


[Serializable]
public class Child : DataEntityBase<Guid>
{
public Child()
{
Id = Guid.NewGuid();
Version = -1;
Name = "Some Child";
}
public Child(string name, Parent parent):this()
{
Name = name;
Parent = parent;
}
public Child(Guid id, string name, Parent parent): this(name, parent)
{
Id = id;
}

public virtual int Version { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual Parent Parent { get; set; }

public override int GetHashCode()
{
return IsSaved ? base.GetHashCode() : HashTool.Compute(Parent, Name);
}
}


============================================================



자 그럼 Parent – Child 에 대응되는 DTO 들을 정의해보면,



[Serializable]
public class ParentDTO : ValueObjectBase
{
public ParentDTO(string name, int age)
{
Name = name;
Age = age;
}

public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }

private IList<ChildDTO> _children;
public IList<ChildDTO> Children
{
get { return _children ?? (_children = new List<ChildDTO>()); }
set { _children = value; }
}

public override int GetHashCode()
{
return HashTool.Compute(Name);
}
public override string ToString()
{
return string.Format("ParentDTO# Id=[{0}], Name=[{1}], Age=[{2}]", Id, Name, Age);
}
}



[Serializable]
public class ChildDTO : ValueObjectBase
{
public Guid Id { get; set; }
public int Version { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ParentDTO Parent { get; set; }

public override int GetHashCode()
{
return HashTool.Compute(Id);
}
public override string ToString()
{
return string.Format("ChildDTO# Id=[{0}], Name=[{1}], Description=[{2}], Parent=[{3}]", Id, Name, Description, Parent);
}
}


 



두 매핑 클래스들이 유사하지요? 다만 NHibernate 엔티티들은 DataEntityBase<T>  라는  NHIbernate용 클래스를 상속 받고, DTO 들은 일반 Persistent Object를 나타내는 ValueObjectBase 클래스를 상속받습니다. 그리고 둘 간의 매핑 정보는 양방향 관계를 가지도록 했습니다.



자 그럼 이제 Mapping 이 제대로 되는지 확인해 봅시다.



/// <summary>
///
AutoMapper 를 이용하여, Association 을 가진 엔티티도 DTO로 매핑이 가능합니다.
/// 즉 Parent-Child 를 ParentDTO-ChildDTO 로 매핑이 가능합니다.
/// </summary>
[TestFixture]
public class DtoMappingFixture : NHRepositoryTestFixtureBase
{
#region << logger >>

private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();

#endregion

protected override void
OnTestFixtureSetUp()
{
base.OnTestFixtureSetUp();

Mapper.CreateMap<Parent, ParentDTO>();
Mapper.CreateMap<Child, ChildDTO>();
Mapper.AssertConfigurationIsValid();
}

[Test]
public void MapToDto()
{
UnitOfWork.CurrentSession.Clear();

var parent = Repository<Parent>.Get(parentsInDB[0].Id);

Assert.IsNotNull(parent);
Assert.AreEqual(parentsInDB[0].Name, parent.Name);
Assert.AreEqual(2, parentsInDB[0].Children.Count);
Assert.AreEqual(2, parent.Children.Count);

var parentDTO = Mapper.Map<Parent, ParentDTO>(parent);
Assert.IsNotNull(parentDTO);
Assert.AreEqual(parentsInDB[0].Name, parentDTO.Name);
Assert.AreEqual(2, parentDTO.Children.Count);

foreach(var childDto in parentDTO.Children)
Console.WriteLine(childDto);
}
}




보시다시피 NUnit 을 이용하여 테스트 코드를 작성하였습니다.



우선 테스트 시작 시에 OnTestFixtureSetUp()에서 CreateMap 을 이용하여 각각 Parent – ParentDTO, Child – ChildDTO 에 대한 매핑을 정의하고, 매핑이 유효한지 확인 합니다.



이제 엔티티를 로드하고, Mapper.Map 을 통해 Parent 엔티티를 매핑해서 ParentDTO 를 만들었습니다. 단지 그것만 했는데, parentDTO.Children 에 값이 제대로 들어오는군요.



이제 DTO를 이용한 외부 서비스가 상당히 간단해질 것입니다. 물론 WCF DataService용으로 NHibernateDataContext를 만들어도 되지만, 전 DTO 방식이 노가다가 좀 더 들어가서 고생은 되지만, 유연성이나 보안등에서 더 땡기네요^^



울 회사 PMS 팀에서 이거 보면 뒤집어지겠군요^^



PS. Assertion을 해야 하는데, 어제 밤에 갑자기 해보는 바람에 Console.WriteLine 을 써 버렸네요… 여러분은 Assertion을 하세요. 꼭!!!

2012년 1월 22일 일요일

Facebook에 NHibernate Group을 만들었습니다.

안녕하세요^^ 블로그로도 NHibernate 정보를 제공하기도 했는데, Facebook에 Group 기능이 있더군요. 이를 이용해서 Facebook에 NHibernate 그룹을 만들었습니다.

http://www.facebook.com/#!/groups/nhibernate.korea/

제가 관리하고 있고요, 신청하시는 분들은 모두 가입되도록 하겠습니다. 관심 있으신 분은 참여해주기 바랍니다.

한가지 더 – Google+ 에도 NHibernate Korea 서클을 만들었습니다.
둘 중 어떤 서비스가 더 좋을 지 몰라 우선 둘 다 만들어서 써보면서, 좋은 쪽으로 단일화해야 할 것 같습니다.

2011년 10월 25일 화요일

NHibernate 학습 및 개발 속도 높이기

저도 처음 NHibernate를 접했을 때, HBM 작성하는데, 상당히 많은 시간을 투자해야 했고, 많은 시행착오를 겪었습니다. 특히, NHibernate 공식 자료에도, Class – HBM – Table 이런 식의 전체 정보가 존재하지 않아 애를 많이 먹었죠.

그래서 시작한 것이, NHibernate 소스의 테스트 코드를 보기 시작했습니다. 물론 실행도 해보고요. 이 때 제가 “뭔가 잘못하고 있구나” 를 깨닳았습니다. 테스트 코드를 보면, DB는 미리 만들어야 하지만, hbm2ddl 을 이용하여, DB Schema 생성용 script가 생성되고, 이를 DB에 실제 적용해서, 테스트를 위한 테이블을 모두 만든 후 테스트를 수행하더군요.

이 것을 알기 전에는 매핑 정보 하나 바꿀 때마다 DB도 바꾸고, 클래스도 바꾸고, 참 무식한 짓을 했습니다.
이제는 아예, NHibernate 테스트 프로젝트를 뜯어보다가, 하나의 DB에 대해서 테스트용 Schema 를 생성하는 것이 아니라, 설정에 따라, 다양한 DB에 대해서 테스트 할 수 있는 테스트용 Framework을 갖추게 되었습니다.

이 후 제게는 큰 변화가 일어났습니다. 그 동안 시도할 생각조차 못해왔던 복잡한 매핑이나, 개념이 잡히지 않던 매핑 속성 값에 대해, 여러 개의 매핑을 제작하여, 상호 비교할 수 있는 그런 체계가 제게 주어진 것이죠.

이렇게 되니, NHibernate에 대한 학습속도가 가파르게 오르더군요. 더군다나, IUserType, Interceptor, DynamicProxy, Listener, 2nd Cache Provider 등을 직접 제작하고, 테스트 하면서, 내부 구조를 빠르게 이해할 수 있게 되어, 이제는 왠만한 경우에는 자체 해결할 정도가 되었습니다.

그럼, 이제 막 NHibernate 를 시작하거나, 너무 힘들어서 포기하신 분들이라면, 다음과 같이 해보시기 바랍니다.

  1. NHibernate Test 프로젝트의 테스트 구조 파악 및 DB 생성 (hbm2ddl 활용) 방법을 파악
  2. 위의 테스트 방식으로 자신만의 Test Framework을 만들 것
  3. 다양한 매핑 방식에 대해 테스트 해 볼 것.
  4. 가능하면, FluentNHibernatePersistenceSpecification 을 활용할 것 (Fluent 방식의 매핑이 아니더라도 가능)
  5. DDD (Domain Driven Develoment), ORM 관련 지식을 쌓을 것
  6. Proxy에 대한 개념을 잡을 것 ( CastleProjectDynamicProxy 추천)

결론적으로 자신만의 Test Framework을 만들지 않고, NHibernate 를 습득하기는 상당히 힘듭니다.
남이 만든 것을 약간 고쳐서 사용해도 무방할 것입니다. 이런 예는 구글링으로 찾아보시면 될 듯 합니다.

2011년 10월 16일 일요일

FluentNHibernate 을 이용한 IUserType 매핑

NHibernate의 기능 중에 가장 큰 장점 중에 하나가 IUserType, ICompositeUserType 을 이용하여, RDBMS 저장소의 컬럼과 속성을 1:1 매핑이라던지, Component 로 직접 매핑이 아닌, 무언가 2차적인 처리를 수행할 수 있는 장치를 두었다는 것입니다.

제가 가장 많이 쓰는 IUserType

  1. 암호화, 압축 등을 수행하여 저장소에 저장하고, 로드 시에는 원본 데이터로 복원하여 제공해 주고, WHERE 절도 자동으로 되는 그런 방식
  2. 여러 속성을 가지는 class 나 struct 를 하나의 Property 처럼 사용할 수 있도록 해주는 ICompositeUserType 을 이용하는 것읍니다. 에를 들면, 주차 (WeekOfYear)는 Year 와 WeekOfYear 두 값이 같이 저장되어야 하는데, 따로 작업하면 안되므려 YearAndWeek 라는 struct 로 정의하여 사용하고, 기간을 나타내는 TimeRange 같은 경우에는 StartTime, EndTime 을 항상 같이 가지고 다녀야 하기 때문에, Component 로 표현하기 보다 ICompositeUserType으로 표현하면 훨씬 좋습니다.

자 그럼 IUserType 으로 NHiberante용 사용자 정의 수형을 만드는 것은 여기 설명을 참고하는 것으로 하고, 이런 사용자 수형을 FluentNHibernate 으로 매핑하는 방법에 대해 설명 드리겠습니다.

우선 사용자 정의 수형을 가지는 엔티티 정의를 보시면,

[Serializable]
public class FUserTypeEntity : DataEntityBase<Int32>
{
public virtual string Name { get; set; }
public virtual string Password { get; set; }
public virtual string Password2 { get; set; }

public virtual string CompressedString { get; set; }

public virtual byte[] CompressedBlob { get; set; }

private TimeRange _activePeriod;

public virtual TimeRange ActivePeriod
{
get { return _activePeriod ?? (_activePeriod = new TimeRange()); }
set { _activePeriod = value; }
}

public virtual YearAndWeek ActiveYearWeek { get; set; }

public virtual Type LanguageType { get; set; }

public virtual DateTime? UpdateTimestamp { get; set; }

public override int GetHashCode()
{
if(IsSaved)
return base.GetHashCode();

return HashTool.Compute(Name, Password);
}
}


과 같습니다. 보시면, CompressedString, CompressedBlob는 IUserType 을 구현한 문자열이나 byte[] 을 압축하여 문자열로 저장하는 UserType 으로 매핑됩니다. 다음으로   ActivePeriod 나   ActiveYearWeek 는 ICompositeUserType 을 구현했습니다. ActivePeriod는 StartTime, EndTime 이라는 속성을 가지고 있는 class 이고, YearAndWeek 는 Year 와 WeekOfYear 값을 가지는 struct 입니다.



이제 Fluent 방식으로 FUserTypeEntity 를 매핑을 하면, 다음과 같습니다.



public class FUserTypeEntityMapping : ClassMap<FUserTypeEntity>
{
public FUserTypeEntityMapping()
{
Table("FUserTypeEntity");
DynamicInsert();
DynamicUpdate();
LazyLoad();

Id(x => x.Id).GeneratedBy.Native();

Map(x => x.Name);

Map(x => x.Password)
.CustomType<RijndaelEncryptStringUserType>()
.Length(MappingContext.MAX_ANSI_STRING_LENGTH_SQL_SERVER);

Map(x => x.Password2)
.CustomType<AriaEncryptStringUserType>()
.Length(MappingContext.MAX_ANSI_STRING_LENGTH_SQL_SERVER);

Map(x => x.CompressedString)
.CustomType<GZipStringUserType>()
.Length(MappingContext.MAX_ANSI_STRING_LENGTH_SQL_SERVER);

Map(x => x.CompressedBlob)
.CustomType<SevenZipBlobUserType>()
.Length(MappingContext.MAX_ANSI_STRING_LENGTH_SQL_SERVER);

Map(x => x.ActivePeriod)
.CustomType<TimeRangeUserType>()
.Columns.Clear()
.Columns.Add("ACTIVE_FROM_DATE")
.Columns.Add("ACTIVE_TO_DATE");

Map(x => x.ActiveYearWeek)
.CustomType<YearAndWeekUserType>()
.Columns.Clear()
.Columns.Add("ACTIVE_YEAR")
.Columns.Add("ACTIVE_WEEK");

Map(x => x.UpdateTimestamp).CustomType("Timestamp");
}
}



우선, Password, Password2 를 보시게 되면,  둘 다 암호화를 해서 저장하는 UserType 을 지정하고, 길이를 CLOB나 VARCHAR(MAX)를 할 수 있도록 합니다.  RijndaelEncryptStringUserType 은 미국 암호화 AES 규격을 따른 것이고,   AriaEncryptStringUserType 은 한국 암호화 규격을 따른 것입니다.



다음으로 압축 저장을 수행해주는 CompressString, CompressedBlob 를 보시면, GZip 으로 압축하던가, 7Zip 알고리즘으로 압축하여 저장해 줍니다. 물론, 로드 시에는 압축을 풀어서 엔티티에서는 Plain Text 나 Byte가 됩니다.



위 두 가지 방식은 모두 IUserType 을 구현한 Custom UserType 이므로, 속성:컬럼이 1:1입니다. 그러므로 CustomType<XxxxUserType>() 을 지정해 주면 알아서 됩니다. 물론 컬럼 명을 변경하고자 한다면,  .Column(“xxxx”) 를 사용하면 되겠지요^^



그럼 이번에는 ICompositeUserType을 사용한 속성에 대한 매핑을 보시죠. 위의 예에서는 ActivePeriod, ActiveYearWeek 입니다. ActivePeriod 는 컬럼 명에서도 유추할 수 있듯이, 기간을 나타냅니다. TimeRangeUserType 으로 수형을 지정하고, 자동으로 매핑되는 컬럼 명을 무시하고 (Columns.Clear()) 순서대로 컬럼 명을 새로 지정했습니다. (한 엔티티에 여러 개의 TimeRangeUserType 이 존재할 수 있으니까요)



FluentNHibernate 1.3 에서는 ICompositeUserType에 대한 매핑 방식이 달라졌지만, 그래도 아직은 FNH-1.2 가 주류이므로, 이렇게 작성했습니다.

FluentNHibernate 으로 CustomType 에 대한 매핑에 대한 설명이 별로 없는 것 같아 작성해 봤습니다…

2011년 10월 14일 금요일

NHibernate Code Mapping 비교 – Join

NHibernate 사용 시 매핑은 필수이고, 그 동안 HBM 만을 사용했었습니다.
이유는 없고, 많은 참고 자료와 익숙해져 있어서였습니다만, 회사 팀원들이 이제 NHibernate 자체에 많이 익숙해져서, 새로운 시도를 해 봤습니다.
물론 그 시도라는 것은 FluentNHibernate 를 이용한 Code Mapping 입니다.
FluentNHibernate 소개를 보시면 알겠지만, 가장 큰 특징은 Code로 Mapping 을 정의하는 것이고, 부가적으로 자동으로 Mapping 테스트도 할 수 있다는 장점이 있습니다.
그럼 오늘은 HBM, FluentNHibernate, NH 3.2 Build-In Code Mapping 을 간단한 예를 가지고 비교해 보겠습니다.
아주 간단한 예 중에 하나의 entity를 두 개의 Table 의 1:1 매핑으로 표현하는 방식인 <join /> 이라는 방식에 대한 구현을 보겠습니다.
엔티티 클래스는 다음과 같습니다.
 
using System;

namespace RCL.Data.NH.DomainModel.Mappings.JoinTable
{
 public class JoinMaster : DataEntityBase<Int32>
 {
  public virtual string Name { get; set; }
 
  public virtual string NickName { get; set; }
 
  public virtual string Description { get; set; }
 }
}



보시다시피, 3개의 Property가 있습니다. Name 속성은 “JoinMaster” 테이블에 저장되고, NickName과 Description은 “JoinDetail” 테이블에 저장되고, 1:1 매핑이 되게 합니다.

기본 HBM 방식으로 Mapping 을 정의한다면 다음과 같습니다.

결과를 보시면, 뭐 별거 아니잖아 하실 것입니다…




  
   
  

  

  
   
   
   
   
  
     
 

NHibernate 에 대해 초급자 수준이라도 쉽게 이해가 되실 것입니다. 

그럼 이런 매핑을 FluentNHibernate 를 이용하여 매핑한다면 다음과 같게 정의할 수 있습니다.
using FluentNHibernate.Mapping;

namespace RCL.Data.NH.DomainModel.Fluent.Mappings
{
 public class FJoinMasterMap : ClassMap
 {
  public FJoinMasterMap()
  {
   Table("FJoinMaster");
   LazyLoad();
   DynamicInsert();
   DynamicUpdate();

   Id(x => x.Id).Column("MasterId").GeneratedBy.Native();

   Map(x => x.Name);

   Join("FJoinDetail", m =>
                       {
                        m.Map(x => x.NickName);
                        m.Map(x => x.Description);
                       });
  }
 }
}

FluentNHibernate 를 직접 사용해보면, Lambda Expression을 사용하게 되어, 손 쉽게 Proeprty 를 매핑할 수 있도록 Intellisense에 나타나게 되어 매핑이 손쉽게 됩니다.
(이 매핑에서 “F” 라는 접두사를 둔 것은 HBM 방식의 매핑과 같은 DB에 생성하기 위해 구분을 위해 넣었습니다.)

다음은 NH-3.2에 새로 제공되는 Built-In Code Mapping 방식을 살펴보겠습니다.


using NHibernate.Mapping.ByCode;
using NHibernate.Mapping.ByCode.Conformist;

namespace RCL.Data.NH.DomainModel.Loquacious
{
 public class CJoinEntityMap : ClassMapping
 {
  public CJoinEntityMap()
  {
   Table("CJoinEntity");
   Lazy(true);
   DynamicInsert(true);
   DynamicUpdate(true);

   Id(x => x.Id, c => c.Generator(Generators.Native));

   Property(x => x.Name);

   Join("CJoinEntityDetail",
        jm =>
        {
         jm.Property(x => x.NickName);
         jm.Property(x => x.Description, c => c.Length(9999));
        });
  }
 }
}


NH-3.2 Code Mapping 방식은 Lambda Expression을 사용했지만, Fluent 방식 (Method Chain) 을 사용하지 않아, 코드가 좀 길어진 느낌입니다. 다만 메소드 명칭이 HBM의 XML Element 요소명과 같아, 쉽게 기억할 수 있는 점이 장점입니다.

HBM 방식의 장점은 기존 매핑에 대한 많은 방식이 HBM으로 되어 있어, 공부하기에는 좋습니다. 즉 기초를 닦기 위해서는 HBM을 꼭 학습해야 합니다.

FluentNHibernate 방식은 실행 시 HBM을 생성하여, NHibernate가 Deserialize를 수행하는 방식이라, 첫 실행 시에 성능 문제가 제기될 수 있지만, 웬만한 시스템에서는 별 의미 없는 단점이라 할 수 있습니다. 코드 매핑의 장점으로는 “Magic String” 을 사용하지 않아, 유지보수나 Refactoring 시에 상당히 유용하다고 볼 수 있습니다.

NH-3.2 Code Mapping 방식은 Code Mapping의 장점과 내부 API 라는 장점으로 XML Deserialize 단계를 생략하게 됩니다. 결국 NHibernate 초기화 비용이 가장 적게 든다고 볼 수 있습니다. 다만 매핑 방식이 Fluent 방식이 아니라, 코드가 길어지고, 가독성이 떨어진다는 점이 단점이라 볼 수 있습니다. 또한 첫 번째 버전이라 그런지, MySQL 이나 PostgreSQL 에서는 매핑이 제대로 안 되는 경우가 있습니다. (앞으로 나올 NH 3.2.1 도 마찮가지로 안됩니다)

여러분은 어떤 방식을 쓰시겠습니까?
저희 회사 내부에서는 압도적으로 FluentNHibernate가 인기가 있더군요

2011년 10월 12일 수요일

FluentNHibernate 으로 ManyToMany 매핑하기

HBM으로 매핑하는 것은 많은 예제가 있는데, FluentNHibernate으로 매핑하는 예제는 FluentNHibernate 사이트에 달랑 한 줄의 코드로 설명이 끝납니다.
아니 ManyToMany가 얼마나 복잡한 건데 달랑 한 줄에 끝내는 겨? 특히나 양방향 (bi-directional) 인 경우에는 둘 중 한 군데에 inverse=true 를 줘야 하는데, 그런 예도 없고 말야…
어쩔 수 없이 테스트를 해봤습니다… 우선은 상세한 매핑 설정을 통해 제대로 작동하는지 검증해 봤습니다.

Many-To-Many 상세 설정
   1: public class FDepartmentMap : ClassMap<FDepartment>
   2: {
   3:     public FDepartmentMap()
   4:     {
   5:         Table("FDepartment");
   6:         DynamicInsert();
   7:         DynamicUpdate();
   8:  
   9:         Id(x => x.Id).Column("DepartmentId").GeneratedBy.Native();
  10:  
  11:         Map(x => x.Code);
  12:         Map(x => x.Name);
  13:  
  14:         References(x => x.Parent)
  15:             .Column("ParentId")
  16:             .Access.Property()
  17:             .Cascade.SaveUpdate()
  18:             .Fetch.Select()
  19:             .LazyLoad(Laziness.Proxy);
  20:  
  21:         HasMany(x => x.Children)
  22:             .Access.CamelCaseField(Prefix.Underscore)
  23:             .Cascade.AllDeleteOrphan()
  24:             .Inverse()
  25:             .LazyLoad()
  26:             .AsSet(SortType.Natural);
  27:  
  28:  
  29:         Component<TreeNodePosition>(x => x.NodePosition,
  30:                                     p =>
  31:                                     {
  32:                                         p.Map(x => x.Order).Column("TreeOrder");
  33:                                         p.Map(x => x.Level).Column("TreeLevel");
  34:                                     });
  35:  
  36:         HasManyToMany(x => x.Users)
  37:             .Table("FDepartmentMember")
  38:             .ParentKeyColumn("DepartmentId")
  39:             .ChildKeyColumn("UserId")
  40:             .Inverse()
  41:             .LazyLoad()
  42:             .AsSet();
  43:     }
  44: }



보시다시피 many-to-many  매핑을 수행하는 부분은 소속 부서원들과의 매핑이고, 겸직을 고려한 설계입니다.

관계 table 명은 “FDepartmentMember” 이고, DepartmentId, UserId 의 값 설정으로 됩니다. 그리고, 중복을 방지하기 위해 ISet<FUser> 이구요…



이 방법은 제가 그 동안 HBM으로 매핑하는 방식에 익숙해서 모두 설정해 준 것이고, 실제로 다음과 같이만 해줘도




   1: HasManyToMany(x => x.Users)
   2:     .Table("FDepartmentMember")
   3:     .Inverse()
   4:     .AsSet();

똑 같은 결과를 얻는다는 것입니다.



FluentNHibernate의 AutoMapping 기능이 상당히 막강하여, 왠만한 정보는 Entity 클래스로부터 추출해 내므로, 굳이 매핑 시 지정하지 않아도 된다는 점입니다.

보면 볼 수록 FluentNHibernate은 매력적인 놈이라 생각됩니다.

2009년 4월 5일 일요일

https://Catharsis.svn.codeplex.com/svn

Based on Microsoft .NET 3.5 SP1 + ASP.NET MVC (RC1)
NHibernate 2.0.1

Nothing else needed, All other stuff is provided directly by Catharsis framework and Catharsis guidance. The core libraries are named ProjectBase to allow smoother introduction to your organization. Catharsis is Open Source.

2009년 3월 21일 토요일

NHibernate 2.1.0 Alpha...

NHibernate 2.1.0 Alpha 1이 Release 되었네요...

svn에서는 Alpha2가 진행중이구요...

많은 기능이 추가되고, Upgrade되었습니다.
다만... alpha2는 아직 버그가 많습니다. 특히 SubQueries 쪽은 검토가 아직 안되었나 봅니다....

아직까지는 2.0.1 GA가 가장 안정적인 것 같습니다.

물론 아주 복잡한 query가 아닌 이상에는 2.1.0 도 쓸만합니다.

2009년 3월 20일 금요일

Role, Group & Members

권한 관리부분에서 가장 큰 부분은 Role 에 속한 Actor에 대한 구분입니다.

Application에서 Role은 크게 두가지로 나뉩니다. 
1. 시스템에서 미리 정의한 System Role
2. 시스템 운영에 따른 실행 산출물에 대한 Role (Instance Role)

1번에 해당하는 것이 Administrator, Users, Guest, Process Modeler User 등이 될 것이고,
2번에 해당하는 것이 Process Owner, Project Managers - 둘 다 특정 Resource에 대해 관련있는 Role입니다.

1번은 유한한 갯수 (100개 미만),
2번은 시스템 운영에 따라 100만개도 생길 수 있습니다.

권한 관리부분에서, 특정 리소스에 대한 접근 허용 여부를 검사하기 위해서는 위의 두가지를
나누어서 검사하는 것이 좋습니다. (Instance Role 이 너무 많기 때문에)

우선 접근 검사를 하고자 할 때의 시나리오를 검토하면

1. 접근 대상이 Application의 정적인 Resource라면 System Role 만 가지고 검사하면 됩니다.
2. 접근 대상이 동적인 Instance Resource라면 System Role로 먼저 검사하고,
    다음으로 Instance Resource와 관련 있는 Role에 Actor가 소속되어 있는지만 검사하면 됩니다.

두 가지를 한꺼번에 할 경우에는 상당히 많은 량의 레코드를 검사해야 하지만,
위와 같이 두 단계로 나누게 되면 검사할 양이 상당히 줄어들게 됩니다.

다만 Instance Resource와 Instance Role 의 Association을 관리해야 하는 기능을 추가해야 하지만,
보통 Role name에 "/Resource/Manager", "/Resource/User" 등으로 이름을 사용하고, 
관련 Instance의 Natural Key를 Association 하면, 레코드 수도 줄고, 상당한 량의 작업도 감소 시킬 수 있습니다.