[Data Access] ORM 原理 (1) : 物件和資料是怎麼繫結 (binding) 的?

我想大家或多或少都聽過 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();
        }
    }
}

這只是一開始而已,我們接下來還會遇到更多的問題。