Nine MVP's Blog

24/09/2013

Entity Framework: Partial Update Object

Introduce

เป็นปากเสียงกันได้เลยสำหรับคนใช้ Entity Framework (EF) และ NHibernate (NH) ถึงเรื่องความสามารถต่างๆ
อาจจะเพราะ EF เกิดมาช้าเลยพากันขัดตาไปเสียหมด อาจจะเพราะว่า NH ได้ถูกนำโค้ดจาก Hibernate ที่เป็น JAVA มาเขียนเป็น C# ใหม่ด้วยกระมัง
ดังนั้นจึงมีของให้ใช้เยอะและตรงใจผู้พัฒนาค่อนข้างมาก

มาถึงเหตุให้ต้องเขียนตอนนี้ขึ้นมาเพราะมีคำถามว่า EF มี Merge() method สำหรับการ Update เฉพาะ property ที่เรากำหนดค่าได้หรือไม่

ยกตัวอย่างเช่นว่า มี 


public class User
{
    public int Id { get; set; }     

public string UserName { get; set; }

    public string Password { get; set; }

    public DateTime LastLogin { get; set; }

}

แต่ต้องการแค่อยากจะอัพเดทแค่ Password กับ LastLogin property เท่านั้นแบบนี้ โดยที่ไม่กำหนด property อื่นๆ
และเมื่อส่งไปอัพเดทใน database ต้องไม่ทำให้ property อื่นๆที่ไม่ได้กำหนดค่ากลายเป็น Null แบบตัวอย่างข้างล่างนี้


//pass user object and specify update properties
userDto.Password = “P@ssw0rd”;
userDto.LastLogin = DateTime.Now;db.PartialUpdate(userDto);

db.SaveChanges();

 

คำตอบคือ ไม่มีหรอกเสียใจด้วยครับ

แต่……ช้าก่อน  เราทำได้ครับ EF เก่งพอที่จะให้เราโชว์ฝีเท้าใส่โค้ดเข้าเพิ่มเติมเข้าไปเองได้

..

SOLUTION 1 Passing Property Name

วิธีแรกก็คือ ก็ส่งชื่อ Property ที่ต้องการจะ update เข้าไปเองก็สิ้นเรื่อง แต่ก็ไม่ถึงกับต้องมานั่งพิมพ์ค่า property name ใส่ string variable เข้าไปกันเองหรอกนะ มีวิธีที่ดีกว่านั้น


เริ่มต้นด้วยการติดตั้ง
Entity Framework ลงในโปรเจคก่อน
จากนั้นสร้าง
User class ขึ้นตาม introduce ด้านบน
ต่อมากก็สร้าง UserContext class สำหรับใช้งานตาม code ด้านล่างนี้


public partial class UserContext : DbContext
{
    public UserContext() : base(“name=UserDB”) { }    

public DbSet<User> Users { get; set; }

}

เสร็จแล้วให้เพิ่ม class file อีกอัน สำหรับสร้าง Partial Class เพื่อเพิ่ม method สำหรับทำ PartialUpdate ตาม codeด้านล่าง ซึ่งจะเห็นได้ว่ามีแต่ Method Name และ Signature ตัว code จะเพิ่มให้ใน block หลังๆครับ


public partial class UserContext
{     

public void PartialUpdate<T>(T entity, params Expression<Func<T, object>>[] propsToUpdate) where T : class {}    

private IEnumerable<string> ConfigPartialUpdate<T>(T entity, out DbEntityEntry<T> entry) where T : class { }

private IEnumerable<string> GetPrimaryKeys<T>() where T : class {}

}

จากโค้ดด้านบนจะเห็นได้ว่ามี 3 methods ซึ่งเป็น public 1, private 2  ซึ่งขออธิบายดังนี้

GetPrimaryKeys() Method เราเอาไว้ดึง primary key ทั้งหมดที่มีใน T class ที่เราส่งเข้ามาเพื่อทำการอัพเดท โดยดึง property name ออกมาเพื่อที่จะให้ข้ามการอัพเดทไป เพราะจะทำให้เกิด Error หากสั่งอัพเดทรายการนี้


private IEnumerable<string> GetPrimaryKeys<T>() where T : class
{    

ObjectContext objectthis = ((IObjectContextAdapter)this).ObjectContext;

ObjectSet<T> set = objectthis.CreateObjectSet<T>();

IEnumerable<string> keyNames = set.EntitySet.ElementType.KeyMembers.Select(k => k.Name);

    return keyNames;

}

ConfigPartialUpdate() Method จะมีหน้าที่ในการตรวจสอบ object ที่จะทำการอัพเดทโดยป้องกันการ attach object ซ้ำซ้อนใน DbContext


private IEnumerable<string> ConfigPartialUpdate<T>(T entity, out DbEntityEntry<T> entry) where T : class
{

    //set validate on save 
   

this.Configuration.ValidateOnSaveEnabled = false;


//get primary keys 

var pks = GetPrimaryKeys<T>();


//get object entry

entry = this.Entry(entity);


//if dbthis is already tracking T object then remove it all and attach new object

if (entry.State == EntityState.Detached && this.ChangeTracker.Entries<T>().Any())

{

foreach (DbEntityEntry<T> item in this.ChangeTracker.Entries<T>())

{

this.Entry<T>(item.Entity).State = EntityState.Detached;

}

this.Set<T>().Attach(entity);

}

//if T object is detached 

else if (entry.State == EntityState.Detached)

{

this.Set<T>().Attach(entity);

}

return pks;

}

PartialUpdate() Method เราจะใช้ในการกำหนด property ที่มีการแก้ไขค่าและบอกให้ EF ทำการอัพเดท
โดยใช้ params Expression<Func<T, object>>[] มาช่วยในการรับค่า property ที่ต้องการจะทำการอัพเดท


public void PartialUpdate<T>(T entity, params Expression<Func<T, object>>[] propsToUpdate) where T : class
{   

DbEntityEntry<T> entry;

var pks = ConfigPartialUpdate(entity, out entry);

foreach (var prop in propsToUpdate)

{

//if prop.name is pk go next loop

if (pks.Contains(prop.Name))

continue;

var errors = entry.Property(prop).GetValidationErrors();

if (errors.Count == 0)

{

entry.Property(prop).IsModified = true;

}

else

{

throw new DbEntityValidationException(string.Format({0} Model has errors please check.”, typeof(T).Name), errors.Select(o => new DbEntityValidationResult(entry, errors)));

}

}

}

..

SOLUTION 1 DEMO TEST

มาทดลอง method ที่เราเขียนกันไว้ดูด้วย code ข้างล่างนี้

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DemoPartialUpdate.DAL;
using DemoPartialUpdate.Domain;
namespace DemoPartialUpdate
{

class Program

{

static void Main(string[] args)

{

using (var db = new UserContext())

{

//If first run

if (db.Database.CreateIfNotExists())

{

for (int i = 1; i < 3; i++)

{

User user = new User();

user.Id = i;

user.UserName = “User” + i;

user.Password = “1234”;

user.LastLogin = DateTime.Now;

db.Users.Add(user);

}

db.SaveChanges();

}


var userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.WriteLine(“Before Update……..”);


foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

Console.WriteLine();


//Simulate DTO object

var userDto = new User();

userDto.Id = 1; //Id is a key it will not update

userDto.Password = “P@ssw0rd”; //Update

userDto.LastLogin = DateTime.Now.AddDays(1); //Update


//pass user object and specify update properties

db.PartialUpdate(userDto, x => x.Password, x => x.LastLogin);

db.SaveChanges();

userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine(“After Update……..”);

foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

}

}

}

}

จากโค้ดด้านบนเป็นการสั่งให้ Create Database หากไม่มีอยู่
และให้ทำการ
loop 2 เพื่อสร้าง User Object ลงไปใน Database
จากนั้นทำการ simulate ด้วยการสมมติว่ารับค่ามาจากภายนอก โดยกำหนด userDto Object ขึ้นมาและระบุ Id = 1 เพื่อบอกว่าให้อัพเดทค่าไปที่ User.Id =1 ต่อมากำหนดค่า 2 property คือ Password, LastLogin เพื่อต้องการให้อัพเดทลง Database
แล้วเราก็ดึงออกมาแสดงผลหลังทำการ
Insert ลงไปครั้งแรกโดยระบุ AsNoTracking() เพื่อไม่ให้ DbContext เก็บค่า object เอาไว้ติดตามในระบบ

และถึงเวลาทดสอบ เราก็เรียก db.PartialUpdate() โดยส่ง userDto เข้าไปและใช้ lambada x=> กำหนด property ที่ต้องการจะให้ update เข้าไป

ต่อมาก็สั่ง SaveChanges() เพื่อให้ commit รายการลง Database

สุดท้ายก็ดึงค่า ขึ้นมาจาก Database ทั้ง 2 User เพื่อแสดงค่าที่เปลี่ยนแปลงไปของ 2 property ที่สั่ง PartialUpdate ไป

เราไปดูใน Database หลังจากที่รันโค้ดชุดด้านบนนี้ไปแล้ว จะพบว่าเจอ 2 records ใน Users Table และมีการ update ค่าถูกต้องที่ User.Id = 1

ซึ่งเราไม่ได้กำหนดค่าให้ UserName Property หากใช้ Update โดยการ Attach() ของ DbContext ตรงๆ จะทำให้กลายเป็น Null ไปนั่นเอง

..

SOLUTION 2 Implement INotifyPropertyChanged

วิธีนี้จะไปยุ่งยากกับ Entity Object ในระบบทั้งหมด แต่จะทำให้ PartialUpdate method สะอาดไม่รกเต็มไปด้วย Paramter ว่าแล้วก็เริ่มลงมือ

แรกเริ่มเราจะเพิ่ม UpdatePartial() method เข้ามาใน partial class UserContext ใหม่อีก 1 method


public partial class UserContext
{

//////////


public void PartialUpdate<T>(T entity) where T : EntityBase

{


DbEntityEntry<T> entry;

var pks = ConfigPartialUpdate(entity, out entry);


foreach (var prop in entity.PropertyChangedList)

{

if (pks.Contains(prop))

continue;

var errors = entry.Property(prop).GetValidationErrors();


if (errors.Count == 0)

{

entry.Property(prop).IsModified = true;

}

else

{

throw new DbEntityValidationException(string.Format({0} Model has errors please check.”, typeof(T).Name), errors.Select(o => new DbEntityValidationResult(entry, errors)));

}

}

}

///////////////

}

เสร็จแล้วให้ทำการสร้าง NotifyPropertyChangedInvocatorAttribute Class เอาไว้ช่วยให้เราไม่ต้องระบุ property name ในตอนเรียก ตาม Code ด้านล่างนี้

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute {

public NotifyPropertyChangedInvocatorAttribute() }

public NotifyPropertyChangedInvocatorAttribute(string parameterName)

ParameterName = parameterName; }


public string ParameterName { get; private set; }

}

ต่อไปจะสร้าง BaseEntity สำหรับเป็นตัวช่วยให้ Entity Classของเราเก็บค่า Property Name ที่มีการเปลี่ยนแปลงค่าจากการ assign ค่าได้

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace DemoPartialUpdate.Domain
{

public abstract class EntityBase : INotifyPropertyChanged

{


protected EntityBase(){ }


private HashSet<string> _propertyChangeList = new HashSet<string>();


public HashSet<string> PropertyChangedList { get { return _propertyChangeList; } }


public void AddPropertyChanged(string propertyName)

{

if (!_propertyChangeList.Contains(propertyName))

_propertyChangeList.Add(propertyName);

}


public void ClearPropertyChanges()

{

_propertyChangeList.Clear();

}


public event PropertyChangedEventHandler PropertyChanged;

[NotifyPropertyChangedInvocator]

protected virtualvoid OnPropertyChanged([CallerMemberName] string propertyName = null)

{

PropertyChangedEventHandler handler = PropertyChanged;

if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));

AddPropertyChanged(propertyName);

}

}

}

จากนั้นไปเปิด User Class มาแก้ไขให้ได้ตามโค้ดด้านล่างนี้ โดยนำ EntityBase ไป Implement

using System;

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.Linq;

using System.Runtime.InteropServices;

using System.Text;

using System.Threading.Tasks;


namespace DemoPartialUpdate.Domain
{

public class User : EntityBase

{


private int _id;

private string _userName;

private string _password;

private DateTime _lastLogin;

public int Id {

get { return _id; }

set { _id = value; }

}

public string UserName {

get { return _userName; }

set {

_userName = value;

OnPropertyChanged();

}

}

public string Password {

get { return _password; }

set {

_password = value;

OnPropertyChanged();

}

}

public DateTime LastLogin {

get { return _lastLogin; }

set {

_lastLogin = value;

OnPropertyChanged();

}

}

}

}

SOLUTION 2 DEMO TEST

ให้เรา copy test class จาก Solution 1แล้วเปลี่ยนเป็น db.PartialUpdate(userDto);

ก่อนรันให้ไปลบ UserDB Database ทิ้งก่อน

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DemoPartialUpdate.DAL;
using DemoPartialUpdate.Domain;
namespace DemoPartialUpdate
{

class Program 

{

static void Main(string[] args)

{

using (var db = new UserContext())

{

//If first run

if (db.Database.CreateIfNotExists())

{

for (int i = 1; i < 3; i++) {

User user = new User();

user.Id = i;

user.UserName = “User” + i;

user.Password = “1234”;

user.LastLogin = DateTime.Now;

db.Users.Add(user);

}

db.SaveChanges();

}


var userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.WriteLine(“Before Update……..”);


foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

Console.WriteLine();


//Simulate DTO object

var userDto = new User();

userDto.Id = 1; //Id is a key it will not update

userDto.Password = “P@ssw0rd”; //Update

userDto.LastLogin = DateTime.Now.AddDays(1); //Update

//pass user object and specify update properties

db.PartialUpdate(userDto);

db.SaveChanges();

userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine(“After Update……..”);

foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

}

}

}

}

ซึ่งก็จะได้ผลลัพธ์เหมือนกับ Solution 1

Conclusion

สรุปว่าทั้งสองวิธีมีข้อดีและข้อเสียแตกต่างกัน แต่ได้ผลลัพธ์เดียวกัน

แบบที่ กำหนด property ที่ต้องการด้วยตัวเอง

ข้อดีคือ ใช้งานง่ายเหมาะกับระบบที่มีโมเดลอยู่แล้ว และไม่ต้องการจะไปรื้อโครงสร้างให้วุ่นวาย
ข้อเสียคือ จำเป็นต้องระบุ
property ที่ต้องการให้ Change ค่าเอง ซึ่งก็อาจจะหลงๆลืมๆส่งไปไม่ครบก็ได้ อาจจะทำงานผิดพลาด

แบบที่ ใช้ INotifyPropertyChanged เข้าช่วยในการตรวจสอบการเปลี่ยนแปลง

ข้อดีคือ ไม่ต้องกังวลว่าจะลืมส่ง Property อะไรไปบ้าง จุดเรียกใช้งานก็ใช้ง่าย
ข้อเสียคือ ต้องแก้ไข
entity class อาจจะกระทบงานเก่าที่มีอยู่

หวังว่าตอนนี้จะเป็นประโยชน์ ในการนำไปใช้งาน และช่วยเพิ่มความรู้และเทคนิคต่างๆให้เพื่อนๆกันได้ไม่มากก็น้อย

download source code  

จบครับ

Advertisements

22/04/2010

ORM Series : Introduce NHibernate

Filed under: ORM — Nine MVP @ 2:27 am

 

  1. ORM Series : Start to ORM World
  2. ORM Series : Introduce to NHibernate
  3. ORM Series : Introduce Entity Framework   next…

Programming Level

  •     Intermediate (ควรมีประสบการณ์เขียนโปรแกรมด้าน OOP, ADO.NET และ SQL )

Required Development Tools and Libraries

  1. Visual Studio 2010 all edition or later
  2. C# 3.0 feature
  3. SQL Server 2005 or later
  4. NHibernate 2.1.2 GA  <- click download

 

Introduce NHibernate

ORM ตัวนี้ผมได้ศึกษามาระยะนึงตั้งแต่ v1.0 จนปัจจุบันมาถึง v2.1.2 กันแล้ว ซึ่ง version นี่เองที่เราจะมาพูดกัน บทความตอนนี้จะเป็นการแนะนำเจ้า NHibernate ว่าคืออะไร มีกลไกการทำงานอย่างไร และวิธีการใช้งานในเบื้องต้น หากใครอยากอ่านประวัติความเป็นมาก็เข้าไปดูได้ที่นี่ครับ  nhibernate history

Figure 1.  Application 3 Layers with NHibernate

จากรูปด้านบนนั่นคือการทำ Architectural Layering ของ Software จะเห็นได้ว่ามีสัดส่วนพื้นฐานง่าย ๆ เป็น 3 ส่วนคือ

  1. Presentation Layer  โค้ดส่วนที่เกี่ยวข้องกับ User Interface ไม่ว่าจะตรวจสอบค่า จัดรูปแบบ ต่าง ๆ จะอยู่ที่ชั้นนี้ทั้งหมด
  2. Business Logic Layer  โค้ดส่วนที่เกี่ยวข้องกับ Business ต่าง ๆ ของระบบเช่น คำนวนค่าต่าง ๆ  การหายอด การขอดูรายการในระบบ และเป็นชั้นที่จัดเก็บ Business Objects ต่าง ๆ ด้วย 
  3. Data Access Layer  โค้ดส่วนที่ใช้จัดการ object ทั้ง Insert/ Delete/Update/Select ลง Database

และ กล่องแดง ๆ นั่นคือ Object Relational Mapping Framework ที่ชื่อ NHibernate ที่เราจะมาทำความรู้จักกันแบบเจาะโครงสร้าง ลงรายละเอียดการทำงานแต่ละส่วนกันให้เข้าใจ ตัวของ NHibernate เองก็จะเกี่ยวข้องอยู่กับ Data Access Layer โดยเราจะเรียกใช้ NHibernate จากใน DAL     ซึ่ง DAL อาจจะ implement interface เอาไว้ ให้ business เรียกใช้ก็ได้ช่วยได้การ decouple layer ได้สมบูรณ์ขึ้น

NHibernate Concept and Architecture

Figure 3. NHibernate Concept Overview

NHibernate จะทำหน้าที่เพียงเปลี่ยนลักษณะของ Objects <-ไปกลับ-> Database Recode ให้เรานั้นเอง       แนวคิดและวิธีการของ NHibernate ในการทำ mapping object เข้ากับ table ใน database จะใช้ XML Mapping, Attribute หรือ utility อย่าง Fluent NHibernate ก็ได้ (บทความนี้สอน XML Mapping)       โดยเราจะต้องตั้งค่าระบุว่า Class ใน Domain Object นี้จะไป Map เข้ากับ Table ไหน และ Property นี้จะไป map เข้ากับ Column ไหนใน Table รวมไปถึงการกำหนด property เพิ่มเติมได้ค่อนข้างยืดหยุ่นทีเดียว แต่ปัจจุบันยังไม่มี GUI Tool ให้เราใช้ จึงค่อนข้างจะต้องใช้ความรู้ในการสร้าง Mapping เช่นกัน

ต่อมาคือ Detail Architecture ของ NHibernate ดังรูปข้างล่างนี้

Figure 4. NHibernate Architecture

จากรูปด้านบนผมจะแนะนำ NHibernate Components ที่เราต้องรู้จักกันก่อนครับ (reference nhibernate document 2.0) โดยขอแนะนำที่จำเป็นสำหรับเริ่มต้นเรียนู้ไว้เพียงแค่นี้ก่อนครับ

  1. SessionFactory (NHibernate.ISessionFactory) เป็นอ็อปเจ็กที่ใช้เริ่มต้นการทำงานของ NHibernate โดยจะ load mapping file และ config ต่าง ๆ เข้ามาตั้งต้นใช้งาน อีกทั้งยังเป็น factory สำหรับสร้าง ISession ซึ่งอาจจะมีแคชของข้อมูล ในกรณีที่มีนำข้อมูลกลับมาใช้งานในระดับของ transactions, process หรือ cluster-level.

  2. Session (NHibernate.ISession) เป็นอ๊อปเจ็กที่ทำงานแบบเธรดเดี่ยว(Single Thread) มีช่วงการทำงานที่สั้น ซึ่งจะเป็นตัวที่ใช้ติดต่อระหว่าง application กับส่วนเก็บข้อมูลของระบบเช่น database เป็นต้น  โดยการทำงานนั้นจะเป็นการใช้งาน ADO.NET ในการติดต่อนั้นเอง ตลอดจนยังเป็น Factory ไว้สร้าง ITransaction เมื่อต้องการใช้งาน transaction  ที่สำคัญยังมีการใช้ Cache กับ Persistant Object ด้วย ช่วยให้เราสามารถทำงานกับ object graph และกรณีค้นหา object จาก ID เป็นต้น

  3. Persistent Objects and Collections : เป็นอ๊อปเจ็กที่มีช่วงชีวิตสั้น ๆ โดยเป็นการทำงานของเธรดเดี่ยว(Single Thread) โดยจะเก็บค่า persistent state เอาไว้ด้วย และอาจจะรวมไปถึง business method บางตัวไว้ด้วย ซึ่งก็แล้วแต่ Design ของแต่ละคน    ซึ่งอ๊อปเจ็กตัวนี้จะเป็นพิมพ์เดียวกับ Domain Model ที่ออกแบบไว้ โดยจะคงคุณสมบัติของ POCO ไว้ได้  และที่สำคัญอ๊อปเจ็กตัวนี้จะอยู่ใน Session ของ ISession  และหาก Session ดังกล่าวปิดลง อ๊อปเจ็กเหล่านี้จะลอยตัวจาก nhibernate และส่งต่อไปยัง layer อื่น ๆ ได้ (โดยอาจจะ map object ไปเป็น dto เพื่อส่งต่อไป presentation layer ก็ได้)

  4. Transient Objects and Collections : คือ อ๊อปเจ็กที่อยู่ใน system memory โดยเป็นอ๊อปเจ็กที่ถูกสร้างขึ้นใหม่ หรือเป็นอ๊อปเจ็กที่ไม่ได้อยู่ในการดูแลของ ISession  

 

ดาวน์โหลดและติดตั้ง NHibernate และ Fluent NHibernate

  1. ทำการดาวน์โหลด NHibernate 2.1.2 GA (<— กดเลย)
  2. แตกzip ไว้ใน C:\NHibernate\NHibernate-2.1.2.GA-bin 

 

ตั้งค่าให้ Visual Studio ใช้งาน NHibernate XML Intellisense

เนื่องจากการพิมพ์ xml configuration ของ nhibernate เองใน visual studio เองนั้น มีโอกาสผิดพลาดสูงมาก intellisense นี้จะช่วยลดความผิดพลาดในการทำงานครับ มี step ดังนี้

  1. หลังจากที่ download NHibernate มาแล้วให้ unzip และเข้าไปใน folder NHibernate-2.1.2.GA-bin\Required_Bins เลือก copy file nhibernate-configuration.xsd และ nhibernate-mapping.xsd
  2. จากนั้นนำไปวางใน visual studio ตาม path ดังนี้ “C:\Program Files\Microsoft Visual Studio X.0\xml\Schemas”
  3. จบขั้นตอน

 

มาสร้างโปรเจ็คแรกกัน First Application with NHibernate

เราจะใช้งานเจ้า NHibernate กับตัวอย่าง Application ง่าย ๆ กันครับ โดยเราจะสร้าง Data Access Layer สำหรับ Customer Profile ซึ่งจะมีไดอะแกรมต่าง ๆ เป็นโมเดลดังนี้

1. Business Domain Class Diagram

Figure 5. Customer Profile Class Diagram

2. Database: Tables Structure

Figure 6. Customer Profile Database Model

หลังจากที่ได้ดู Class Diagram และ Database Diagram ทั้ง 2 ไดอะแกรมไปแล้วเราก็จะมาทำการสร้างงานโดยใช้ Visual Studio กัน

สร้าง Database

1. สร้าง database ชื่อ CustomerDB

2. ทำการสร้างเทเบิ้ลด้วย script ล่างนี้

USE [CustomerDB]
GO

CREATE TABLE [dbo].[Customer](
    [Id] [uniqueidentifier] NOT NULL,
    [FirstName] [nvarchar](50) NULL,
    [LastName] [nvarchar](50) NULL,
    [Age] [int] NULL,
    [Sex] [char](5) NULL,
CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Address](
    [Id] [uniqueidentifier] NOT NULL,
    [Road] [nvarchar](255) NOT NULL,
    [City] [nvarchar](255) NOT NULL,
    [Country] [nvarchar](100) NOT NULL,
    [PostCode] [char](5) NOT NULL,
    [Owner_Id] [uniqueidentifier] NULL,
CONSTRAINT [PK_Address] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Address]  WITH CHECK ADD  CONSTRAINT [FK_Address_Customer] FOREIGN KEY([Owner_Id])
REFERENCES [dbo].[Customer] ([Id])
GO
ALTER TABLE [dbo].[Address] CHECK CONSTRAINT [FK_Address_Customer]
GO

 

 

เริ่มสร้างโปรแกรม

มีขั้นตอนดังนี้

1. สร้างโปรเจ็คไฟล์

1.1  ไปที่ Menu –> File –> New –> Project… –> จากนั้นเลือก Visual C# –> .NET Framework 3.5 –> Class Library –> ใส่ค่า Name:= Vol1.Sample01.MyDAL, Location:= c:\ORMSeries, Solution name:= ORMSeries –>  กด OK

1.2 จะได้โปรเจ็คที่มีโครงสร้างดังภาพด้านล่างนี้

และให้ลบ Class1.cs ทิ้ง

 

2. เรียกใช้ NHibernate library

2.1 เรียกใช้ NHibernate คลิ๊กขวาที่ Vol1.Sample01.MyDal Project –> เลือก add reference… –> browse ไปที่ “C:\NHibernate\NHibernate-2.1.2.GA-bin\Required_Bins” –> เลือก NHibernate.dll กด OK

       

2.2 เรียกใช้ Dynamic Proxy Library คลิ๊กขวาที่ Vol1.Sample01.MyDal Project –> เลือก add reference… –> browse ไปที่ “C:\NHibernate\NHibernate-2.1.2.GA-bin\Required_For_LazyLoading\Spring” –> เลือก NHibernate.ByteCode.Spring.dll กด OK

2.3 มาตรวจสอบ Library ใน Reference กันครับ

 

3. สร้าง Entity Class

3.1 สร้างโฟลเดอร์ โดยคลิ๊กขวาที่โปรเจ็ค –> Add –> New Folder ตั้งชื่อว่า “DomainEntities”

3.1 สร้าง Customer Class –> คลิ๊กขวาที่โฟลเดอร์ “DomainEntities” –> Add –> Class… –> ตั้งชื่อว่า  “Customer.cs”  และ copy code ด้านล่างไปแปะ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Vol1.Sample01.MyDAL.DomainEntities
{
    public class Customer
    {
        public virtual Guid Id { get; private set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual int Age { get; set; }
        public virtual Sex Sex { get; set; }
        public virtual IList<Address> Addresses { get; set; }

        public Customer()
        {
            Addresses = new List<Address>();
        }
    }

    public enum Sex
    {
        Male, Female
    }
}

 

3.2 สร้าง Address Class –> คลิ๊กขวาที่โฟลเดอร์ “DomainEntities” –> Add –> Class… –> ตั้งชื่อว่า  “Address.cs”  และ copy code ในตารางด้านล่างไปแปะ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Vol1.Sample01.MyDAL.DomainEntities
{
    public class Address
    {
        public virtual Guid Id { get; private set; }
        public virtual string Road { get; set; }
        public virtual string City { get; set; }
        public virtual string Country { get; set; }
        public virtual string PostCode { get; set; }

        public Address()
        {
        }

    }
}

 

4. สร้าง Mapping File ใช้สำหรับบอก NHibernate ว่า assembly file –>Class–>Property ใด map เข้ากับ Database –>Table –> Colume ใดบ้าง

     4.1  สร้าง mapping สำหรับ Customer Class: คลิ๊กขวาที่โฟลเดอร์ “DomainEntities” –> Add –> New Item… –> Xml File  –> ตั้งชื่อว่า  “Customer.hbm.xml” –> คลิ๊ก Add

       

     จากนั้นและ copy xml code ในตารางด้านล่างไปแปะ และทำการ Save

 

<?xml version="1.0" encoding="utf-8" ?>
<!–  เริ่มต้นสร้าง mapping  พร้อมระบุ assembly name ของ class ที่บรรจุอยู่ภายใน   และระบุความลึกของ namespace ไปถึงตำแหน่งที่ class อยู่  –>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Vol1.Sample01.MyDAL"
                   namespace="Vol1.Sample01.MyDAL.DomainEntities" >

  <!–  เริ่ม map class พร้อมระบุ table ที่จะ map เข้าด้วยกัน  –>
  <class name="Customer" table="Customer">

    <!– กำหนด id ของ class map เข้ากับ Pk ของ column –>
    <id name="Id" column="Id">
      <!– ระบุให้ NHibernate ทำการ auto assign Guid ให้กับ Property นี้ –>
      <generator class="guid"/>     
    </id>
    <!– เนื่องจากชื่อ property เหมือนกับ column ใน DB จึงไม่ต้องระบุ column name –>
    <property name="FirstName" column="FirstName"/>  
    <property name="LastName" />
    <property name="Age" />
    <property name="Sex"  />
     <!–bag คือ collection object มีมากกว่า 1 ซึ่งเป็นลักษณะ 1:M ซึ่ง customer สามารถมี address
    ได้มากกว่า 1 และกำหนดให้ collection type เป็น generic–>
    <bag name="Addresses"  generic="true" cascade="all" >
       <!–ระบุ colume FK name ของ table address–>
      <key column="Owner_Id" not-null="true" foreign-key="Id"/>
       <!–ระบุ class name ที่จะดึงข้อมูลมาใส่ collection–>
      <one-to-many class="Address"/>
    </bag>
  </class>
</hibernate-mapping>

 

4.2  สร้าง mapping สำหรับ Address Class: คลิ๊กขวาที่โฟลเดอร์ “DomainEntities” –> Add –> New Item… –> Xml File  –> ตั้งชื่อว่า  “Address.hbm.xml” –> คลิ๊ก Add

 

   

และ copy xml code ด้านล่างไปแปะ

<?xml version="1.0" encoding="utf-8" ?>
<!–  เริ่มต้นสร้าง mapping  พร้อมระบุ assembly name ของ class ที่บรรจุอยู่ภายใน   และระบุความลึกของ namespace ไปถึงตำแหน่งที่ class อยู่  –>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Vol1.Sample01.MyDAL"
                   namespace="Vol1.Sample01.MyDAL.DomainEntities" >
  <!–  เริ่ม map class พร้อมระบุ table ที่จะ map เข้าด้วยกัน  –>
  <class name="Address" table="Address">

    <!– กำหนด id ของ class map เข้ากับ Pk ของ column –>
    <id name="Id" column="Id">
      <!– ระบุให้ NHibernate ทำการ auto assign Guid ให้กับ Property นี้ –>
      <generator class="guid"/>
    </id>

    <!– เนื่องจากชื่อ property เหมือนกับ column ใน DB จึงไม่ต้องระบุ column name ก็ได้ –>
    <property name="Road" column="Road"/>
    <property name="City" />
    <property name="Country" />
    <property name="PostCode" />
  </class>
</hibernate-mapping>

 

 

4.3  จะได้ output file ตามรูปด้านล่างนี้

 

และใน Solution Explorer ให้ทำการคลิ๊กเลือก Customer.hbm.xml และไปปรับค่าที่ Property windows แก้ไขค่า Build Action = Embeded Resource ตามรูปด้านล่าง

ทำซ้ำกับ file Address.hbm.xml เช่นเดียวกัน

 

 

 

5. สร้าง Config file สำหรับการตั้งค่าการทำงานของ NHibernate ทั้งหมด

5.1  Solution Explorer คลิ๊กขวาที่ Vol1.Sample01.MyDAL –> เลือก Add –> New Item… –> XML File –> ตั้งชื่อว่า “hibernate.cfg.xml” –> คลิ๊ก Add  จากนั้น copy code ในตารางด้านล่างไปแปะ และทำการบันทุึก

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <!– สร้าง session factory –>
  <session-factory name="Vol1.Sample01.MyDAL">
    <!– cache provider เป็นการระบุให้ nh ใช้ cache –>
    <property name="cache.provider_class">NHibernate.Cache.HashtableCacheProvider, NHibernate</property>
    <!– กำหนดให้ใช้ cache ของ query  –>
    <property name="cache.use_query_cache">true</property>
    <!– ไม่ใช้การตรวจสอบในตอนสร้าง session –>
    <property name="query.startup_check">false</property>

    <!– จำนวน batch ในการทำงาน –>
    <property name="adonet.batch_size">10</property>
    <!– กำหนด transaction level –>
    <property name="connection.isolation">ReadCommitted</property>

    <!– กำหนดให้ gen "" ครอบ sql ที่เป็น keyword ป้องกัน error –>
    <property name="hbm2ddl.keywords">auto-quote</property>
    <!– กำหนดให้ show sql ที่ gen ได้ ที่ console log –>
    <property name="show_sql">false</property>
    <!– กำหนดให้ format sql ให้อ่านง่าย เว้นบรรทัด ที่ console log–>
    <property name="format_sql">true</property>

    <!– กำหนด driver สำหรับติดต่อ dabase  –>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <!– กำหนดตัวแปลง sql สำหรับ database  –>
    <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
    <property name="connection.connection_string">Server=.;initial catalog=CustomerDB;Integrated Security=SSPI</property>

    <property name="use_outer_join">true</property>
    <property name="command_timeout">444</property>
    <property name="query.substitutions">true 1, false 0, yes ‘Y’, no ‘N'</property>
    <property name="adonet.wrap_result_sets">false</property>
    <!– กำหนด DI สำหรับสร้าง proxy class ปกติเดิม NH จะใช้ castle แต่สำหรับ v2.1.x นั้น มีให้เลือกใช้ Spring.Net, Castle, Linfu –>
    <property name="proxyfactory.factory_class">NHibernate.ByteCode.Spring.ProxyFactoryFactory, NHibernate.ByteCode.Spring</property>

    <!– !!!!!!!!!  กำหนด assembly name ที่ต้องการจะใช้ NH ทำการ load เข้ามาทำงาน    !!!!!!!!!! –>
    <mapping assembly="Vol1.Sample01.MyDAL"/>
  </session-factory>
</hibernate-configuration>

 

 

5.2 บังคับ Client Project ให้เอา config file ของ NH ไปใช้งานด้วย  โดยไปที่ Solution Explorer เลือก hibernate.cfg.xml –> เลื่อนไปดูที่ Properties window ปรับค่า Build Action = Content และ Copy to Output Deirectory = Copy always

 

 

6. สร้าง Helper เพื่อไว้เรียกใช้ Session ของ NH ได้สะดวกขึ้น (ตัวอย่างแบบง่าย สำหรับ Client App)

6.1  ไปที่ Solution Explorer เลือก Vol1.Sample01.MyDAL คลิ๊กขวาเลือก –> Add –> Class..  ตั้งชื่อว่า “NHibernateHelper.cs” ให้ Copy code จากด้านล่างนี้แปะ และทำการ save

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using NHibernate;
using NHibernate.Cfg;

namespace Vol1.Sample01.MyDAL
{
    /// <summary>
    /// Helper นี้เหมาะสำหรับ basic client application (ไม่เหมาะกับ web app)
    /// </summary>

    public static class NHibernateHelper
    {
        // กำหนดตัวแปร factory ไว้เป็น static เมื่อสร้างขึ้นใช้งานใน App ไม่ต้องสร้างใหม่ทุกครั้งเนื่องจาก จะใช้เวลาในการสร้างนานมาก
        private static readonly ISessionFactory sessionFactory;
        // สร้าง Session
        private static ISession currentSession;

        static NHibernateHelper()
        {
            // ทำการสร้าง factory
            sessionFactory = new Configuration().Configure().BuildSessionFactory();
        }

        // เปิด session เพื่อทำงานกับ DB
        public static ISession GetCurrentSession()
        {
            if (currentSession == null)
            {
                // เปิด session ใหม่
                currentSession = sessionFactory.OpenSession();
            }
            return currentSession;
        }

        // ทำการปิด session คล้ายการปิด db connection
        public static void CloseSession()
        {
            if (sessionFactory == null)
            {
                return;
            }
            currentSession.Close();
        }

        // ปิด factory ยกเลิกการใช้งาน
        public static void CloseSessionFactory()
        {
            if (sessionFactory != null)
            {
                sessionFactory.Close();
            }
        }
    }

}

 

7. สั่ง Build ด้วยการกด Ctrl + Shift + B  หรือไปที่ Menu –> Build –> Build Solution  เมื่อสำเร็จ จะได้ DAL โปรเจ็คที่มีรูปร่างหน้าต่างประมาณนี้

 

 

8.  นำ MyDAL ของเราไปใช้งาน

8.1 ทำการสร้าง Client Project  เริ่มต้นไปที่ Menu –> File –> Add –> New Project..   ตั้งชื่อว่า ClientTest  และกด Add

    

8.2 ทำการ Add Reference ของ NHibernate  ย้อนไปทำตามข้อ 2.1 และ 2.2

8.3 ทำการ Add Reference ของ Vol1.Sample01.MyDAL  โดยไปที่ Solution Explorer –> เลือก TestClient Project และทำตามรูปด้างล่าง

    

    

     จะได้ output ตามรูปนี้

      

 

8.4  เปิด Program.cs ขึ้นมาทำการเขียนโค้ดด้านล่างนี้

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClientTest
{
    class Program
    {
        static void Main(string[] args)
        {
           //// เปิด session เพื่อเตรียมทำงานกับ Database
            NHibernate.ISession session = Vol1.Sample01.MyDAL.NHibernateHelper.GetCurrentSession();
            // ใช้งาน transaction
            NHibernate.ITransaction tx = session.BeginTransaction();

           #region Save
            // ทำการสร้าง customer instance
            Vol1.Sample01.MyDAL.DomainEntities.Customer cust1 = new Vol1.Sample01.MyDAL.DomainEntities.Customer();
            cust1.FirstName = "Somchai";
            cust1.LastName = "Sakleoi";
            cust1.Sex = Vol1.Sample01.MyDAL.DomainEntities.Sex.Male;
            cust1.Age = 25;

            // ทำการสร้าง address ของ customer
            Vol1.Sample01.MyDAL.DomainEntities.Address addr = new Vol1.Sample01.MyDAL.DomainEntities.Address();
            addr.Road = "Ratchada";
            addr.City = "Huaykwang";
            addr.Country = "Thailand";
            addr.PostCode = "10310";

           // เพิ่ม address ให้ customer
            cust1.Addresses.Add(addr);

           // สั่ง Save ลง Database
            session.Save(cust1);
            #endregion

            #region Search
            // ทำการค้นหา customer object ใน table ด้วยเงื่อนไข firstname like ‘Som%’
            var res = session.CreateCriteria(typeof(Vol1.Sample01.MyDAL.DomainEntities.Customer))
                .Add(NHibernate.Criterion.Restrictions.Like("FirstName", "Som%")).List<Vol1.Sample01.MyDAL.DomainEntities.Customer>() ;

            Console.WriteLine(" Search customer by FirstName ‘Som%’ found {0} object ", res.Count);
            foreach (var customer in res)
            {
                Console.WriteLine("customer firstname={0} lastname={1}  have address {2} ", customer.FirstName,
                                  customer.LastName, customer.Addresses.Count);
            }
           #endregion

            #region Update
            var custx = res[0];
           // แก้ไขค่า firstname ของ custx
            custx.FirstName = "XXX";
            // สั่ง update custx ใน table
            session.Update(custx);
            Console.WriteLine("Update customer name from ‘{0}’ to ‘{1}’",res[0].FirstName, custx.FirstName);
           #endregion

            #region Delete
            //ลบ object custx ออกจาก table
            session.Delete(custx);
            Console.WriteLine("Delete customer name ‘{0}’ ", custx.FirstName);
            #endregion

            // สั่งให้ commit transaction
            tx.Commit();
            // สั่งปิด Session
            Vol1.Sample01.MyDAL.NHibernateHelper.CloseSession();

            Console.ReadKey();
        }
    }
}

 

8.5  เมื่อทำการกด Run โปรแกรมด้วยการกด F5 จะได้ output ตามรูปด้านล่าง

 

อธิบายการทำงานของ Code ด้านบน

จะเห็นได้ว่ามีการสร้าง customer และ address  และได้มีการสั่งให้ Save ซึ่ง NHibernate จะบันทึกทั้ง Customer , Address ลงเทเบิ้ล

           // สั่ง Save ลง Database
            session.Save(cust1);

จากนั้นก็ได้มีการ Search customer object ด้วยการกำหนดเงื่อนไข FirstName Like ‘Som%’  ซึ่งส่งค่ากลับมาเป็น List<Customer>

          // ทำการค้นหา customer object ใน table ด้วยเงื่อนไข firstname like ‘Som%’
           var res = session.CreateCriteria(typeof(Vol1.Sample01.MyDAL.DomainEntities.Customer))
                .Add(NHibernate.Criterion.Restrictions.Like("FirstName", "Som%")).List<Vol1.Sample01.MyDAL.DomainEntities.Customer>() ;

ต่อมาจะสั่งให้ update customer ที่ search มาได้

           // แก้ไขค่า firstname ของ custx
            custx.FirstName = "XXX";
            // สั่ง update custx ใน table
            session.Update(custx);

 

และสุดท้าย คำสั่ง Delete 

            //ลบ object custx ออกจาก table
            session.Delete(custx);

 

สรุปตอนท้าย

จะเห็นว่าการใช้งาน NHibernate ค่อนข้างจะมีขั้นตอนซับซ้อน ซึ่งนี่เป็นสิ่งที่ทาง Hibernate (java) เขาใช้งานกันมายาวนาน และขึ้นชื่อว่า Hibernate นั้นสร้างชื่อเสียงเป็นอย่างมาก   เพราะความยืดหยุ่นในการทำงานและประสิทธิภาพการทำงานนั้นจัดได้ว่าในระดับที่ดีและไว้ใจได้เนื่องจากใช้งานกันมายาวนาน  ยังรวมไปถึงเหล่า contributor ต่าง ๆ ที่คอยพัฒนาไลบราลี่ไว้เสริมการทำงานอื่นๆ ให้ NHibernate อีกมากมาย      ซึ่ง NHibernate ได้พัฒนาโดยอิงตาม Hibernate ทุกประการ     จากบทความนี้เป็นเพียงพื้นฐานเริ่มต้นเล็กน้อย  ก่อนจะต่อยอดไปศึกษาในเรื่องที่ซับซ้อนกว่านี้เช่น Mapping Pattern, Performance Tuning, Caching, Transaction และ การใช้งานร่วมกับ framework อื่น ๆ เช่น Spring.Net, AOP เป็นต้น 

ซึ่งในตอนต่อไปจะมาสอนการสร้าง Mapping ในรูปแบบต่างๆ ที่ซับซ้อนกว่านี้ด้วย Real World Business Domain

 


 

สามารถ download source code ได้ที่นี่

ORMSeries Code Sample Download Here!!

 

Chalermpon Areepong Nine (นาย)

Microsoft MVP Thailand ASP.NET

email : nine_biz-talk.net at hotmail dot com

09/07/2009

ORM Series : Start to ORM World

Filed under: ORM — Nine MVP @ 8:53 am

Agenda:

  1. ORM Series : Start to ORM World
  2. ORM Series : NHibernate

Introduce

ห่างหายไปนานครับ กับการเขียนบทความลง GF นี่เรียกว่ากลับมาในรอบปีเลยก็ว่าได้ ส่วนใหญ่จะไปเน้นงาน Offline Meeting เสียมากกว่า เรามาเข้าเรื่องกัน

ยุคของการพัฒนาระบบงานคอมพิวเตอร์นั้น ได้มีวิวัฒนาการมาเรื่อย ๆ จนถึงปัจจุบันนี้พวกเราเหล่านักพัฒนาทั้งหลาย คงได้พบเจอระบบงานที่คุ้นเคยกันมากที่สุด คงหนีไม่พ้นระบบงานธุรกิจที่ต้องเก็บบันทึกข้อมูล เรียกข้อมูลมาทำรายงานและประมวลผลต่าง ๆ

พวกเราเป็นนักพัฒนาโดยใช้เทคโนโลยีของไมโครซอฟต์ เริ่มต้นที่ผมได้รู้จัก Library ที่ไว้ใช้เข้าถึง Database มีทั้ง DAO, RDO และ ADO ซึ่งตอนนั้นก็ยุคของ Microsoft Access และ VB6 กำลังรุ่งเรืองเลยก็ว่าได้ โดยการทำงานจะมุ่งเน้นไปที่ Database, Table, Row, Column, StoreProcedure และ Function สนใจเพียงลักษณะของโครงสร้าง column ที่จะดึงมาแสดงผลข้อมูล การคำนวนค่าใน database และส่งกลับมายังโปรแกรมเพื่อทำงานใด ๆ ต่อไป

Figure 1. Currently Microsoft Data Access Component (msdn reference)

ต่อมายุคคือยุคของ ADO.NET ซึ่งย้ายมาทำงานบน .NET Framework เป็นการเพิ่มความสามารถของ ADO โดยเสนอ Dataset Class โดยทำงานในลักษณะของ multi table, relation และอื่น ๆ อีกมากมาย ในตอนนั้นเรียกว่าฮือฮากันเลยทีเดียว เกิดการเปลี่ยนแปลงการเขียนโปรแกรมกันพอสมควร (รวมผมด้วย) ซึ่ง Dataset จะมีทั้ง Untyped และ Typed ให้เลือกใช้งานกัน จนทุกวันนี้ก็ยังมีใช้งานกันแพร่หลาย อีกทั้งมี library ตัวใหม่ๆ อย่าง TableAdapter เป็นต้น ปัจจุบันนี้ Dataset มีใช้งานทั่วไปทั้งในระบบงานเล็ก ๆ ไปจนถึงระบบงานขนาดใหญ่ก็ยังคงพบเห็น เพราะว่ามีความรวดเร็วในการพัฒนา มีความยืดหยุ่นสูงในการทำงานของ code และยังสามารถ serialize ตัวเองไปเป็น xml เพื่อส่งผ่านไปทาง Web Service Technology ซึ่งหลังจากที่ใช้งานมาตลอดยุคหนึ่ง จนมาถึงปัจจุบันในความเห็นส่วนตัว ผมมองว่า Dataset เหมาะสมกับงานที่มุ่งเน้นไปที่การออกแบบระบบทาง Functionality + Data เพราะมองเห็นไปถึง Database Schemay โดยถูกกำหนดการทำงานโดย Business Functionality สิ่งนี้เองที่ทำให้ Dataset ยังคงอยู่จนทุกวันนี้ และจะยังคงอยู่ต่อไปอีกนาน เพราะว่าบุคลากรนักพัฒนาส่วนใหญ่ยังคงใช้ Functionality Design and Programming หรือเรียกได้อีกแบบว่า Data Driven Programming (ตัวอย่าง.. หลังได้ ERD, User Screen, Requirement แล้วลุยพัฒนาเลย)

.NET Framework เป็น framework ที่พัฒนาขึ้นให้ภาษาต่าง ๆ ที่ต้องการจะเข้ากับ framework นั้นต้องตรงตามมาตรฐานที่กำหนดไว้ และกันนี้เองทำให้ภาษาเหล่านั้นต้อง support Object Oriented ไปด้วยโดยปริยาย และด้วยความสามารถนี้ทำให้การ port library, framework ต่าง ๆ จากกลุ่ม OO Language อย่าง JAVA มีความนั้นเป็นไปได้สูง และนี่ก็เป็นจุดเปลี่ยนของ Microsoft Technology เหล่านักพัฒนาเริ่มทะยอยหันกันมาศึกษาและแลกเปลี่ยนความรู้ของ .NET Framework กันแพร่หลาย จนกระทั่งได้มีการนำ open source จาก Java หลาย ๆ ตัวย้ายลงมาพัฒนาบน .NET Framework ให้เราได้เห็นกันจนทุกวันนี้ ไม่ว่าจะเป็น Unit Testing Framework, Logging Framework, MVC Framework, DI และ Object Relational Mapping Framework ก็คือที่เราจะมาคุยกัน

ที่มาของ ORM โลกปัจจุบันเราก็ทราบกันดีว่า Object Oriented Analysis Design and Programming นั้น ช่วยให้เราพัฒนาระบบงานให้มีความใกล้เคียงกับโลกในชีวิตจริง ข้อดีต่าง ๆ ทางด้านการพัฒนาระบบ การดูแลรักษา การพัฒนาต่อยอด ล้วนแล้วแต่เหนือกว่าการพัฒนาแบบ Functionality Style ค่อนข้างมาก แต่ด้วย Database Technology ในปัจจุบันนี้ RDBMS นั้นเป็นที่นิยมใช้งานในปัจจุบันและแพร่หลาย แถมยังมีทีท่าว่าจะรุ่งเรืองต่อไปอีกนาน นี่ก็เป็นปัญหาของการพัฒนาระบบงานแบบ Object Oriented อย่างมาก เนื่องจากคอนเซ็ปการทำงานนั้นแตกต่างกัน จึงกลายเป็นที่มาของ ORM

What’s Object Relational Mapping

ลักษณะของ Business Object จะมีความหลากหลายของโครงสร้าง ตลอดจนลักษณะความสัมพันธ์ระหว่างอ็อปเจ็คก็มีความซับซ้อนทั้งแบบ Hierarchy, Composite และ Association ซึ่งแตกต่างจาก Table Relation ในดาต้าเบสมาก ทำให้ผู้พัฒนาต้องเกิดปัญหาในการจัดการเก็บข้อมูลลง RDBMS

Figure 2. Table Relation Model

Figure 3. Object Relation Model

จากรูปด้านบน จะเป็นการเปรียบเทียบระหว่าง Table Relation กับ Object Relation  จะพบว่ามีลักษณะใกล้เคียง แต่ใน real world นั้นบางครั้ง Object Relation อาจจะมีความซับซ้อนกว่านี้เช่น  Register Customer Class และ Non Register Customer Class ได้ inheritance Customer Class ไป และการเข้าถึง Object อื่น ๆ ที่เกี่ยวข้องกันจาก Diagram เดิมนั้นจะเปลี่ยนไป เช่น Register Customer Class สามารถวางบิลได้ แต่ Non Register Customer Class จะไม่สามารถวางบิลได้ เพราะเงื่อนไขว่า Non Register Customer ไม่สามารถ Order สินค้าได้เป็นต้น

ความซับซ้อนของ Business Object ที่ได้กล่าวไปข้างต้นนั้นเป็นจุดที่สร้างปัญหาในการเก็บข้อมูลลง Table ใน Database เป็นอย่างมาก จนได้เกิดหลักการ Map Object เข้ากับ Table ใน Database ออกมาเป็นหลาย Pattern ซึ่งจะเป็นลักษณะของ Best Practice แต่ไม่ใช่ Standard  และจาก Pattern ต่าง ๆ เหล่านี้ก็ไม่ได้ช่วยลดปัญหาในการพัฒนาตัว ORM Library ให้กับชาว OOP ลงเท่าไหร่ เพราะต้องเสียเวลาในการพัฒนา DAL ที่ทำหน้าที่เป็น ORM อีกชั้นนึง คอยไปดึง Column จาก Table ต่าง ๆ มาสร้างเป็น Object ในระบบ และต้องนำ Property ของ Object ในระบบไป insert ลงยัง Column ใน Table ต่าง ๆ และระดับของ Object ที่มี Ralation กันลึก ๆ หลายระดับ แค่นี้ก็แสบสมองแล้วครับ 

หลายคนก็พยายามเขียน DAL Method ขึ้นเองเพื่อใช้ตอบสนอง Business Method ที่ต้องการใช้งาน Object แต่จากที่กล่าวไว้เบื้องต้นด้านบน นอกจะจะเสียเวลาในการพัฒนาแล้ว ยังเสี่ยงต่อ Bug ที่จะเกิดขึ้นในอนาคต และดูแล้วยากต่อการแก้ไขเปลี่ยนแปลงในตอนหลังด้วย  แต่ไม่ต้องกังวลครับ ปัญหาเหล่านี้ได้มีนักพัฒนาแก้ปัญหาให้ท่านแล้ว ซึ่งมีผู้พัฒนา ORM Framework ออกมาหลายรายทั้งในรูป Open Source และ Commercial แต่จะใช้พูดถึงในบทความ จะขอยกตัวอย่างแค่ ADO.NET Entity Framework และ NHibernate เท่านั้น เพื่อไม่ให้เกิดความสับสนในการศึกษาครับ (จริง ๆ ตัวเดียวก็ปวดหัวแล้ว)

เกรงว่าหากบรรยายไปจะยืดเยื้อ อ่านต่อเกี่ยวกับ ORM ที่นี่ http://www.agiledata.org/essays/mappingObjects.html อ่านจบแล้วจะเข้าใจโลกของ ORM ขึ้นอีกครับ

Object Relational Mapping Framework

ORM Framework จะช่วยลดงานพัฒนาส่วนของการติดต่อกับ Database ให้ครับ โดยคุณจะสามารถ Find/Add/Update/Delete Object ผ่านลงไปยัง Database ได้โดยไม่ต้องเขียน Code จัดการเอง รวมไปถึงข้อดีต่าง ๆ จาก ORM Framework แต่ละตัวเช่น Caching, Validation, etc. ซึ่ง Functionality ที่ ORM Framework จะต้องมีและควรจะมีนั้นมีประมาณนี้ครับ

    • รองรับการทำ Inheritance ระหว่าง Entities, และจะต้องเรียกใช้งานในลักษณะของ Polymorphism
    • สามารถรองรับ relations แบบ 1-1, 1-n, n-n
    • รองรับการทำ transactions
    • Aggregates สำหรับ จัดการดึง object (equivalent to SQL’s SUM, AVG, MIN, MAX, COUNT)
    • รองรับการทำ grouping (SQL’s GROUP BY)
    • Support Database อย่างน้อย 1 ชนิด (เอ… งง)
    • มี OQL, OPath ใช้ในการดึง object
    • รองรับการทำ Databinding (ส่วนใหญ่ IList ก็เป็น standard ของ .NET Data Control แล้ว)
    • ทำ Join (inner, outer)
    • Concurrency Management
    • Database specify type พวก column type แปลก ๆ อย่าง uniqueidentify, sequence ประมาณนั้น
    • สามารถ Map 1 object มาจากหลาย ๆ Table ได้
    • สามารถดึงข้อมุลจาก Table เดียวไปเป็น หลาย ๆ object ได้
    • ควรจะมี GUI ช่วยในการทำ Mapping
    • ควรจะมี Code Gen
    • ควรจะสร้าง Table Schema ได้จาก MAP
    • ระบบควรจะมีประสิทธิภาพในการทำงานสูงโดยรวม
    • สามารถทำ Lazy Loading Object ได้
    • สามารถสร้าง dynamic sql จาก code ได้เอง
    • มี Cache สำหรับ Data ที่จำเป็น
    • สามารถ optimized queries ให้เหมาะสมได้
    • จัดการลักษณะ Circular References
    • ทำ Cascade updates. Bulk updates or deletions
    • …. อีกมากมาย

สาเหตุที่ต้องทำให้คุณควรจะเลือกใช้ ORM Framework ที่มีอยู่แล้วในปัจจุบัน นั้นมีดังนี้

  1. ORM ช่วยลดเวลาในการพัฒนางานของคุณลง 20-50% รวมกรณีต่าง ๆ ในระบบเล็ก ๆ ที่มีประมาณ 10 tables ใน Database และมี Domain Object ราว ๆ 20 objects คุณต้องเขียนโค้ดในการจัดการหลายหมื่นบรรทัด แต่ถ้าคุณใช้ ORM คุณจะใช้เวลาเพียงแค่ไม่เกิน 2 วันครับ
  2. ORM ส่วนใหญ่ที่มีมักเป็น Better of Design และ Code Quality ยิ่งตัวที่มีอายุการพัฒนายาวนาน มี community และมี developer ใช้งานเยอะ ๆ แล้ว ล้วนแต่ไว้วางใจได้ครับ 
  3. ลดคนที่ต้องมาพัฒนาส่วนจัดการ Database
  4. ORM ช่วยลดเวลาในการทำ Test แน่นอนหละครับ มี Community มี User นำไปใช้งานจริง มี bug/issue อะไรก็แจ้งให้แก้ไข ยิ่งพวก Commercial ด้วยแล้ว QC เชื่อถือได้แน่นอนครับ
  5. ORM ช่วยให้คุณลดความยุ่งยากในการเรียนรู้และการทำงานกับ Database ให้เหลือเพียง คุณจะ Get/Update/Add/Delete object ยังไงเท่านั้น

เป็นไงครับ ฟังแล้วพอจะรู้ทางเลือกแล้วใช่มั้ยครับ ว่าเราจะเลือกพัฒนา Custom ORM เองหรือจะใช้ ORM Framework ที่มีอยู่มากมายในปัจจุบัน ผมมีรายชื่อ ORM มาฝากครับ

Name

Open Source

Commercial

Current Version

Multiple Database

LINQ support
ADO.NET Entity Framework Microsoft 1.0 Yes / with 3rd Party Yes
LINQ To SQL Microsoft 1.0 SP1 MSSQL only Yes
NHibernate
(port from Hibernate JAVA)
Fabio Maulo 2.0.1 GA Yes with LINQ to NH
Castle Active Record Castle Project 2.0 Alpha 1 Yes N/A
SubSonic Rob Conery 2.2 Yes Yes
LLBLGen Pro LLBLGen 2.6 Yes Yes
Genome Genome 4.1 Yes Yes
           

Table 1. ORM tool List

ซึ่งบทความนี้ผมจะขอพูดถึง ORM 2 ตัวคือ NHibernate และ ADO.NET Entity Framework ครับ ซึ่งในตอนหน้าจะลงรายละเอียดของ NHibernate

เจอกันตอนหน้าเร็ว ๆ นี้ สวัสดีครับ

 

Nine (นาย)

GF Community Lead

Microsoft MVP Thailand

Blog at WordPress.com.