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  

จบครับ

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: