我想大家或多或少都聽過 Entity Framework 或是 NHibernate Framework 這種大型應用程式開發的 Framework 吧,它們都是做 ORM (Object Relational Mapping) 技術的資料存取函式庫,只是很多人都只看它有什麼功能,卻沒有多少人對它內部感興趣-為什麼它們可以精確的對應 SQL 的欄位和物件屬性呢?我試著以一系列的文章來介紹 ORM 到底做了什麼事。
(由於操作錯誤,原本的 Part 1 被置換成 Part 2,感謝麥穗兄的支援讓本文得以回復。)
我想大家或多或少都聽過 Entity Framework 或是 NHibernate Framework 這種大型應用程式開發的 Framework 吧,它們都是做 ORM (Object Relational Mapping) 技術的資料存取函式庫,只是很多人都只看它有什麼功能,卻沒有多少人對它內部感興趣-為什麼它們可以精確的對應 SQL 的欄位和物件屬性呢?我試著以一系列的文章來介紹 ORM 到底做了什麼事。
首先,傳統的資料存取法,都是透過 ADO.NET 或是 JDBC 這種類別庫提供的核心資料存取類別來做,以 ADO.NET 為例,少不了的是 SqlConnection, SqlCommand, SqlDataReader, SqlDataAdapter 與 SqlParameter 等類別,不同的 SQL 指令使用的物件方法又不同,我們以 Northwind 為例,在 Northwind 資料庫內的 Customers 資料表有 11 個欄位,今天我要存取它,程式碼的長相大概會是這樣:
SqlConnection db = new SqlConnection("initial catalog=Northwind; integrated security=SSPI");
SqlCommand dbcmd = new SqlCommand("SELECT * FROM Customers", db);
db.Open();
SqlDataReader reader = dbcmd.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
while (reader.Read())
{
...
}
reader.Close();
db.Close();
使用 DataTable 的話,自然就是用 SqlDataAdapter 搭配 SelectCommand,再下 Fill() 指令即可。只是今天我們想要用物件來做這件事,也就是說我們不用 DataTable,而是想用 List<Customer> (Customer 為物件),透過強型別的方式讓編譯時期可以發現資料型別的錯誤,也便於在不同的系統或程式間流通。所以我們定義了 Customer 類別:
public class Customer
{
public string CustomerID { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
}
每一個欄位都以一個屬性來設定,這個類別由於只有資料,所以可稱為 POCO (Plain-Old-CLR-Object),今天我想要把資料直接代入到這個類別物件,用傳統的作法,我們可以這樣做:
while (reader.Read())
{
Customer customer = new Customer();
if (!reader.IsDBNull(reader.GetOrdinal("CustomerID")))
customer.CustomerID = reader.GetValue(reader.GetOrdinal("CustomerID")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("CompanyName")))
customer.CompanyName = reader.GetValue(reader.GetOrdinal("CompanyName")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("ContactName")))
customer.ContactName = reader.GetValue(reader.GetOrdinal("ContactName")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("ContactTitle")))
customer.ContactTitle = reader.GetValue(reader.GetOrdinal("ContactTitle")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("Address")))
customer.Address = reader.GetValue(reader.GetOrdinal("Address")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("Region")))
customer.Region = reader.GetValue(reader.GetOrdinal("Region")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("City")))
customer.City = reader.GetValue(reader.GetOrdinal("City")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("PostalCode")))
customer.PostalCode = reader.GetValue(reader.GetOrdinal("PostalCode")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("Country")))
customer.Country = reader.GetValue(reader.GetOrdinal("Country")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("Phone")))
customer.Phone = reader.GetValue(reader.GetOrdinal("Phone")).ToString();
if (!reader.IsDBNull(reader.GetOrdinal("Fax")))
customer.Fax = reader.GetValue(reader.GetOrdinal("Fax")).ToString();
customers.Add(customer);
}
只是...一般的程式師看到這麼長的指令都會嫌煩了,欄位一多時可能這段程式會更可觀吧,那麼我們何不用個更簡單的方法,只要由屬性的名稱和欄位的名稱對應,就能夠自動將值套用到屬性中?要做到這個能力,我們可利用 Reflection 的機制來做,這樣一來,程式就會變成這樣:
while (reader.Read())
{
Customer customer = new Customer();
for (int i = 0; i < reader.FieldCount; i++)
{
PropertyInfo property = customer.GetType().GetProperty(reader.GetName(i));
property.SetValue(customer, (reader.IsDBNull(i)) ? "[NULL]" : reader.GetValue(i), null);
}
customers.Add(customer);
}
如何?從一串落落長的程式縮短到只有幾行,這就是 Reflection 的威力,透過 PropertyInfo 自動代值,可省下很多 coding 的時間,而且隨著 CPU 愈來愈快,Performance 也不會被消耗的太多。
完整程式碼:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ConsoleApplication2
{
class ProgramStep1
{
static void Main(string[] args)
{
// step 1. load data from data source and binding with Reflection.
SqlConnection db = new SqlConnection("initial catalog=Northwind; integrated security=SSPI");
SqlCommand dbcmd = new SqlCommand("SELECT * FROM Customers", db);
List<Customer> customers = new List<Customer>();
db.Open();
SqlDataReader reader = dbcmd.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
while (reader.Read())
{
Customer customer = new Customer();
for (int i = 0; i < reader.FieldCount; i++)
{
PropertyInfo property = customer.GetType().GetProperty(reader.GetName(i));
property.SetValue(customer, (reader.IsDBNull(i)) ? "[NULL]" : reader.GetValue(i), null);
}
customers.Add(customer);
}
reader.Close();
db.Close();
foreach (Customer customer in customers)
{
Console.WriteLine("id: {0}, name: {1}, address: {2}, phone: {3}",
customer.CustomerID, customer.CompanyName, customer.Address, customer.Phone);
}
Console.WriteLine("");
Console.WriteLine("Press ENTER to exit.");
Console.ReadLine();
}
}
}
這只是一開始而已,我們接下來還會遇到更多的問題。