[Data Access] ORM 原理 (5) : 欄位對應的進階考量

在原理 (4) 中,我們使用了特徵項 (attribute) 來處理欄位對應的問題,只是這個方法對於可能時常異動欄位名稱,或是想要利用 copy/paste 以及擴大使用範圍的需求來說,可能就沒那麼恰當,因為使用特徵項最大的缺點就是:它是寫死 (hard-code) 的,若是想要修改欄位名稱的話,勢必又要重新 compile...

在原理 (4) 中,我們使用了特徵項 (attribute) 來處理欄位對應的問題,只是這個方法對於可能時常異動欄位名稱,或是想要利用 copy/paste 以及擴大使用範圍的需求來說,可能就沒那麼恰當,因為使用特徵項最大的缺點就是:它是寫死 (hard-code) 的,若是想要修改欄位名稱的話,勢必又要重新 compile,對修改的彈性來講會是一個不小的問題。所以我們要想個方法,可以讓它們能夠有彈性一點。我們有兩種作法,一種是用介面 (interface) 來實作欄位對應,另一種則是用組態檔來對應。

我們先來講用介面對應這件事。介面的好處是大家都遵循相同的規則,只要實作了介面的物件,就一定擁有該介面的規則,利用這樣的特性,我們可以定義一個處理欄位對應的介面,其中只有一個方法,就是用來對應屬性和表格欄位。

namespace ConsoleApplication2.Contracts
{
    public interface IDataSourceColumnMapper
    {
        string GetDataSourceColumn(string PropertyName);
    }
}

有了這個介面後,我們就可以在 Entity 類別中實作這個介面,本例以原理 (4) 時使用的 Employee 類別來實作:

namespace ConsoleApplication2
{
    public class Employee : Contracts.IDataSourceColumnMapper 
    {
        public string EmployeeID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Title { get; set; }
        // [DataSourceColumn("HomePhone")] // this line is for phrase 4.
        public string Phone { get; set; }
        public string GetDataSourceColumn(string PropertyName)
        {
            switch (PropertyName)
            {
                case "EmployeeID":
                    return "EmployeeID";
                case "FirstName":
                    return "FirstName";
                case "LastName":
                    return "LastName";
                case "Title":
                    return "Title";
                case "Phone":
                    return "HomePhone";
                default:
                    throw new NotSupportedException("ERROR_PROPERTY_CANT_MAP_DATA_SOURCE_COLUMN");
            }
        }
    }
}

然後修改資料存取程式,由原本的自特徵項取值,改成使用介面:

foreach (PropertyInfo property in properties)
{
    int ordinal = -1;
    Type propType = property.PropertyType;
    // method 1, implement a interface to map entity and schema.
    Contracts.IDataSourceColumnMapper dsMapper = employee as Contracts.IDataSourceColumnMapper; 

    // get column index, if not exist, set -1 to ignore.
    try
    {
        ordinal = reader.GetOrdinal(dsMapper.GetDataSourceColumn(property.Name));
    }
    catch (Exception)
    {
        ordinal = -1;
    }
    // set value.
    if (ordinal >= 0)
    {
        TypeConverters.ITypeConverter typeConverter = TypeConverters.TypeConverterFactory.GetConvertType(propType);
        if (!propType.IsEnum)
        {
            property.SetValue(employee,
                Convert.ChangeType(typeConverter.Convert(reader.GetValue(ordinal)), propType), null);
        }
        else
        {
            TypeConverters.EnumConverter converter = typeConverter as TypeConverters.EnumConverter;
            property.SetValue(employee,
                Convert.ChangeType(converter.Convert(propType, reader.GetValue(ordinal)), propType), null);
        }
    }               
}

執行程式,可以得到 Employees 的資料,如同原理 (4)。介面的定義和使用並不難,只是它還是有個問題:寫死,雖然我們可以利用將 Entity 定義和 binding/use 的程式碼分開,但是還是會面臨到修改對應時的 re-compile 問題,我們真正希望做到的,是完全不用 re-compile 就能修改,所以我們就需要使用組態檔。如果對組態檔不熟,可參考本篇文章

為了要達成欄位的定義,以及考量擴充性,我們會需要稍微複雜一點點的組態架構:

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="entitySetConfiguration" type="ConsoleApplication2.Configuration.EntitySetConfiguration, ConsoleApplication2" />
  </configSections>
  <entitySetConfiguration>
    <entities>
      <entity type="ConsoleApplication2.Employee" schema="Employees">
        <maps>
          <map propertyName="EmployeeID" schemaName="EmployeeID" />
          <map propertyName="FirstName" schemaName="FirstName" />
          <map propertyName="LastName" schemaName="LastName" />
          <map propertyName="Title" schemaName="Title" />
          <map propertyName="Phone" schemaName="HomePhone" />
        </maps>
      </entity>
    </entities>
  </entitySetConfiguration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

其中,entitySetConfiguration 就是我們定義的組態檔,它的程式內容是:

namespace ConsoleApplication2.Configuration
{
    public class EntitySetConfiguration : ConfigurationSection
    {
        [ConfigurationProperty("entities", IsRequired = false, IsKey = false)]
        [ConfigurationCollection(typeof(EntityConfigurationCollection),
                CollectionType = ConfigurationElementCollectionType.BasicMap, AddItemName = "entity")]
        public EntityConfigurationCollection EntityConfigurations { 
             get { return base["entities"] as EntityConfigurationCollection; }
        }       
    }
}

其下還有 EntityConfiguraiton, EntitySchemaMapCollection 和 EntitySchemaMap 等類別,在這裡就不貼原始碼了,請逕自下載原始碼瀏覽。

整個組態系統完成後,再來修改原始資料存取的程式:

namespace ConsoleApplication2
{
    class ProgramStep4_2
    {
        static void Main(string[] args)
        {
            // step 4-2. data mapping with interface implementation.
            SqlConnection db = new SqlConnection("initial catalog=Northwind; integrated security=SSPI");
            SqlCommand dbcmd = new SqlCommand(@"SELECT EmployeeID, LastName, FirstName, Title, HomePhone FROM Employees", db);
            List<Employee> employees = new List<Employee>();
            // for method 2, prepare configuration.
            Configuration.EntitySetConfiguration entitySetConfiguration =
                ConfigurationManager.GetSection("entitySetConfiguration") as Configuration.EntitySetConfiguration;
            Configuration.EntityConfiguration employeeMapConfiguration =
                entitySetConfiguration.EntityConfigurations.GetConfigurationFromType(typeof(Employee).FullName); 

            db.Open();
            SqlDataReader reader = dbcmd.ExecuteReader(CommandBehavior.CloseConnection);
            while (reader.Read())
            {
                Employee employee = new Employee();
                PropertyInfo[] properties = employee.GetType().GetProperties();
                foreach (PropertyInfo property in properties)
                {
                    int ordinal = -1;
                    Type propType = property.PropertyType; 
                    // method 2, provision map information from configuration.
                    Configuration.EntitySchemaMap schemaMap =
                         employeeMapConfiguration.EntitySchemaMaps.GetConfigurationFromPropertyName(property.Name);
                    // get column index, if not exist, set -1 to ignore.
                    try
                    {
                        ordinal = reader.GetOrdinal(schemaMap.EntitySchemaName);
                    }
                    catch (Exception)
                    {
                        ordinal = -1;
                    }
                    // set value.
                    if (ordinal >= 0)
                    {
                        TypeConverters.ITypeConverter typeConverter = TypeConverters.TypeConverterFactory.GetConvertType(propType);
                        if (!propType.IsEnum)
                        {
                            property.SetValue(employee,
                                Convert.ChangeType(typeConverter.Convert(reader.GetValue(ordinal)), propType), null);
                        }
                        else
                        {
                            TypeConverters.EnumConverter converter = typeConverter as TypeConverters.EnumConverter;
                            property.SetValue(employee,
                                Convert.ChangeType(converter.Convert(propType, reader.GetValue(ordinal)), propType), null);
                        }
                    }               
                }
                employees.Add(employee);
            }
            reader.Close();
            db.Close();
            foreach (Employee employee in employees)
            {
                Console.WriteLine("id: {0}, name: {1}, title: {2}, phone: {3}",
                    employee.EmployeeID, employee.FirstName + ' ' + employee.LastName, employee.Title, employee.Phone);
            }
            Console.WriteLine("");
            Console.WriteLine("Press ENTER to exit.");
            Console.ReadLine();
        }
    }
}

修改完後,試執行此程式,應可得到與前面相同的結果,不過這次我們可以試著改一下 app.config 中的欄位對應,例如把 Phone 的定義改掉,或是設成其他的欄位,再執行時就會發現資料不同 (無法對應的話會變成空白)。

Source Code: https://dotblogsfile.blob.core.windows.net/user/regionbbs/1111/201111171442484.rar