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

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은 매력적인 놈이라 생각됩니다.