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;
var pks = GetPrimaryKeys<T>();
entry = this.Entry(entity);
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(); }
Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(“Before Update……..”);
{ Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}“, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”)); } Console.WriteLine();
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
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 { //////////
{
var pks = ConfigPartialUpdate(entity, out entry);
{ if (pks.Contains(prop)) continue; var errors = entry.Property(prop).GetValidationErrors();
{ 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; }
} |
ต่อไปจะสร้าง 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 {
{ if (!_propertyChangeList.Contains(propertyName)) _propertyChangeList.Add(propertyName); }
{ _propertyChangeList.Clear(); }
[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;
public class User : EntityBase {
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(); }
Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(“Before Update……..”);
{ Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}“, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”)); } Console.WriteLine();
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
สรุปว่าทั้งสองวิธีมีข้อดีและข้อเสียแตกต่างกัน แต่ได้ผลลัพธ์เดียวกัน
แบบที่ 1 กำหนด property ที่ต้องการด้วยตัวเอง
ข้อดีคือ ใช้งานง่ายเหมาะกับระบบที่มีโมเดลอยู่แล้ว และไม่ต้องการจะไปรื้อโครงสร้างให้วุ่นวาย
ข้อเสียคือ จำเป็นต้องระบุ property ที่ต้องการให้ Change ค่าเอง ซึ่งก็อาจจะหลงๆลืมๆส่งไปไม่ครบก็ได้ อาจจะทำงานผิดพลาด
แบบที่ 2 ใช้ INotifyPropertyChanged เข้าช่วยในการตรวจสอบการเปลี่ยนแปลง
ข้อดีคือ ไม่ต้องกังวลว่าจะลืมส่ง Property อะไรไปบ้าง จุดเรียกใช้งานก็ใช้ง่าย
ข้อเสียคือ ต้องแก้ไข entity class อาจจะกระทบงานเก่าที่มีอยู่
หวังว่าตอนนี้จะเป็นประโยชน์ ในการนำไปใช้งาน และช่วยเพิ่มความรู้และเทคนิคต่างๆให้เพื่อนๆกันได้ไม่มากก็น้อย
จบครับ