SQL Express로 Castle ActiveRecord 시작하기

Castle ActiveRecord 2012/01/13 16:35
우선 Nuget으로 Castle ActiveRecord를 설치한 후에,
일단 App.config 파일 설정은 이렇게
  그리고 간단한 샘플 코드
tags : active-record
Trackback 0 : Comment 0

Code First CTP로 만든 모델에 대한 EDMX 디자인 파일 만들기

Entity Framework 2012/01/13 13:54
NHibernate로 모델링을 하거나 EntityFramework CTP CodeFirst 로 만든 모델의 단점은?
각각 모델 객체들의 관계를 한 눈에 파악하기 쉽지 않다는 것이다.

물론, NHibernate를 위한 여러가지 유료 툴들이 있어서 사용하면 되겠지만, 유.료. 다....

그럼 EF CTP는??

역시 방법이 있다.

코드로 모델링을 완료한 다음, 이를 역으로 EDMX 파일로 뽑아내는 방법이다.

1. 일단 EF CTP5를 이용해서 모델 객체와 컨텍스트 객체를 만든다.
    
public class Category
    {
        public string CategoryId { getset; }
    [MaxLength(10)]
        public string Name { getset; }
        public virtual ICollection<Product> Products { getset; }
    }

    public class Product
    {
        public int ProductId { getset; }
        public string Name { getset; }
        public string ProductCode { getset; }
        public string CategoryId { getset; }
        public virtual Category Category { getset; }
}
    public class ProductContext : DbContext
    {
        public ProductContext() : base("CodeFirstSampleDB") {}

        public DbSet<Category> Categories { getset; }
        public DbSet<Product> Products { getset; }
    }

2. DbContext 클래스는 OnModelCreating() 이라는 메소드를 제공하는데, 이는 메소드 이름처럼 모델을 만들때 호출된다.
   이 OnModelCreating() 메소드에 아래와 같이 edmx 파일을 생성하는 코드를 작성한다.
    
protected override void OnModelCreating
                              (System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
    {
        var provider = new DbProviderInfo("System.Data.SqlClient""2005");
        var model = modelBuilder.Build(provider);
        model.WriteEdmx(
            provider, 
            new XmlTextWriter(@"D:\MyWorkSpaceForVS\EF_CTP5_Test\CodeFirstSample\Product.edmx",
                Encoding.ASCII));
    }
위의 코드를 위해서는 아래의 두가지 네임스페이스가 추가되어져야 한다.
using System.Data.Entity.ModelConfiguration.Edm.Db;
using System.Data.Entity.ModelConfiguration.Edm.Db.Mapping;

3. 프로젝트를 실행한 후 프로젝트 속성에서 모든 파일 보기를 클릭하면 아래와 같이 EDMX 디자이너 파일이 생성된것을 확인할 수 있다.


Trackback 0 : Comment 0

Domain Model Relationships both in EF side and DB side

Entity Framework 2012/01/13 13:53
Entity Framework에서 사용하는 도메인 객체 간의 관계(Association)는 크게 세가지이다
 - One
 - One or Zero
 - Many

각각의 관계가 어떻게 데이터베이스에 반영되는지 간략하게 표로 만들어봤다.
Trackback 0 : Comment 0

Nuget Gallery 서버에 내 패키지 등록하기 : Uploading package to nugget gallery

Nuget 2011/12/16 08:14

맨날 Nuget에서 필요한 라이브러리를 다운로드 받아서 사용하기만 해봤지 한번도 Nuget에 뭘 올리려는 생각은 해본 적이 없었다.

그래서 시도해 보기로 한 내 패키지 올려보기 !!

몇 번의 삽질끝에 성공하긴 했지만 생각보다 만만친 않았다.

 

이곳 싸이트에 가면 자세한 업로드 방법이 나와있긴 하지만 너무 바이블식의 장황한 설명이여서 내가 정말 필요한 부분만 발췌해서 정리해 놓기로 한다. 그리고 저 사이트에서 설명하는대로 패키지를 업로드하는 방법은 여러가지가 있지만, dll 어셈블리 파일들 뿐만 아니라 텍스트 파일, 자바스크립트 파일, PowerShell 스크립트 등의 여타 다른 파일들도 배포하기 위해서는 아래서 설명할 방법을 사용하는게 가장 쉬운 방법이다.

 

패키지 업로드는 크게

  • Nuget Convention에 따라 폴더 구조 만들어 주기
  • *.nuspec 파일 생성하기
  • *.nupkg 파일 생성하기
  • Nuget 갤러리에 배포하기

단계로 나눌 수 있다.

 

Nuget Convention에 따라 폴더 구조 만들어 주기

먼저 Nuget에서 원하는 폴더 구조 만들어 주기… 에 앞서 업로드 할 클래스 라이브러리를 하나 만들자.

그 다음 루트 폴더(최상위에 있는 DotNetRoo 폴더) 하위에 Nuget이라는 폴더와 함께 하위에 Nuget Convention 대로 폴더들을 만든다. 아래 그림 참조

 

그림처럼 Nuget 폴더 밑에 버전을 관리할 목적의 DotNetRoo.0.0.3 폴더, 그 하위에 Nuget Convention을 따르는 세 폴더 content, lib, tools 폴더를 만들어 놓는다.

Content 폴더에는 나중에 이 패키지를 nuget에서 다운로드받아서 설치했을때 그 프로젝트의 루트에 복사될 파일과 폴더를 위치시킨다. 이번엔 테스트 용도로 Test.txt 파일을 만들어서 넣어 놓는다. Lib 폴더에는 나중에 이 패키지를 nuget에서 다운로드 받아서 설치했을때 '레퍼런스 추가' 될 dll 파일들을 넣어 놓는다. 따라서 DotNetRoo/DotNetRoo/bin/debug 폴더 밑에 있는 DotNetRoo.dll 파일을 복사해서 lib 밑에 넣어둔다.

그런데, 이 때 우리가 원하는 닷넷 프레임웍의 최소 버전을 지정할 수 있는데 그림에서 처럼 net40이라고 폴더를 만들고 그 하위에 DotNetRoo.dll 파일을 넣어두면 이 패키지를 다운로드해서 설치할 때 최소한 .NET 4.0 버전을 요구하게 된다. net40 폴더 아래에 DotNetRoo.dll을 넣어 둔다.

마지막으로 tools 폴더는 나중에 이 패키지가 설치된 이후에 그 프로젝트의 Package Manager Console에서 사용할 수 있는 PowerShell 스크립트 파일들이 위치하게 된다. 이 때 Ps1 파일(PowerShell 스크립트) 이름을 Init.ps1, Install.ps1, Uninstall.ps1과 같이 Nuget Convention에 따라서 지어주면 나중에 패키지가 설치될 때 자동으로 실행되도록 할 수 있다. 일단 이 폴더는 빈 폴더로 놔두자.

 

*.nuspec 파일 생성하기

다음 단계는 nuspec라는 확장자를 가지는 XML 파일을 만들어 주는 것이다. *.nuspec 파일은 업로드할 패키지의 메타 정보를 가지고 있는 파일이라고 생각하면 된다.

이를 위해서는 커맨드 창에서 nugget 명령어를 실행해야 하는데 이를 위해서는 이 곳에서 nugget.exe를 다운로드 받은 후 환경변수 PATH에 그 폴더 경로를 추가하도록 한다.

그 다음 커맨드 창에서 /DotNetRoo/DotNetRoo 폴더로 이동한 후 nugget spec라는 명령을 실행시킨다.

이 폴더 위치에서 실행시키는 이유는 *.csproj 파일 때문이다. 저 파일을 인식해서 nuspec 파일을 만든다.

그럼 DotNetRoo.nuspec 라는 파일이 생성되고 열어보면 $id$, $version$와 같은 치환이 필요한 문자들이 들어가 있는데 아래 그림처럼 적절하게 치환시키면 된다.

그리고 이 파일은 /DotNetRoo/Nuget/DotNetRoo.0.0.3 폴더 밑으로 이동(복사가 아니고)시켜 놓도록 하자.

 

*.nupkg 파일 생성하기

그런 후 커맨드 창에서 DotNetRoo.0.0.3 파일이 있는 /DotNetRoo/Nuget/DotNetRoo.0.0.3 폴더로 이동한 다음 아래의 명령어를 실행시킨다.

그러면 DotNetRoo.0.0.3.nupkg 파일이 생성된 것을 볼 수 있다.

 

Nuget 갤러리에 배포하기

이제 마지막 단계이다. 커맨드 창의 같은 경로에서 아래와 같이 명령어를 실행한다.

여기서 xxxxxxxxxxxxxxxxxxxxxxx 라고 된 부분은 api-key키로 치환해야할 곳인데, http://nuget.org에서 회원가입을 한후 계정정보 페이지에 가보면 API Key라고 된 부분에 명시된 문자열 키를 복사하여 넣어주면 된다.

여기까지 무리없이 실행하였으면 nuget gallery에 내가 만든 DotNetRoo가 업로드되어 배포되었을 것이다.

 

내가 만든 패키지 다운로드 !!!

업로드한 패키지가 제대로 다운로드 되는지 확인해 보기 위해서 콘솔 프로젝트를 하나 만들고 Package Manager Console에서 install-package를 실행 해보자.

솔루션 탐색기를 보면 DotNetRoo 어셈블리가 추가되었고, 첫 단계에서 content 폴더에 넣었던 test.txt 파일도 추가된 것을 볼 수 있다.

 

이걸로 nuget 갤러리 서버에 패키지를 업로드하고 다운로드 하는 배포과정이 모두 끝났다.

다음 단계는???

신나게 오픈 소스 플젝 코딩하는거~

 

p.s 최근 Microsoft를 떠나 GitHub로 옮긴 Phill Haack이 어느날 Miguel(Mono 프로젝트의 창시자이자 Xamarin의 설립자)에게 오픈 소스에 이렇게 투자하는 이유가 뭐냐고 물었더니, "A rising tide lifts all boats"라는 대답을 듣고 많은 걸 생각하게 됐다고 한다. 또, 일맥 상통하는 얘긴데, NHibernate 프로젝트의 메인 개발자이자 RavenDB의 개발자인 Ayende가 자기가 평생 오픈 소스로 개발과 사업을 해오면서 프로젝트든 사업이든 한번도 실패한 적이 없다는 얘기도… 최근에 내 머릿속을 맴돈다.

tags : nuget
Trackback 0 : Comment 0

PowerShell에서 Git 사용하기

PowerShell 2011/12/14 10:49
오늘 아침 Haacked가 gitbash 대신 PowerShell을 사용해서 git 명령어들을 쓰는 방법을 올려놨네... 
posh-git 란 놈을 사용하는 거임.

gitbash 쓰면서 붙여넣기 등등을 할때 좀 불편했는데 잘 됐군...

설치방법도 매우 간단한데, 요약하자면
  • git.cmd의 경로를 PATH에 추가한다. 내 경우에는 C:\Program Files (x86)\Git\cmd
  • 아래 명령어를 PowerShell에서 차례로 실행하면 끝
    • Set-ExecutionPolicy Unrestricted
    • (new-object Net.WebClient).DownloadString("http://psget.net/GetPsGet.ps1") | iex install-module posh-git
    • 엔터 !!
    • PowerShell 재부팅  
tags : git, PowerShell
Trackback 0 : Comment 0

POCO + WCF DataServices

Entity Framework 2011/12/10 10:12
WCF DataServices와 POCO 클래스로 생성시킨 Entity Framework를 같이 사용하기 위해선
Data Service에서 다음과 같은 설정이 필요하다.

public class WcfDataService : DataServiceNorthwindEntities >
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*"EntitySetRights.AllRead);
            config.SetServiceOperationAccessRule("*"ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
        }

        protected override NorthwindEntities CreateDataSource()
        {
            // 아래처럼 Proxy 생성 기능을 꺼버려야한다.
            var ctx = new NorthwindEntities();
            ctx.ContextOptions.ProxyCreationEnabled = false;
            return ctx;
        }
    }

그런데 엊그제 나온 Code First 방식에서는 저 설정을 할 수가없다. 
ContextOptions는 ObjectContext 클래스의 멤버인데
Code First에서 생성한 Context 클래스는 ObjectContext가 아닌 DbContext를 상속받고 있기 때문이다.
그렇다면, 방법은?
아직 모르겠다. 찾아보자.

Edited : 2010/12/11
찾긴 찾았다.
http://blogs.microsoft.co.il/blogs/gilf/archive/2010/12/11/using-ef-dbcontext-with-wcf-data-services.aspx
어차피 DbContext가 ObjectContext의 랩퍼이니 ObjectContext를 다시 꺼내서 DataService로 노출시키자는 건데,,, 흠....
그닥 근본적인 해결책으로는 보이진 않는다.
어차피 WCF DataServices 자체가 ObjectContext를 사용하는 EF에 맞게 미리 작업되어있으니 궁극적인 해결책은 없을거같다.
이 기회에 WCF DataService도 EF나 NHibernate 등의 ORM등과 어댑터 같은 API로 결합시킬수 있도록 바뀌어야할거같다. 
매번 EF가 새로 나올때마다 인터페이스 작업을 또 할 수도 없는것 아닌가...
어쨌건 패스~


Trackback 0 : Comment 0

Entity Framework 4.1 Code First 의 일대일 맵핑 전략

Entity Framework 2011/10/31 17:04

 
마이크로소프트웨어 10월호에 게재되었던 Entity Framework 4.1 일대일 맵핑에 관한 기사입니다. 지난 6,7,8월호에 쓴 기사에서 일대일 맵핑 부분이 좀 모자란거 같아 그에 대한 보강 내용입니다
 

Entity Framework 4.1 Code First 의 일대일 맵핑 전략   

 

ORM 영역에서 일대일 관계는 One-to-Many 관계나 Many-to-many 관계와 함께 가장 흔하고도 중요하게 다루어지는 관계이다. 이번 글에서는 Entity Framework 4.1 Code First를 사용하는 어플리케이션에서 어떻게 일대일 관계를 설정하는지에 대해서 알아볼 것이다. 참고로, 이번 글에서 사용한 샘플 코드는 https://github.com/RayKwon/MasoEFSample에서 다운로드 받을 수 있다.

 

모델간의 관계가 단순하게 일대일 관계라고 하더라도 실제 어플리케이션의 성질에 따라 미묘한 차이가 생길 수 있으며, 이는 어플리케이션 개발에 지대한 영향을 끼칠 수 있다. 따라서, 도메인을 설계할 때는 도메인 모델과 실제 데이터베이스 사이의 인프라스트럭쳐 역할을 할 ORM 기술의 특성도 염두에 두고 있어야만 할 것이다.

 

EF 4.1에서 지원하는 모델간의 일대일 관계를 맵핑하는 방법은 여러 가지가 있다. 양쪽 모델이 모두 키를 가지고 있는 엔티티간의 맵핑을 지원해주는 방법도 있고 반면에 키를 가지고 있는 모델과 그렇지 않고 값만 가지고 있는 모델간의 맵핑을 지원해주는 방법도 있다. 전자의 경우에는 PK를 공유하는 방법, 별도의 FK 컬럼을 사용하는 방법 등이 있을 수 있고 후자의 경우에는 지난 7월호 글에서 살펴 본 ComplexType을 사용하는 방법이 있을 수 있겠다.

 

 

ComplexType을 사용한 맵핑

 

<리스트 1> 과 같이 Student Address라는 모델이 있다고 가정해 보자.

 

public class SchoolContext : DbContext

    {

        public DbSet<Student> Students { get; set; }

    }

 

public class Student

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public Address AddressInformation { get; set; }

    }

 

   [ComplexType]

    public class Address

    {

        public string City { get; set; }

        public string Street { get; set; }

    }

 

<리스트 1> ComplexType을 사용한 맵핑

 

이렇게 모델을 설계한 설계자의 의도를 “Student에게는 오직 하나의 Address만이 할당되고 Address 모델은 별도의 Identity, 즉 키가 되는 필드를 가지고 있지 않을 것이라고 가정해보자. 이런 경우에 Address 모델의 City, Street 등의 필드들은 실제 데이터베이스 테이블에서는 <그림 1>처럼 별도의 테이블이 아닌 Student 테이블의 필드로 생성되게 된다.

 

<그림 1> ComplexType설정을 통해 생성된 테이블

 

EF에서는 이처럼 유일키를 가지고 있지 않은 모델을 ComplexType 이라고 지칭하는데, Domain Driven Design에서 지칭하는 Value Object라고 보면 된다. 이러한 ComplexType EF에서 사용할 때 몇 가지 제약 사항을 가지게 되는데, 첫 번째, Student 객체를 생성할 때는 반드시 Address도 생성해 주어야 한다는 것이다. 설령 빈 값을 가지더라도 Null이 되어서는 안 된다. 두 번째로는 Student를 통해 Address를 접근 할 때 Lazy Loading을 사용할 수 없다는 것이다. Student AddressInformation 프로퍼티를 virtual로 선언하더라도 ComplexType에 대해서는 Lazy Loading을 사용할 수 없다. 따라서 대량의 이미지나 사이즈가 큰 텍스트 데이터를 ComplexType으로 별도 객체에서 관리하더라도 Lazy Loading의 이점을 볼 수 없다.

또한 ComplexType으로 설정된 객체는 한 가지 객체 외에서는 사용할 수 없다. 다시 말하면, Student외에 Teacher라는 엔티티를 만들었다고 가정했을 때 Address 객체는 키가 없으므로 Teacher에서 Student가 사용한 동일한 Address를 공유할 수 없다는 것이다.

따라서, ComplexType을 통한 일대일 맵핑은 다분히 제한적인 용도로 밖에는 사용할 수 없다.

엄밀히 말하면 이러한 ComplexType은 일대일 관계 맵핑의 한 방법이라고 볼 수는 없다. 왜냐하면 흔히 어떠한 모델간의 관계를 언급할 때는 유일키를 가지고 있는 엔티티들을 대상으로 하지 ComplexType과 같은 Value Object들을 가정하지 않기 때문이다. 다만, ComplextType도 객체간의 일대일 연결을 해주는 방법임에는 틀림없으니 간략히 살펴보았다. ComplexType에 대한 좀더 자세한 설명은 지난 7월호 "DataAnnotation FluentAPI를 활용한 모델 설정" 기사를 참조하도록 하자.

 

주요키 공유를 통한 맵핑

 

앞서 설명한 ComplexType 을 사용한 맵핑의 단점을 극복하기 위해서 우선 생각해 볼 수 있는 방법은 Student처럼 Address도 키 값을 가지고 있는 엔티티로 선언해주는 것이다. <리스트2> Address ComplexType이 아닌 엔티티로 만들고 Address.Student, Student.Address 프로퍼티를 통해 Student 엔티티와 양방향 관계설정을 맺었다.

 

public class SchoolContext : DbContext

    {

        public DbSet<Address> Addresses { get; set; }

        public DbSet<Student> Students { get; set; }

    }

 

    public class Address

    {

        public int Id { get; set; }

        public string City { get; set; }

        public string Street { get; set; }

        public virtual Student Student { get; set; }

    }

 

    public class Student

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public virtual Address Address { get; set; }

    }

<리스트 2> Student Address간의 평등한 일대일 설정

 

언뜻 엔티티간의 관계는 평등한 일대일로 보이고 별 이상이 없어 보인다. 그러나 위의 모델을 어플리케이션을 통해 사용하려고 하면 <그림 2>와 같은 예상치 못한 예외가 발생하게 된다.

 

<그림 2> 주종관계 판단 기준이 모호할 때 발생하는 예외

 

이러한 예외가 발생하게 되는 이유는, EF는 모든 엔티티간의 관계를 기본적으로 주(Principal)와 종의 관계로 보기 때문이다. 예를 들어 일대다의 관계를 가지고 있는 모델이 있다면 일이 되는 엔티티가 주가 되고 다가 되는 엔티티는 종으로 보는 것이다. 이 원칙은 일대일 관계에서도 마찬가지로 적용이 되는데 Student Address는 서로가 양방향으로 참조를 하고 있는 일대일 관계이기 때문에 EF로서는 어느 쪽이 주가 되고 종이 되는지 판별할 수 없는 상황이 되는 것이다. 만일 두 엔티티에서 서로를 참조하고 있는 프로퍼티들, , Address.Student Student.Address 중에 한쪽을 지우면 위의 예외는 발생하지 않는다. 그 때는 어느 쪽이 주가 되고, 종이 되는지 확연히 결정할 수 있기 때문이다.

 

만일 <리스트 2>처럼 반드시 양방향 참조를 가져야 하는 일대일 관계라면, <리스트 3>처럼 FluentAPI를 사용해서 어느 쪽이 주가 되는지를 지정해 주면 된다.

 

protected override void OnModelCreating(DbModelBuilder modelBuilder)

        {

            modelBuilder.Entity<Student>()

                        .HasRequired(s => s.Address)

                        .WithRequiredPrincipal(a => a.Student);

 

            // 또는

 

            modelBuilder.Entity<Student>()

                        .HasOptional(s => s.Address)

                        .WithRequired(a => a.Student);

        }

<리스트 3> FluentAPI를 통해 주가 되는 엔티티를 지정해 주고 있다.

 

<리스트 3>의 두 방법 모두 Student가 주가 된다고 EF에게 알려주고 있다. <그림 3><리스트 3>의 두 가지 FluentAPI로 생성된 테이블 구조이다.

 

<그림 3> 주요키를 공유하는 일대일 관계

 

Addresses 테이블의 주요키인 Id 컬럼이 Students 테이블을 참조하는 외래키로도 사용된 것에 주목하자. 두 엔티티 간의 관계가 일대일이라고는 하지만 논리적으로 두 엔티티가 동시에 서로의 존재를 전제하는 관계란 있을 수 없다. 따라서 어느 한 쪽이 다른 한 쪽에 항상 의존적일 수 밖에 없는 것이고 Student Address 간에는 Address Student 엔티티에게 의존적인 관계를 가지고 있다. 이런 맥락에서 EF도 반드시 어느 한 쪽을 Principal로 지정해 줄 것을 요구하는 것이다. 당연한 얘기이지만, 현재의 관계에서는 새로운 Address를 저장할 때는 반드시 그와 연결된 Student 엔티티를 지정해 주어야 한다. 그렇지 않다면 외래키 충돌 예외를 발생하게 된다. 따라서 Address를 저장할 때는 항상 Student에 대한 정보를 가져와야만 하는 불편이 따른다.

 

이 방법의 다른 한가지 특징은 ComplexType을 사용할 때와 달리 Student 외의 다른 엔티티 에서도 Address를 사용할 수 있다는 것이다. 예를 들면, <리스트 4>에서는 Teacher라는 새로운 엔티티를 추가하고 Student와 마찬가지로 Address와 일대일 관계를 가지도록 설정하고 있다.

 

public class SchoolContext : DbContext

    {

        public DbSet<Address> Addresses { get; set; }

        public DbSet<Student> Students { get; set; }

        public DbSet<Teacher> Teachers { get; set; }

 

        protected override void OnModelCreating(DbModelBuilder modelBuilder)

        {

            modelBuilder.Entity<Student>()

                        .HasOptional(s => s.Address)

                        .WithRequired(a => a.Student);

 

            modelBuilder.Entity<Teacher>()

                        .HasOptional(t => t.Address)

                        .WithRequired(a => a.Teacher);

        }

    }

 

    public class Address

    {

        public int Id { get; set; }

        public string City { get; set; }

        public virtual Student Student { get; set; }

        public virtual Teacher Teacher { get; set; }

    }

 

    public class Student

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public virtual Address Address { get; set; }

    }

 

    public class Teacher

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public virtual Address Address { get; set; }

    }

<리스트 4> Address의 주요키를 공유하여 여러 엔티티에서 Address를 참조하고 있다.

 

하지만, <리스트 4>와 같은 관계를 이용하여 <리스트 5>와 같이 새로운 Student Address를 저장하려고 하면 외래키 충돌 예외를 만나게 된다.

 

SchoolContext context = new SchoolContext();

Address address = new Address { City = "제천" };

Student student = new Student { Name = "Scott Hanselman", Address = address };

context.Students.Add(student);

context.SaveChanges();

<리스트 5> 새로운 Student Address를 저장하고 있다.

 

그 이유는 Address Id 프로퍼티(테이블 관점에서는 주요키)는 이제 Student 엔티티를 참조하는 외래키일 뿐만이 아니라 Teacher 엔티티를 참조하는 외래키로도 사용되는데, <리스트 5>에서는 Address와 연관되는 Teacher를 지정하지 않았기 때문이다. 새로운 Address를 저장하기 위해서는 항상 Student Teacher를 지정해 주어야 한다. ComplexType을 사용할 때와 달리 여러 엔티티에서 Address를 사용할 수 있는 장점은 있지만, 방금 언급한대로 항상 Address와 연관된 엔티티들을 지정해 주어야 하는 점은 여간 불편한 점이 아닐 수 없다. 또 다른 제약 사항은 Student에서는 오직 한번만 Address에 대해서 참조할 수 있다는 것이다. 예를 들면, Student.FirstAddress, Student.SecondAddress, 등등과 같이 Address를 여러 번 참조할 수 없다.

 

 

외래키를 통한 맵핑

 

주요키를 공유하는 일대일 관계에서 언급한 몇 가지 제약사항이 설계 해야 할 어플리케이션에서 치명적인 약점이 된다면 외래키를 통한 일대일 관계 맵핑 방법을 고려해 볼 수 있다. 이 방법은 <그림 4>와 같이 Addresses 테이블의 Id 컬럼은 주요키로만 사용하고 Student_Id와 같은 별도의 외래키를 두어서 다른 테이블과의 연관관계를 표시하는 방법이다.

 

<그림 4> 외래키를 통한 타 테이블 참조

 

<그림 4>와 같은 관계를 만들기 위해선 <리스트6>과 같은 Fluent API가 사용될 수 있다.

 

protected override void OnModelCreating(DbModelBuilder modelBuilder)

        {

            modelBuilder.Entity<Student>()

                        .HasOptional(s => s.Address)

                        .WithOptionalPrincipal(a => a.Student);

        }

<리스트 6> FluentAPI를 통해 외래키를 사용하는 일대일 맵핑을 설정

 

그런데 <리스트 6>의 방법은 앞서 살펴봤던 주요키 공유 방법의 문제점이었던 여러 엔티티에서 Address 엔티티를 공유하는 문제나 한 엔티티에서 Address를 여러 번 참조하는 문제들을 해결해주긴 하지만, 데이터베이스의 관점에서 봤을 때는 그다지 효율적이지 못한 관계설정이다. <그림 6>과 같은 테이블 디자인을 본다면 대부분의 DBA들은 문제를 제기할 것이다. Addresses 테이블은 일종의 코드 마스터와 같은 성질의 테이블인데 이를 참조하는 모든 테이블의 키를 Addresses 테이블이 가지고 있는 것이다. 이는 추후 Addresses 테이블을 참조하는 테이블이 늘어날 때 마다 Addresses 테이블을 수정해야 하기 때문에 별로 추천할 만한 디자인이 아니다. 아마도 모델 설계자와 DBA 모두가 만족할 만한 테이블 디자인은 <그림 5> 보다는 <그림 6>에 가까울 것이다.

 

<그림 5> Addresses에서 관련 테이블의 모든 외래키를 가지고 있는 모습

 

<그림 6> Students, Teachers 등에서 Addresses에 대한 외래키를 가지고 있다

 

그리고 <그림 6>과 같은 관계를 설정하기 위해서는 <리스트 7> 과 같은 Fluent API 설정이 사용될 수 있다.

 

protected override void OnModelCreating(DbModelBuilder modelBuilder)

        {

            modelBuilder.Entity<Student>()

                        .HasOptional(s => s.FirstAddress)

                        .WithMany()

                        .HasForeignKey(s => s.FirstAddressId);

 

            modelBuilder.Entity<Student>()

                        .HasOptional(s => s.SecondAddress)

                        .WithMany()

                        .HasForeignKey(s => s.SecondAddressId);

 

            modelBuilder.Entity<Teacher>()

                        .HasOptional(t => t.Address)

                        .WithMany()

                        .HasForeignKey(t => t.AddressId);

        }

<리스트 7> Fluent API를 통해 외래키 설정

 

Address엔티티 쪽이 아닌Student, Teacher 엔티티에서 Address 엔티티에 대한 참조와 외래키를 가져가는 방식이다. 그런데 맵핑 설정에서 한가지 의아해 보이는 것이 있다. WithMany() 메소드이다. 우리는 분명 지금까지 일대일 맵핑을 염두에 두고 있었는데 갑자기 일대다나 다대다 관계에서나 볼 법한 WithMany()가 등장했다. 이것은 EF가 바라보는 관점의 차이에서 비롯된 것인데, Address 엔티티 입장에서는 Student Teacher 등 여러 엔티티와 값을 공유하므로 일대다의 관점으로 본 것이다. 그러나 만약 어떠한 Address 값을 하나의 Student Teacher외에는 참조하지 않는 완벽한 일대일 관계로 가져가려면 Students-FirstAddressId Students-SecondAddressId, Teachers-AddressId 등의 외래키에 Unique Constraint를 적용하면 된다. 그러나, 현재 EF에서는 안타깝게도 Unique Constraint를 적용하는 방법을 지원하지 않기 때문에 이럴 때는 DML스크립트를 사용해서 제약을 가하는 수 밖에 없다. 필자가 제안하는 방법은 EF의 컨텍스트가 데이터베이스를 생성할 때 이러한 부가적인 스크립트를 실행시킬 수 있는 방법을 사용하는 것이다. <리스트 8> Seed 메소드를 오버라이딩해서 필요한 Unique Constraint 를 추가하고 있다.

 

public class SchoolContextInit : DropCreateDatabaseAlways<SchoolContext>

    {

        protected override void Seed(SchoolContext context)

        {

            context.Database.ExecuteSqlCommand("ALTER TABLE Students ADD CONSTRAINT uc_FirstAddress UNIQUE(FirstAddressId)");

        }

    }

<리스트 8> 별도의 스크립트 실행을 통해 Unique Constraint 제약을 추가

 

이상으로 EF 4.1에서 지원하는 일대일 관계 맵핑 방법들에 대해서 살펴보았다. 이번 글에서 언급하지는 않았지만, 하나의 테이블을 여러 개의 엔티티로 나누어 관리하는 방법도 넓은 의미에선 일대일 관계 맵핑 전략중 하나로 볼 수도 있다. 지면 관계상, 이에 대해선 필자의 블로그를 통해서 계속 설명하겠다.

 

 

Trackback 0 : Comment 0

Why ORM?

Entity Framework 2011/10/25 23:21
ORM의 장점은 무엇일까?

오래되고도 또다시 반복되는 지겨운 이야기지만,,, 기록해 놓자.

되도록이면 실용적이고 쉽게 수긍할 수 있는 이유만 나열하자. 
어차피 RDB와 OO 랭귀지 간의 Impedance Mismatch 같은 얘기를 꺼내봤자, 쌩쿼리와 프로시져가 언제나 짱이라고 생각하는 개발자들에게는 씨알도 안먹힐 것이다.

그럼,,,

1. 깔끔한 코드를 유지할 수 있다.
   코드에서 또는 XML파일에서 그 지저분한 쿼리문들을 깔끔히 제거할 수 있다. 

2. 특정 DB 벤더에 종속되는 어플리케이션에서 벗어날 수 있다.
    NHibernate는 물론이고 Entity Framework 마저도 Provider와 Connection String만 변경해주면
    대부분의 상용 DB를 지원하는 어플리케이션을 쉽게 만들 수 있다.
    단순히 ORM을 사용했을 뿐인데 어플리케이션의 경쟁력이 늘어난다.
   
    아래는 Entity Framework가 SQL Server 외의 타 DB를 지원하게 해주는 프로바이더가 나와 있는 링크이다. 
    
    또한 Oracle에서도 올해말 EF용 프로바이더 베타 버전을 내놓을 것이라고 한다.

3. 생산성이 드라마틱하게 상승할 수 있다.
    ORM을 사용하면 한두줄이면 끝날 쿼리가 Hand Coded Query를 쓰면 수십~수백 줄이 될 수도 있다.
     
    e.g. var result = context.Customers.Include("Orders").Take(10).Skip(10).ToList();
    
    위의 EF 코드를 직접 쿼리로 작성했을 때 분량이 얼마나 늘어날 지 상상해 보자. 
    실제 어플리케이션에서 이 정도는 약과에 불과하다.
    테이블의 컬럼수가 많을수록, 테이블 간의 관계가 복잡하면 복잡할 수록 ORM의 파워를 느낄수 있을 것이다.

    흔히들 ORM을 잘 모르는 개발자에게 ORM을 간단히 설명해 주면 가장 먼저 묻는 질문이 열번 중에 아홉번은 이거다.
    "Join, Sub query와 같은 복잡한 쿼리문을 어떻게 ORM으로 바꿀거냐? "
    
    이 질문은 ORM에 대한 컨셉의 이해가 부족하기 때문이다.
    한 마디로 대답 하자면 "그럴 필요없다." 이다.
    그 복잡한 쿼리를 그대로 ORM으로 번역할 필요가 없다. 
    가져오고 싶은 객체와 관련된 객체들은 자동으로 바인딩 되어 가져와진다.
    이를 위해 필요한건, 잘 설계된 Entity와 Lazy Loading, Eager Loading 등에 대한 이해만 있으면 된다.  

4. 테스트를 쉽게 해준다.
    Unit Test Case를 작성한다고 가정해 보자. Unit Test는 DB 테스트가 아니다. 
    따라서 가상의 Mock 객체나 Fake 컨텍스트를 만들어 테스트하기 마련인데
    ORM을 사용하면 NHibernate에서는 ISession을 구현한 Fake 세션을, EF에서는 XXXEntities와 유사한 모양의 인터페이스를 
    만들어 이를 구현한 Fake 컨텍스트를 사용하면 쉽게 테스트가 가능하다. 
    
5. 만일 Data Access Layer에서 리턴하는 타입이 DataSet이 아니라 Entity 타입이라면 ORM을 쓰지 않을 이유가 없다.
   리턴 타입으로 Entity를 사용하면서 날쿼리를 작성한다고 가정해보자.
   쿼리 결과를 Entity에 바인딩하기 위해 지겨운 루프 작업을 해야만 한다.
   지겨울 뿐만 아니라 에러도 잘 난다.

6. .... 일단 오늘은 여기까지,
   생각나는대로 여기에 붙여쓰도록 하자.



Trackback 0 : Comments 4

점점 진보하는 NHibernate

NHibernate 2011/09/23 20:38
얼마전 정말 오랫만에 자바 교육을 받았다. 그것도 8일에 걸친 종일 교육,,, 
거의 6~7년 만에 다시 한번 자바를 만질 기회가 생겨서 반가웠고, 더욱이 최근 다시 한번 급관심 중인 NHibernate의 오리지널 자바 버전을 차근히 강의를 통해 배울 기회가 생겨서 넘 좋았다........하.지.만 

내 욕심이 과한 거였는지 아니면 강사님이 너무 노련한(?) 건지 몰라도,,, 전혀 깊이 있는 교육이 되질 못해서 많이 실망스러웠다.
암튼, 실로 간만에 다시 겪게된 자바에서 가장 인상 깊었던 것은 이클립스 였다.
그 예전의 느리고 무겁던 이클립스만 상상했던 내게 생각보다 훨씬 가볍고 여러 툴이나 프레임웍들과의 통합된 기능은 와~우~ 였다.

그에 반해 자바 언어 자체에 대해서는 예전과 비교해서 큰 변화나 향상된 점을 크게 느끼지 못했다.  
교육중에 어쩔 수 없이 C#과 비교를 할 수 밖에 없었는데, C#엔 있지만(정확히 말하면, 닷넷엔 있지만,,) 자바엔 없는 기능이 눈에 많이 띄었다. 대표적으로 델리게이트, 람다, LINQ, named parameter 등등,,

그런데도 불구하고 자바가 닷넷보다 시장에서 여전히 우위를 점하고 있는 가장 큰 이유는 아마도 막강한 오픈 소스 때문이 아닌가 싶다. 말해 무엇한 스프링, 하이버네이트, 아이바티스, 메이븐 등등,,

그리고, 알다시피 자바 진영에서 성공한 오픈 소스 들은 하나둘씩 닷넷 버전으로 포팅이 되었고 Spring.NET과 iBATIS.NET, NHibernate가 닷넷 진영에서도 사용중이다. 

아쉽게도 iBATIS.NET은 점차 그 인기가 시들해지는듯 하고(최근 이름을 MyBatis로 바꾼것 빼고는 더 이상의 버전업도 없고 커뮤니티도 거의 사장 분위기다), Spring 프레임웍이 자바진영에서 거의 표준 어플리케이션 프레임웍처럼 사용되는데 반해 Spring.NET은 계속해서 버전업이 되고 있긴 하지만, 외부 환경적인 요소들로 인해 점차 사용자층이 얕아지는 추세인듯 하다. 

외부 환경적인 요소들의 예를 꼽자면, WCF의 등장으로 인해 Spring.NET Service 모듈의 의미가 퇴색된 점, Castle 프로젝트의 Windsor, Unity, NInject, 등등등 가벼우면서도 XML이 아닌 어트리뷰트 또는 코드 레벨에서 DI 작업이 가능한(물론 Spring.NET으로도 DI 작업을 코드에서 할수 있긴 하다) 싱싱한 DI 프레임웍 들의 출현, 그리고 ASP.NET MVC 프레임웍의 출현으로 Spring.NET Web 모듈에서 심혈을 기울였던 웹폼 지원 기능들이 빛을 바라고 있는 점, 등을 들 수 있겠다.

얼마 안있으면 Spring.NET 2.0 버전이 출시될 예정이라고 하니 기대를 하고 있긴 하지만, 대폭적이고 혁신적인 개선이 이루어 지지 않으면 적어도 내가 보기에 자바 진영의 오리지널 Spring 프레임웍 만큼의 영광을 누리기는 쉽지 않아보인다. 

암튼 본론은,,, 
앞서 언급한 두 오픈소스 프레임웍이 죽쑤고 있는데 반해 여전히, 또는 오리지널 자바 버전의 인기를 뛰어넘는 관심을 받고 있는 NHibernate의 선전이 인상적이라는 것과 이 선전이 향후에도, 적어도 2~3년 정도는 더 갈것 같다는 것이다.

10월달에 출시될(아마도 10월8일이 될 가능성이 매우 크다 12월9일일 가능성이 높아졌습니다. http://jasondentler.com/blog/2010/09/september-nhibernate-news/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+BasiclyEverything+(BASICly+everything)) NHibernate 3.0 베타 버전을 가만히 들여다 보면, 오리지널 자바 버전인Hibernate 프레임웍을 뛰어 넘었다는 생각 마저도 든다. 이전 포스트에서도 언급했지만, Linq To NHibernate(작년에 출시된 Linq 버전과는 다르다) 기능이라든가, ICriteria의 기능을 보완하기 위한 QueryOver 기능, Fluent NHibernate 기능 등은 오리지널 하이버네이트 버전에선 지원하고 있지못한 기능들이며 단순한 기능 개선 정도가 아니라 프레임웍의 컨셉과 수준을 한단계 올려놓은 기능들이다. 청출어람이라는 구태의연한 비교까지 하지 않더라도 확실히 오리지널 Hibernate 보다 진보된 모습이 분명하다.

그런데 이러한 진보된 기능들은 다분히 자바 언어에서 지원하고 있지못한 닷넷 언어만의 고유한 기능들 때문이다.
바로 Linq Provider, Lamda, 델리게이트 가 그놈들이다. 서두에 언어적 비교를 잠깐 언급한 이유가 여기있다.
닷넷 출시 초기에 자바 진영과 닷넷 진영의 해묵은 언어적 논쟁에 휘말릴까봐 매우 조심스럽긴 하지만, 어찌됐든, 이러한 기능이 아직 자바에서 지원되지 못하고 있는 것은 사실이다.

델리게이트와 람다가 닷넷에서 선보였던 초기에 왜 이런 난해한 문법과 기능을 추가했는지 매우 의아했는데, 그 실용적인 쓰임새들이 보이면서 이제는 조금씩 이해가 갈것같다. 흠......

 
tags : NHibernate
Trackback 0 : Comments 2

Entity Framework 4.1 API의 주요 변화

Entity Framework 2011/08/31 20:53

마이크로소프트웨어 8월호, Entity Framework 4.1 연재의 마지막 기사입니다.

 

Entity Framework 4.1 API의 주요 변화

 

지난 글에서는 DataAnnotation API Fluent API를 사용해서 모델을 맵핑하고 설정하는 방법들을 알아보았다. 이번 글에서는 Entity Framework (이하 EF) 4.1에서 변화된 주요 API 및 향상된 기능들에 대해서 알아보도록 하겠다.

 

-----------------------------------------------------------------

2011. 6 Nuget을 이용한 Entity Framework 4.1 활용

2011. 7 DataAnnotation Fluent API를 활용한 모델 설정

2011. 8 Entity Framework 4.1 API의 주요 변화

-----------------------------------------------------------------

 

우리는 이전 두 번의 기사를 통해 EF 4.1에 대한 개요와 새로운 방식의 맵핑 방법에 대해서 알아보았다. 연재의 마지막인 이번 호에서는 EF 4.1에서 새롭게 제공하는 API들과 그에 대한 좀더 구체적인 활용방법들에 대해서 알아보도록 하겠다.

 

먼저 새로운 API들을 소개하기 이전에 확실히 알고 있어야 할 한가지 개념에 대해서 짚고 넘어가도록 하겠는데 그것은 EF에서 관리하는 엔티티 객체의 상태이다. EF의 컨텍스트 클래스가 관리하는 엔티티 객체는 System.Data.EntityState라고 하는 enum 이 표현하는 다섯 가지 상태 중 하나를 가지게 된다. 다섯 가지 상태는 다음과 같다.

 

l  Added : 컨텍스트에 의해 관리되고 있지만 데이터베이스에는 아직 저장되지 않은 상태.

l  Modified : 컨텍스트에 의해 관리되고 데이터베이스에도 존재하지만 일부 또는 전체 프로퍼티의 값이 변경된 상태.

l  Unchanged : 컨텍스트에 의해 관리되고 데이터베이스에도 존재하며 그 값도 변화 없이 그대로 유지하고 있는 상태.

l  Deleted : 컨텍스트에 의해 관리되고 데이터베이스에도 존재하지만 SaveChanges() 메소드가 호출되면 데이터베이스에서 삭제될 상태.

l  Detached : 컨텍스트에 의해 관리되고 있지 않은 상태.

 

그리고 이러한 엔티티의 상태는 SaveChanges() 메소드가 호출될 경우 다음과 같은 변화를 가지게 된다.

l  Added 상태인 엔티티는 데이터베이스에 저장되고 Unchanged 상태로 변경된다.

l  Modified 상태인 엔티티는 데이터베이스에 저장되고 Unchanged 상태로 변경된다.

l  Unchanged 상태인 엔티티는 아무런 변화가 발생하지 않는다.

l  Deleted 상태인 엔티티는 데이터베이스에서 삭제되고 컨텍스트에 의해 더 이상 관리되지 않는다.

 

그리고 엔티티들의 상태는 <리스트 1>과 같이 엔티티의 각종 메타데이터에 접근할 수 있는 Entry() 메소드와 State 프로퍼티를 통해서 확인이 가능하다.

 

CompanyContext context = new CompanyContext();

                var emps = context.Employees.ToList(); 

                foreach (var emp in emps)

                {

                    Console.WriteLine("{0} {1}", emp.Id,

                                                               context.Entry(emp).State.ToString());

                }

<리스트 1> Entry() 메소드를 통해서 엔티티의 여러 메타데이터에 접근할 수 있다.

 

이와 같은 엔티티의 상태는 이후에 설명할 각종 기능들을 이해하기 위해서도 필요하지만 EF 를 올바르게 사용하기 위해서 반드시 알고 있어야 할 개념이므로 꼭 숙지하도록 하자.

 

Find()

 

EF를 사용하여 데이터베이스에 저장된 데이터를 가져오기 위해서 우리는 주로 LINQ 쿼리를 사용한다. 그런데 LINQ 쿼리는 해당 엔티티가 이미 컨텍스트에 존재하든 그렇지 않든 상관없이 항상 데이터베이스에 대한 질의를 수행하므로 퍼포먼스적인 고려가 필요할 경우엔 다른 옵션을 생각해 볼 필요가 있다. 이 때 해결책으로 사용될 수 있는 것이 EF 4.1에서 새롭게 제공하는 Find() 메소드이다.

 

Find() 메소드는 컨텍스트에 있는 엔티티를 먼저 조회하며 해당 엔티티의 주요 키를 파라미터로 하여 필요한 값을 쿼리 할 때 요긴하게 사용될 수 있다. <리스트 2>를 보자.

 

CompanyContext context = new CompanyContext();

 

                Employee newEmp = new Employee

                                                           {

                                                               Id = 3,

                                                               LastName = "경주",

                                                               FirstName = ""

                                                            };

                context.Employees.Add(newEmp);

                context.SaveChanges();

               

                // 첫번째 Find() 메소드 호출

                var emp = context.Employees.Find(3);

                Console.WriteLine("{0} {1}{2} {3}", emp.Id,

                                                                     emp.LastName,

                                                                     emp.FirstName,

                                                                     context.Entry(emp).State.ToString());

 

                context.Employees.Remove(newEmp);

 

               // 두번째 Find() 메소드 호출

                var deletedEmp = context.Employees.Find(3);

                Console.WriteLine("{0} {1}{2} {3}", deletedEmp.Id,

deletedEmp.LastName,

deletedEmp.FirstName,

context.Entry(emp).State.ToString());

<리스트 2> Find() 메소드를 이용하여 엔티티를 가져오고 있다.

 

첫번째 Find() 메소드가 호출되는 부분에서 볼 수 있듯이 Id 값이 3 Employee 엔티티를 가져오고 있다. 그리고 다시 Employee를 삭제하기 위해 Remove() 메소드를 사용하여 해당 엔티티의 상태를 Deleted로 변경한 후 다시 한번 Finde() 메소드를 호출하여 Employee를 가져오고 있다. 여기서 주의 깊게 봐야 할 것은 Employees.Remove(newEmp)를 사용하여 엔티티를 삭제하였지만 SaveChanges() 메소드를 호출하지 않았기 때문에 여전히 컨텍스트에는 존재하고 있으며 따라서 두 번째 호출된 Find() 메소드는 여전히 Id 3 Employee 엔티티를 성공적으로 가져올 수 있다.

그리고 이 때 Find() 메소드는 데이터베이스로 실제 쿼리를 실행시키지 않으며, 이미 컨텍스트에 존재하고 있는 Employee를 그대로 반환하게 된다. 그러나 만약에 <리스트 3>과 같이 Remove()후에 SaveChanges() 메소드를 호출하게 되면 해당 Employee 엔티티는 컨텍스트와 데이터베이스에서 모두 삭제되어 Id 3 Employee는 더 이상 컨텍스트에 존재하지 않으므로 두 번째 Find() 메소드는 데이터베이스로 실제 SELECT 쿼리를 수행하게 된다. 물론 데이터베이스에도 더 이상 존재하지 않으므로 null을 반환하게 될 것이다.

 

CompanyContext context = new CompanyContext();

 

                Employee newEmp = new Employee

                                                             {

                                                                  Id = 3,

                                                                  LastName = "경주",

                                                                  FirstName = ""

                                                              };

                context.Employees.Add(newEmp);

                context.SaveChanges();

               

                var emp = context.Employees.Find(3);

                Console.WriteLine("{0} {1}{2} {3}", emp.Id,

                                                                     emp.LastName,

                                                                     emp.FirstName,

                                                                     context.Entry(emp).State.ToString());

 

                context.Employees.Remove(newEmp);

                context.SaveChanges();

 

                // Id 3 Employee는 더 이상 컨텍스트에 존재하지 않으므로 실제 데이터베이스로 SELECT 쿼리를 수행하게 된다.

                var deletedEmp = context.Employees.Find(3);

                Console.WriteLine("{0} {1}{2} {3}", deletedEmp.Id,

                                                                     deletedEmp.LastName,

                                                                     deletedEmp.FirstName,

                                                                     context.Entry(emp).State.ToString());

<리스트 3> Find() 메소드는 컨텍스트에 해당 엔티티가 존재하지 않을 경우 데이터베이스로 질의를 수행하게 된다.

 

종합하여보면 Find() 메소드는 해당 엔티티의 주요 키를 기반으로 쿼리를 수행하며, 일차적으로 현재 컨텍스트 내에 키 값을 가지고 있는 엔티티가 있는지 검색 후, 있으면 엔티티를 반환하고 그렇지 않으면 데이터베이스로 질의를 수행하는 특징을 가진다.

 

만약에 주요 키 값이 여러 가지인 엔티티일 경우엔 <리스트 4> 와 같이 Order 파라미터를 사용해서 컬럼의 순서를 지정하고 Finde() 메소드에서는 그 순서대로 파라미터 값을 입력받게 된다.

 

public class Employee

    {

        [Key]

        [Column(Order=0)]

        public int Id { get; set; }

 

        [Key]

        [Column(Order=1)]

        public int No { get; set; }

}

 

---- 코드 생략 ----

 

CompanyContext context = new CompanyContext();

 

                Employee newEmp = new Employee

                                                           {

                                                                Id = 3,

                                                                No=4,

                                                                LastName = "기봉",

                                                                FirstName = ""

                                                            };

                context.Employees.Add(newEmp);

                context.SaveChanges();

 

                // 첫 번째 파라미터는 Id, 두 번째 파라미터는 No 프로퍼티의 값을 의미한다.              

                var emp = context.Employees.Find(3, 4);

 

<리스트 4> Order 파라미터를 통해 정해진 컬럼 순서는 Find() 메소드의 파라미터 순서와 일치된다.

 

 

프로퍼티 값의 추적

 

EF 4.1에서는 엔티티에 존재하는 각각의 프로퍼티들에 대해 데이터베이스에서 쿼리를 통해서 최초 가져왔던 최초 값(Original Values), 그리고 그 값이 변화가 있었든 그렇지 않든 현재 프로퍼티가 지니고 있는 현재 값(Current Values)을 관리한다. 더불어 현재 데이터베이스에 저장되어 있는 값을 조회할 수 있는 방법을 지원한다.  <리스트 5>를 보자.

 

CompanyContext context = new CompanyContext();

              

context.Employees.Add(new Employee {  Id = 1,

FirstName = "효중",

LastName = "" });

                context.SaveChanges();

 

               // 데이터베이스에서 가져온 최초 값을 변경한다.

                var emp = context.Employees.Find(1);

                emp.FirstName = "수진";

               

                // DbPropertyEntry 클래스의 OriginalValue 프로퍼티를 통해 최초 값을 가져온다.

                Console.WriteLine("Original Name= {0}{1}", emp.LastName,

context.Entry(emp).Property(e=>e.FirstName).OriginalValue.ToString());

 

                // DbPropertyEntry 클래스의 CurrentValue 프로퍼티를 통해 현재 변경된 값을 가져온다.

                Console.WriteLine("Current Name= {0}{1}", emp.LastName,

                      context.Entry(emp).Property(e=>e.FirstName).CurrentValue.ToString());

 

                // SQL 쿼리를 통해 값을 변경한다.

                context.Database.ExecuteSqlCommand("UPDATE EMPLOYEES SET FIRSTNAME='' WHERE ID=1");

 

                // 현재 데이터베이스에 저장되어 있는 값을 가져온다.

                Console.WriteLine("Database Name= {0}{1}", emp.LastName,

                     context.Entry(emp).GetDatabaseValues()["FirstName"].ToString());

<리스트 5> EF는 원래 값과 현재 값을 관리한다.

 


<그림 1> FirstName 프로퍼티의 최초 값, 현재 값, 현재 DB에 저장된 값

 

DbPropertyEntry 클래스의 OriginalValue, CurrentValue 프로퍼티를 통하면 싱글 프로퍼티의 해당 값을, DbEntityEntry 클래스의 OriginalValues, CurrentValues를 통하면 해당 엔티티의 모든 프로퍼티 값들에 대한 최초, 현재 값을 조회할 수 있다.

 

마지막 줄의 GetDatabaseValues() 메소드는 현재 데이터베이스에 저장되어 있는 값을 가져오는데 동시성 관리를 위해 값을 비교하거나 할 때 유용하게 사용할 수도 있다. 사실 동시성 관리는DataAnnotations API TimeStamp 어트리뷰트나 ConcurrencyCheck 어트리뷰트를 통해서 하는 편이 더 수월한데 이에 대해서는 지난 7월호 기사를 참조하기 바란다.

 

한 가지 주지해야 할 점은 Property() 메소드는 콤플렉스 타입의 객체가 아닌 이상 프로퍼티에 대해 단계적인 접근을 지원하지 않는다. , context.Entry(emp).Property(e=>e.Team.TeamName) 과 같은 접근이 허용되지 않는다는 말이다. 이러한 경우엔 context.Entry(emp.Team).Property(t=>t.TeamName)과 같이 Team 엔티티 자체를 Entry() 메소드의 파라미터로 넘겨줘야 Team 프로퍼티의 값에 대한 접근이 가능하다.

 

 

연관 엔티티 로딩

 

특정 엔티티와 연관된 엔티티를 로딩하는 방법은 이전 버전과 마찬 가지로 Lazy Loading, Eager Loading, Explicit Loading이 있는데 API에 약간의 개선이 있다.

 

먼저 Eager Loading을 위해서 사용되었던 Include()기존 버전에서는 문자열로만 연관 엔티티를 지정할 수 있었는데 EF 4.1에서는 Include(e => e.Team)과 같이 람다 표현식을 지원해주어 좀 더 편리하게 사용할 수 있게 되었으며, Lazy Loading에 관련해서는 지난 글에서도 잠시 설명했지만, virtual 키워드로 선언된 네비게이션 프로퍼티에 대해서는 기본적으로 Lazy Loading이 지원되며, context.Configuration.LazyLoadingEnabled = false와 같이 지정하면 모든 엔티티에 대해 Lazy Loading 기능은 작동하지 않는다. 하지만, 이러한 설정으로 Lazy Loading 기능을 꺼놓았더라도, <리스트 6> 처럼 Reference() 메소드나 Collection() 메소드의 Load() 메소드를 사용하면 필요할 때 즉시 연관 엔티티를 로딩할 수 있다.

 

CompanyContext context = new CompanyContext();

 

                // Lazy Loading 기능을 꺼놓아도 Explicit Loading 기능은 여전히 작동한다.

                context.Configuration.LazyLoadingEnabled = false; 

                var emp = context.Employees.Find(1); 

                context.Entry(emp).Reference(e => e.Team).Load(); 

                Console.WriteLine(emp.Team.TeamName); 

                context.Entry(emp).Collection(e => e.Clubs).Load(); 

                Console.Write(emp.Clubs.First().ClubName);

<리스트 6> Explicit Loading 방법

 

메소드 이름에서 짐작할 수 있듯이 Reference() 메소드는 단일 객체인 네비게이션 프로퍼티에 대해서, Collection() 메소드는 컬렉션 타입의 네비게이션 프로퍼티에 대해서 사용한다. 또한 필터링 기능이나 카운트 등이 필요할 때는 <리스트 7>과 같이 Query() 메소드를 사용하여 IQueryable<T> 타입으로 변환하면 된다.

 

context.Entry(emp)

.Reference(e => e.Team)

.Query()

.Where(t=>t.TeamName.Equals("Finance Team"))

.Load();

 

                Console.WriteLine(emp.Team.TeamName);

 

                int nCnt = context.Entry(emp).Collection(e => e.Clubs).Query().Count();

 

                Console.Write("Related Number of Clubs : {0}", nCnt);

<리스트 7> Query() 메소드를 사용하여 IQueryable<T> 타입으로 변환

 

 

Local

 

EF 4.1에서는 엔티티 조회 시에 데이터베이스에 대한 쿼리를 실행시키지 않고 컨텍스트에 있는 데이터를 조회 할 수 있는 방법으로 DbSet 클래스에서 Local이라는 프로퍼티를 제공한다. 우선 <리스트 8>의 예제 코드를 보자

 

CompanyContext context = new CompanyContext(); 

                context.Employees.Add(new Employee { Id = 1,

FirstName = "효중",

LastName = "" });

                context.Employees.Add(new Employee { Id = 2,

FirstName = "성기",

LastName = "" });

                context.Employees.Add(new Employee { Id = 3,

FirstName = "근영",

LastName = "" });

               // 데이터베이스에 반영한다.

context.SaveChanges();

 

                // 새로운 엔티티를 추가하지만 실제 저장은 하지 않는다.

                context.Employees.Add(new Employee { Id = 4,

FirstName = "수진",

LastName = "" });

 

                // 컨텍스트에서 Id 2 엔티티를 Deleted 상태로 수정한다.

                // 데이터베이스에서 실제 삭제는 하지 않는다.

                context.Employees.Remove(context.Employees.Find(2));

 

                // 컨텍스트에서 Id 3 엔티티를 Modified 상태로 수정한다.

                // 마찬가지로 실제 데이터베이스에 업데이트 하지는 않는다.

                context.Employees.Find(3).LastName = "";

 

                Console.WriteLine("****** DbSet Entities *******");

                foreach (var item in context.Employees)

                {

                    Console.WriteLine("{0} {1}{2} {3}", item.Id,

item.LastName,

item.FirstName,

context.Entry(item).State.ToString());

                }

 

                Console.WriteLine("****** Local Entities *******");

                foreach (var item in context.Employees.Local.OrderBy(e=>e.Id))

                {

                    Console.WriteLine("{0} {1}{2} {3}", item.Id,

item.LastName,

item.FirstName,

context.Entry(item).State.ToString());

                }

 

Console.WriteLine("****** All Entities in the Context *******");

foreach (var item in context.ChangeTracker.Entries<Employee>())

                {

                    Console.WriteLine("{0} {1}{2} {3}", item.Entity.Id,

item.Entity.LastName,

item.Entity.FirstName,

item.State.ToString());

                }

<리스트 8> Local의 활용

 

<리스트 8>의 결과는 <그림 2>와 같다.

 


<그림 2> Local 프로퍼티는 Deleted 상태인 엔티티를 제외한 컨텍스트의 모든 엔티티를 데이터베이스 질의 없이 반환한다.

 

<그림 2>의 결과를 통해서 다음과 같은 몇 가지를 알 수 있다.

l  DbSet 엔트리를 호출하면 데이터베이스를 조회한다. 따라서 마지막에 추가한 Id4인 엔티티가 조회되지 않았다.

l  DbSet 엔트리를 통해 데이터베이스에 있는 데이터를 조회했다고 하더라도, 각 엔트리의 상태는 현재 컨텍스트 내에서의 상태이지 데이터베이스에서의 상태가 아니다. , Id 2 3인 엔티티를 각각 삭제하고 수정했지만 SaveChanges()를 통해서 데이터베이스에 반영하지 않았기에 각각 엔트리의 상태는 여전히 Deleted이고 Modified 인 것이다. 만일 각각의 수정과 삭제 작업 이후에 SaveChanges()를 호출 했다면 수정한 엔트리의 상태는 Unchanged 상태로 바뀌어 있을 것이고 삭제한 엔트리는 컨텍스트에 포함되어 있지 않을 것이다. <그림 3>의 결과를 참조.

l  Local 프로퍼티는 데이터베이스 질의를 실행하지 않는다.

l  Local 프로퍼티는 컨텍스트에 존재하는 엔티티를 모두 반환한다. Deleted 상태인 엔티티는 반환하지 않는다. Deleted 상태인 엔티티까지 모두 가져오려면 context.ChangeTracker.Entries() 메소드를 사용하면 된다. Entries() 메소드 역시 실제 데이터베이스 쿼리는 실행하지 않고 현재 컨텍스트 내의 엔트리만을 반환한다.

 


<그림 3> 수정, 삭제 작업 후 SaveChanges()를 호출 했을 때의 결과

 

 

기타 변화들

 

EF 4.1에서는 SQL 쿼리를 직접 실행시키기 위해서 <리스트9>와 같이 SqlQuery, ExecuteSqlCommand 메소드를 지원한다.

 

var emps = context.Employees.SqlQuery("SELECT * FROM EMPLOYEES");

                foreach (var item in emps)

                {

                    Console.WriteLine("{0} {1}{2}", item.Id,

item.LastName,

item.FirstName);

                }

 

                var numberofEmps = context.Database.SqlQuery<int>("SELECT COUNT(*) FROM EMPLOYEES").First();

                Console.WriteLine("Total Employees : {0}", numberofEmps);

 

                var deletedCount = context.Database.ExecuteSqlCommand("DELETE FROM EMPLOYEES");

                Console.WriteLine("{0} employees deleted", deletedCount);

<리스트 9> SqlQuery, ExecuteSqlCommand를 통해서 실제 SQL 쿼리를 실행시킬 수 있다.

 

예제 코드에서처럼 엔트리의 SqlQuery와 달리 Database.SqlQuery는 엔티티 타입이 아닌 형태의 결과 값도 반환할 수 있도록 지원하고 있다. 그리고 context.Employees.SqlQuery("EXECUTE SP_GetAllEmployees")와 같이 Stored Procedure 이름을 통해 SP의 실행도 가능하다.

 

POCO 엔티티들의 변화를 감지하기 위해서 EF는 최초 쿼리시의 상태에 대한 스냅샷을 유지하는 등 여러 가지 작업을 진행한다. 따라서 당연히 여기에는 많은 리소스가 사용될 것인데 EF 4.1은 이를 자동으로 감지하는 기능이 디폴트로 작동되고 있다. 따라서 <리스트 10>과 같이 기능을 잠시 꺼두면 퍼포먼스 향상에 어느 정도 도움이 될 수 있다.

 

context.Configuration.AutoDetectChangesEnabled = false;

 

                foreach (var item in Employees)

                {

                    context.Employees.Add(item);

                }

 

                context.SaveChanges();

 

                context.Configuration.AutoDetectChangesEnabled = true;

<리스트 10> AutoDetectChangesEnabled는 디폴트로 true이다.

 

그리고, 당연히 이와 같은 기능은 Spring.NET과 같은 AOP 컨테이너나 MVC를 사용하고 있다면 ActionFilter 등을 사용해서 비즈니스 로직 코드에서는 제외해 놓는 것이 버그를 덜 양산할 수 있는 방법일 것이다.

 

마지막으로 EF 4.1정식 버전에 포함되지 않은 기능으로써 CompiledQuery 가 있다. EF 4.0에서는 쿼리를 실행할 때 CompiledQuery를 통해 상당한 성능 향상을 볼 수 있었다. 특히 복잡한 쿼리일 경우는 더욱 더 그랬는데, EF 4.1 정식 버전에서 이 기능이 빠져서 많은 개발자들의 불만이 있었으나 지난달 출시된 EF June 2011 CTP 버전에서부터 CompiledQuery 기능이 지원되며 이뿐만 아니라, 이전에는 수작업으로 일일이 CompileQuery.Compile() 메소드를 통해서 하던 일을 이제는 EF 상에서 실행되는 모든 LINQ 쿼리에 대해서 기본적으로 컴파일 되어 실행되므로 개발자들의 수고를 덜게 되었다.

 

그리고 ADO.NET Team에서는 EF 4.1을 지원하기 위하여 EF Power Tools를 개발하여 지원하고 있다. 아직 CTP1이지만 데이터베이스에서 DbContext 기반의 EF 코드를 생성하는 Reverse Engineering 기능을 지원해주며, Code First 방식으로 모델링을 할 때 한 가지 아쉬운 점인 EDMX 파일을 생성 해주어 전체 엔티티의 관계를 한 눈에 파악할 수 있도록 도와주는 등 유용한 기능들을 지원해주고 있으니 꼭 설치해서 활용해 보도록 하자.

 

이로써 총 3회의 연재를 마친다. 그 동안 관심 있게 연재 기사를 읽어주신 분들과 피드백을 주셨던 분들께 감사의 인사를 드리며 이번 연재에서는 EF 4.1을 주로 기능적인 관점에서 다루었지만, 필자의 블로그를 통해서 더욱 더 심층적이고 아키텍쳐적인 관점에서 EF를 다루도록 할 예정이니 앞으로도 많은 관심을 부탁 드리며 글을 맺도록 하겠다.     

 
p.s 본 기사의 저작권은 필자(권효중)만이 아닌 마이크로소프트웨어에게도 있습니다. 아쉽지만 저작권자의 허락없이 글 내용을 그대로 복사하는 것은 자제해주시길 부탁드립니다. 

 

 

Trackback 0 : Comment 0
◀ PREV : [1] : [2] : [3] : [4] : [5] : ... [10] : NEXT ▶