C#.NET อ่านฐานข้อมูล MySql แบบเท่ๆ ตอนที่ 2
ใน ตอนที่แล้ว เราได้ทำการสร้าง Connection แบบ Singleton เพื่อนำมาใช้งานในทุกๆที่ ที่ต้องการการเชื่อมต่อกับฐานข้อมูลไปแล้ว
ตอนนี้เราจะมาสร้าง class ที่ช่วยให้ชีวิตในการทำงานกับฐานข้อมูลมีความสุขและสบายกายมากขึ้น สิ่งนี้จะเรียกว่า ORM: Object Relation Mapping ได้รึเปล่าก็ยังไม่รู้ แต่ผมจะขอเรียกมันว่า ORM ละกัน เพราะมันใช้งานคล้ายๆกัน เอ๊ะหรือจะเรียกว่า DAL: Data Access Layer ดี อันนี้ก็ไม่รู้ ขี้เกียจคิดอธิบายครับ บอกแล้วว่าผมเป็นคนขี้เกียจ ถ้าลองดูตามไปอาจจะพอจับทางได้ล่ะครับว่ามันคืออะไร และจะเอาไปต่อยอดยังไงได้อีก
สร้าง class ใหม่ ผมสมมติให้มันเป็น ORM เพราะฉะนั้นก็ตั้งมันซะว่า ORM.cs ซะเลยละกัน ใน class นี้ ผมจะสร้างคำสั่ง query ต่างๆเตรียมเอาไว้ เวลาจะใช้งานจะได้ไม่ต้องมาพิมพ์ "SELECT * .." ให้เมื่อยตุ้มอีก แต่ทีนี้การ query ข้อมูลมันทำได้เป็นไม่รู้กี่แบบ จะเขียนให้หมดได้ไงฟะ เออน่า ลองดูไปก่อน เดี๋ยวจะบอกให้นะจ๊ะทิงจา...
เราจะให้มันเป็น abstract class เพราะเราจะไม่เรียกใช้มันโดยตรง
ORM.cs
เราจะให้มันเป็น abstract class เพราะเราจะไม่เรียกใช้มันโดยตรง
ORM.cs
public abstract class ORM
{
}
ต่อไปมันต้องมีการ query ข้อมูล เพื่อความงดงาม เราจะสร้างคลาสใหม่ขึ้นมาเพื่อโปรแกรมวิธีการ query ที่เขียนเสร็จแล้วก็ลืมมันไปได้เลย
สร้าง class ใหม่ชื่อว่า Query.cs แล้วยัดไส้มันซะ
Query.cs
public class Query
{
private readonly List<string> _binding = new List<string>();
private readonly List<string> _keys = new List<string>();
private readonly string _tableName;
private readonly List<string> _where = new List<string>();
private string _limit = null;
//constructor รับชื่อของตารางในฐานข้อมูลเข้าไป
public Query(string tableName)
{
_tableName = tableName;
}
//เรามาสะสม Where กันตรงนี้แหละ
public Query Where(string key, string oper, string value)
{
_where.Add(String.Format("AND {0} {1} @{0}", key, oper));
_binding.Add(value);
_keys.Add(key);
return this;
}
//เอาไว้ล้างค่า Where เวลาเรียกใหม่จะได้ไม่มีเงื่อนไขเดิมติดมาด้วย
public void ClearWhereCluase()
{
_where.Clear();
}
//set limit ทำ overload ไว้ 2 แบบให้รับได้ทั้ง int, string
public void Limit(string limit)
{
_limit = limit;
}
//overload
public void Limit(int limit)
{
_limit = limit.ToString();
}
}
เอาล่ะ ทีนี้เราจะมาเริ่มทำการ Select ข้อมูลจากฐานข้อมูลกันแล้ว แต่เราไม่รู้ว่าเราจะ select อะไรไปใส่ object ไหน ดังนั้นที่เราต้องการคือ ให้มันรับ object อะไรก็ได้ ไปค้นหาในตารางไหนก็ได้โดยให้โปรแกรมเรารู้เอง เช่นจะค้นหาสินค้าไปใส่ไว้ใน class Product จากตาราง products หรือค้นหาสมาชิกไปใส่ใน class Member จากตาราง members
ดังนั้นเราต้องรู้ก่อนว่า ไอ้สิ่งที่เราจะค้นหาเนี่ยมันคืออะไร และจะไปหาจากตารางไหน ในที่นี้เพื่อให้มันง่าย ผมจะตั้งชื่อ class ที่เป็น Model ของวัตถุต่างๆเป็นเอกพจน์ และชื่อตารางในฐานข้อมูลเป็นพหูพจน์ เช่น Model ชื่อ Product ตางรางก็ควรจะชื่อ products(ตัวพิมพ์เล็กทั้งหมด)
คำสั่งนี้เอาไว้แปลงเป็นพหูพจน์
//ขี้เกียจพิมพ์เยอะ ใครขยันก็เพิ่มๆแกรมม่าเข้าไปเองนะ
public string GetTableName(string className)
{
switch (className.ToLower().Substring(className.Length - 1))
{
case "y":
return className.ToLower().Substring(0, className.Length - 1) + "ies";
case "s":
return className.ToLower() + "es";
case "f":
return className.ToLower().Substring(0, className.Length - 1) + "ves";
default:
return className.ToLower() + "s";
}
}
เรามางมกันต่อที่ query.cs ครับ การที่เราจะรู้ว่า object ที่เรารับเข้ามานั้นเป็นอะไร มี property อะไรบ้าง มีค่าอะไรบ้าง เราจะทำผ่าน Reflection ครับ
Reflection คืออะร๊ายยยยยยย!!
Refection เป็นการใช้โค้ดอ่านโค้ด เพราะว่าตอนคอมไพล์โปรแกรมเนี่ย มันจะสร้าง metadata ของโค้ดออกมา เราก็เข้าไปอ่าน metadata ของมันนี่แหละเอามาทำงานกะโค้ดเราอีกที เพื่อให้เราสามารถสร้าง object แบบ dynamic ได้
อธิบายคร่าวๆ
สมมติว่าเรามี Class A อยู่ ถ้าเป็น php เราสามารถทำแบบนี้ได้
a(A);
function a($value){
$x = new $value();
}
ซึ่งเราทำแบบนี้กับ c# ไม่ได้
a(A);
void a(Type value)
{
var x = new value();
}
ดังนั้น เราจึงต้องใช้ Reflector เพื่อ instantiate object ขึ้นมาในตอน runtime เพื่อให้เราสามารถสร้าง object อะไรก็ได้ขึ้นมา มาเริ่มใช้งานจริงกันดีกว่า เพิ่ม method เข้าไปใน Query.cs
//เอาไว้เช็คดูว่า ค่าที่รับมานี้มีอยู่ใน field ของตารางในฐานข้อมูลรึเปล่า
public bool HasColumn(IDataRecord r, string columnName)
{
try
{
return r.GetOrdinal(columnName) >= 0;
}
catch (IndexOutOfRangeException)
{
return false;
}
}
//เอา limit มาสร้างเป็น string
private string GetLimit()
{
if (_limit == null)
{
return "";
}
return " LIMIT " + _limit;
}
//เอาค่า Where ใน List มาต่อเป็น String
private string GetWhere()
{
if (_where.Count == 0)
{
return "";
}
string where = String.Join(" ", _where);
string whereSql = GetRightString(where, "AND");
return " WHERE " + GetRightString(whereSql, "OR");
}
//ตัดเอาแต่ข้อความด้านขวาของตัวที่ค้นหา
private string GetRightString(string str,string cut)
{
int position= str.LastIndexOf(cut);
if(length > -1)
{
return str.Substring(position+cut.Length);
}
return str;
}
//Select ข้อมูลจากฐานข้อมูล ความสนุกมันอยู่ตรงนี้แหละ
public dynamic Get(Type classType, string column = "*", string order = "")
{
string sql = "SELECT " + column + " FROM " + _tableName + GetWhere() + " " + order + " " + GetLimit();
var command = new MySqlCommand(sql, UniqueDB.Instance.GetConnection());
for (int i = 0; i < _binding.Count; i++)
{
command.Parameters.AddWithValue(_keys[i], _binding[i]);
}
MySqlDataReader reader = command.ExecuteReader();
//สร้าง Generic List ที่จะรับค่าบางอย่างตามที่ส่งมาใน classType
Type listGeneric = typeof (List<>);
Type[] param = {classType};
object dbObjects = Activator.CreateInstance(listGeneric.MakeGenericType(param));
//เรียกใช้ Method Add ของ List เพราะเราใช้ .Add() จากการทำแบบนี้ไม่ได้
MethodInfo add = dbObjects.GetType().GetMethod("Add");
while (reader.Read())
{
//เป็นการ new object แนวคล้ายๆแบบ php ที่เขียนไว้ก่อนหน้านี้
object obj = Activator.CreateInstance(classType);
//ตรงนี้ใช้ Reflection นะแจ้
PropertyInfo[] props = obj.GetType().GetProperties();
foreach (PropertyInfo prop in props)
{
if (HasColumn(reader, prop.Name.ToLower()))
{
if (reader[prop.Name.ToLower()] != DBNull.Value)
prop.SetValue(obj, reader[prop.Name.ToLower()], null);
}
}
//มันคือ list.Add(obj); น่ะนะ
add.Invoke(dbObjects, new[] {obj});
}
reader.Close();
ClearWhereCluase();
return dbObjects;
}
สังเกตที่ method Get() ผมจะใช้ return type เป็น dynamic เพราะเราไม่ทราบว่าเราจะ return ค่าอะไรออกมา เรารู้แค่ว่ามันเป็น list ของอะไรซักอย่างเท่านั้น การใช้ dynamic จะเป็นการยกหน้าที่ในการจัดการ type ไปทำในตอน Runtime ซะ ทีนี้เราก็ไม่ต้องสนใจแล้วว่ามันจะส่งค่าเป็น List ของ Type อะไร
กลับมาที่ ORM.cs ตอนนี้เราจะเริ่มเอามันมาลองใช้ดูละ
ORM.cs
public abstract class ORM
{
//ใช้ readonly เพราะต้องการให้แก้ไขได้แต่ใน Constructor เท่านั้น
private readonly Query _query;
protected ORM()
{
// GetTableName คืออันที่เขียนแปลงเอกพจน์เป็นพหูพจน์เมื่อกี้
// การสั่ง GetType() หรือ this.GetType() จะได้ Type ของคลาสลูก
_query = new Query(GetTableName(GetType().Name));
}
public ORM Limit(int limit)
{
_query.Limit(limit);
return this;
}
public ORM Limit(string limit)
{
_query.Limit(limit);
return this;
}
//อันนี้ใช้ทำ paging ได้ด้วยนะ
public ORM Limit(int from, int to)
{
_query.Limit(from + "," + to);
return this;
}
public void Where(Enum enums, string oper, string value)
{
_query.Where(GetKey(enums), oper, value);
}
public void ClearWhereCluase()
{
_query.ClearWhereCluase();
}
public dynamic Get(string column = "*", string order = "")
{
return _query.Get(GetType(), column, order);
}
//ค้นหาข้อมูลทั้งหมด ซึ่งเรารู้ว่ามันเป็น List ก็เอา interface ของ List มาใช้ซะ
public IList Find(string order = "")
{
return Get("*", order);
}
//ค้นหาข้อมูลที่ต้องการด้วยตัวของ object เลย
public IList Find(object data)
{
//แน่นอนว่ามันต้องใช้ Reflector
PropertyInfo[] props = data.GetType().GetProperties();
//ไอ้นี่เรียกว่า Lambda Expression ผมชอบใช้เพราะขี้เกียจพิมพ์ foreach
props.ToList().ForEach(prop =>
{
if (data.GetType().GetProperty(prop.Name).GetValue(data, null) != null)
{
string value = "";
if (
data.GetType().GetProperty(prop.Name).GetValue(data, null) is
DateTime)
value =
((DateTime)
data.GetType().GetProperty(prop.Name).GetValue(data, null)).
SetToMySQLDateString();
else
value =
data.GetType().GetProperty(prop.Name).GetValue(data, null).
ToString();
_query.Where(prop.Name, "=", value);
}
});
return _query.Get(GetType());
}
}
มาลอง Select ข้อมูลจากฐานข้อมูลกันเลยดีกว่า ทำตามขั้นตอนดังนี้
- คลิกขวาที่ Solution -> Add new project -> c# -> console application ละกันง่ายดี
- ทำการ Add Reference จาก Solution โดยเลือกโปรเจคที่เรากำลังทำอยู่นี้
- อย่าลืม Add Reference MySql.Data.dll
แว้บมาดูฐานข้อมูลของเรากันก่อน สมมติว่าตอนนี้ผมมีฐานข้อมูล products อยู่แล้ว โดยมีไส้ในราวๆนี้
โดยที่ id เป็น Auto Increment
Table: products
และมีข้อมูลราวๆนี้
public class Product:ORM
{
public int? Id { get; set; }
public string Name { get; set; }
public double? Price { get; set; }
public string Description { get; set; }
}
เครื่องหมาย ? ข้างหลัง primitive type นี้ใส่ไว้เพื่อให้มันมีค่าเป็น null ได้ (Nullable<T>) เอาไว้รับค่า null นั่นแล
มาที่ไฟล์ Program.cs เพื่อลองเล่นมันดูดีกว่า พิมพ์ลงไปตามนี้
private static void Main(string[] args)
{
//เชื่อมต่อและเปิดฐานข้อมูล
UniqueDB.Instance.SetConnection("localhost", "database_name", "username", "password").GetConnection().Open();
Product product = new Product();
List<product> products = product.Find() as List<product>;
products.ForEach(x=>Console.WriteLine(x.Description));
UniqueDB.Instance.GetConnection().Close();
Console.ReadLine();
}
เมื่อ Run ดูจะได้ผลแบบนี้ Yes!! ถือว่าเริ่มต้นได้สวยละ
ต่อมาลองค้นหาแบบนี้
private static void Main(string[] args)
{
//เชื่อมต่อและเปิดฐานข้อมูล
UniqueDB.Instance.SetConnection("localhost", "database_name", "username", "password").GetConnection().Open();
Product product = new Product();
product.Name = "Play Station 4";
List<product> products = product.Find(product) as List<product>;
products.ForEach(x=>Console.WriteLine(x.Description));
UniqueDB.Instance.GetConnection().Close();
Console.ReadLine();
}
เมื่อ Run แล้วจะได้แบบนี้ :)
ตอนนี้เราสามารถ select ข้อมูลจากฐานข้อมูลได้แล้วโดยที่ไม่ต้องเขียน SQL อีก เพียงแค่ทำงาน Extend จากคลาส ORM เท่านั้น ซึ่งจะช่วยย่นระยะเวลาการทำงานลงไปได้มากเลย แถมยังเอาไปต่อยอดเอาไปทำ framework ได้อีกด้วยนะ
ตอนต่อไป ผมจะเขียนเรื่องการ INSERT และ UPDATE ข้อมูลในฐานข้อมูล โดยที่การทำไอ้สองอย่างนี้ จะทำด้วยการสั่ง product.Save(); เท่านั้น แล้วโปรแกรมจะไปจัดการให้เองว่าจะ INSERT หรือ UPDATE ข้อมูล
ไว้พบกันตอนหน้าครับ




Thanks very much.
ReplyDeleteขอบคุณครับ รอครับ
ReplyDelete