[Data Access] ORM 原理 (10) : 全程式碼對映–當 ORM 遇到 Lambda 與 Fluent Interface

ORM 原理前面8集中己經講述了基本的ORM核心內的運作方式,大多數的ORM其實都是這麼做,當然還會做一些更進一步的最佳化工作,例如產生SQL的方式等。不過既然都是寫程式的,當然會希望這些對應欄位的設定工作可以完全的程式化 (Coded Map),而不用再假手那麼多的設定檔。

ORM 原理前面8集中己經講述了基本的ORM核心內的運作方式,大多數的ORM其實都是這麼做,當然還會做一些更進一步的最佳化工作,例如產生SQL的方式等。不過既然都是寫程式的,當然會希望這些對應欄位的設定工作可以完全的程式化 (Coded Map),而不用再假手那麼多的設定檔。

這個方法在 NHibernate 看得到,例如:

public class ProductMap : ClassMap<Product>
{
    public ProductMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Map(x => x.UnitPrice);
        Map(x => x.UnitsOnStock);
        Map(x => x.Discontinued);
    }
 
}

(Reference: http://nhforge.org/blogs/nhibernate/archive/2008/09/05/a-fluent-interface-to-nhibernate-part-1.aspx)

在Entity Framework中也看得到,例如:

image

(Reference: http://weblogs.asp.net/scottgu/archive/2010/07/23/entity-framework-4-code-first-custom-database-schema-mapping.aspx)

 

我自己是比較偏好 NHibernate 的設定方式,簡單又直覺,只是要做到這樣的話,要懂的東西就又變多了,俗語云:表面上愈簡單的東西愈難做。要做到這樣的設定法,除了要能在方法中埋入 Lambda 以外,又要能做到連續設定,這表示我們可以利用 Fluent Interface 的手法來做。

以下是最終的成果:

namespace CodedMapDAL
{
    public class DataServiceTest
    {
        [STAThread]
        public static void Main(string[] args)
        {
            CustomerRepository repository = new CustomerRepository();
            IEnumerable<Customer> customers = repository.GetCustomers();

            foreach (Customer c in customers)
                Console.WriteLine("id: {0}, name: {1}, phone: {2}", c.Id, c.Name, c.Phone);

            Console.ReadLine();
        }
    }

    public class CustomerRepository : ClassMap<Customer, CustomerMap>
    {        
        public IEnumerable<Customer> GetCustomers()
        {
            return this.GetContext().LoadDataObjects<Customer>();
        }
    }

    public class Customer 
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Phone { get; set; }
    }

    public class CustomerMap : ClassDataMap<Customer>
    {
        public CustomerMap()
        {
            // schema table name.
            Schema("Customers");

            // column maps.
            Id(c => c.Id).SchemaName("CustomerID");
            Map(c => c.Name).SchemaName("CompanyName");
            Map(c => c.Phone);
        }
    }
}

其中,Customer 是一個很簡單的 DTO 物件 (POCO),CustomerRepository 則是使用 Repository Pattern 將實際的資料存取與商業邏輯層隔離的手法,CustomerRepository 使用了 ClassMap<T, T> 這個抽象類別來實作一般的資料存取功能。而最重要的要算是 CustomerMap 了,它使用了 ClassDataMap<T> 來實作 Coded Map 的功能。ClassDataMap<T> 的原始程式碼如下:

namespace CodedMapDAL
{
    public class ClassDataMap<T> : IClassDataMap 
        where T : class
    {
        public Type ClassDataType { get; private set; }
        public string SchemaName { get; private set; }
        public List<ClassDataMapInfo> _dataMapInfo = new List<ClassDataMapInfo>();

        public ClassDataMap()
        {
            PropertyInfo[] properties = typeof(T).GetProperties();
            this.ClassDataType = typeof(T);

            if (properties == null || properties.Length == 0)
                return;

            foreach (PropertyInfo property in properties)
            {
                ClassDataMapInfo map = new ClassDataMapInfo();
                map.MapProperty(property);
                this._dataMapInfo.Add(map);
            }
        }

        public ClassDataMapInfo Id(Expression<Func<T, object>> MapField)
        {
            string propertyName = this.GetMapPropertyName(MapField);

            var query = from item in this._dataMapInfo
                        where item.Property.Name == propertyName
                        select item;

            if (query.Count() == 0)
                return null;
            else
            {
                var map = query.First();
                map.SchemaName(propertyName).AsKey();
                return map;
            }
        }

        public ClassDataMapInfo Id(Expression<Func<T, object>> MapDataColumn, string SchemaPropertyName)
        {
            string propertyName = this.GetMapPropertyName(MapDataColumn);

            var query = from item in this._dataMapInfo
                        where item.Property.Name == propertyName
                        select item;

            if (query.Count() == 0)
                return null;
            else
            {
                var map = query.First();
                map.SchemaName(SchemaPropertyName).AsKey();
                return map;
            }
        }

        public ClassDataMapInfo Map(Expression<Func<T, object>> MapDataColumn)
        {
            string propertyName = this.GetMapPropertyName(MapDataColumn);

            var query = from item in this._dataMapInfo
                        where item.Property.Name == propertyName
                        select item;

            if (query.Count() == 0)
                return null;
            else
            {
                var map = query.First();
                map.SchemaName(propertyName);
                return map;
            }
        }

        public ClassDataMapInfo Map(Expression<Func<T, object>> MapDataColumn, string SchemaPropertyName)
        {
            string propertyName = this.GetMapPropertyName(MapDataColumn);

            var query = from item in this._dataMapInfo
                        where item.Property.Name == propertyName
                        select item;

            if (query.Count() == 0)
                return null;
            else
            {
                var map = query.First();
                map.SchemaName(SchemaPropertyName);
                return map;
            }
        }

        public void Schema(string SchemaName)
        {
            this.SchemaName = SchemaName;
        }

        private string GetMapPropertyName(Expression<Func<T, object>> MapDataColumn)
        {
            Expression expression = MapDataColumn.Body;

            if (expression.NodeType == ExpressionType.Constant)
            {
                if (!(expression.Type == typeof(string)))
                    throw new InvalidOperationException("ERROR_MAPPING_NAME_MUST_BE_STRING");
                else
                    return (expression as ConstantExpression).Value.ToString();
            }

            if (expression.NodeType == ExpressionType.Convert)
                expression = ((UnaryExpression)expression).Operand;

            if (expression is MemberExpression)
                return (expression as MemberExpression).Member.Name;

            return string.Empty;
        }

        public IEnumerable<ClassDataMapInfo> GetClassDataColumnMap()
        {
            return this._dataMapInfo;
        }

        public string GetSchemaName()
        {
            return this.SchemaName;
        }
    }
}

ClassDataMap<T> 的主要任務是設定DTO各屬性對應資料表的關聯,例如屬性A對應到欄位B,屬性C是Key,屬性D是FK等等,而在使用時,只要定義一個自己的 ClassDataMap 類別,繼承它,然後在建構式中設定各欄位的屬性就行了。但是因為要做到像這樣:

public class CustomerMap : ClassDataMap<Customer>
{
    public CustomerMap()
    {
        // schema table name.
        Schema("Customers");

        // column maps.
        Id(c => c.Id).SchemaName("CustomerID");
        Map(c => c.Name).SchemaName("CompanyName");
        Map(c => c.Phone);
    }
}

所以我們不能只用一般單純的函式作法,而是要利用 Fluent Interface 可串接的方式來做,另外,為了讓開發人員能直接以寫程式的直覺方式設定對應,因此我們導入了 Lambda Expression 來讓這件事對開發人員來說是直覺的。

只是問題來了,以 c => c.Id 為例,我要如何讀出對應的正是 c.Id ? 一般的認知會是 c => “Id”,但是這樣會無法應用 Visual Studio 對 DTO 做的 Intellisense,而且很容易出錯 (例如大小寫打錯),所以我們等於是要剖析 Lambda Expression 以取出對應的資料。所幸,微軟的 System.Linq.Expressions 命名空間中有可以幫我們處理的一些方法:

private string GetMapPropertyName(Expression<Func<T, object>> MapDataColumn)
{
    Expression expression = MapDataColumn.Body;

    if (expression.NodeType == ExpressionType.Constant)
    {
        if (!(expression.Type == typeof(string)))
            throw new InvalidOperationException("ERROR_MAPPING_NAME_MUST_BE_STRING");
        else
            return (expression as ConstantExpression).Value.ToString();
    }

    if (expression.NodeType == ExpressionType.Convert)
        expression = ((UnaryExpression)expression).Operand;

    if (expression is MemberExpression)
        return (expression as MemberExpression).Member.Name;

    return string.Empty;
}

透過使用 Expression,我們可以解析每個 Lambda 運算式的每一個元素,以及取得它的一些特性資料,而如果是物件成員的話,我們還可以進一步的取得它的名稱,而本文並不打算解析這些運算子的作法,如果有興趣,可參考:http://community.bartdesmet.net/blogs/bart/archive/2009/08/10/expression-trees-take-two-introducing-system-linq-expressions-v4-0.aspx

而像 Id(), Map() 這些方法,是用來設定屬性的對應,它們會傳回 ClassMapDataInfo 類別物件,其原始碼如下:

namespace CodedMapDAL
{
    public class ClassDataMapInfo
    {
        private Type _defaultValueType = null;

        public PropertyInfo Property { get; private set; }
        public PropertyInfo ForeignKeyProperty { get; private set; }
        public bool IsKey { get; private set; }
        public bool IsForeignKey { get; private set; }
        public bool IsAutoGenerated { get; private set; }
        public bool IsGuidColumn { get; private set; }
        public bool IsIgnoreColumn { get; private set; }
        public string SchemaMapName { get; private set; }
        public string SchemaForeignKeyMapName { get; private set; }
        public object DefaultValue { get; private set; }

        public ClassDataMapInfo()
        {
            this.IsKey = false;
            this.IsAutoGenerated = false;
            this.IsForeignKey = false;
            this.IsGuidColumn = false;
            this.IsIgnoreColumn = false;
            this.SchemaMapName = string.Empty;
            this.SchemaForeignKeyMapName = string.Empty;
            this.ForeignKeyProperty = null;
            this.DefaultValue = null;
            this.Property = null;
        }

        public ClassDataMapInfo MapProperty(PropertyInfo Property)
        {
            this.Property = Property;
            return this;
        }

        public ClassDataMapInfo AsKey()
        {
            this.IsKey = true;
            return this;
        }

        public ClassDataMapInfo AsIgnoreColumn()
        {
            this.IsIgnoreColumn = true;
            return this;
        }

        public ClassDataMapInfo AsGuidColumn()
        {
            this.IsGuidColumn = true;
            return this;
        }

        public ClassDataMapInfo AsForeignKey(PropertyInfo ForeignKeyProperty)
        {
            this.IsForeignKey = true;
            this.ForeignKeyProperty = ForeignKeyProperty;
            return this;
        }

        public ClassDataMapInfo AsForeignKey(string SchemaForeignKeyMapName)
        {
            this.IsForeignKey = true;
            this.SchemaForeignKeyMapName = SchemaForeignKeyMapName;
            return this;
        }

        public ClassDataMapInfo AutoGenerated()
        {
            this.IsAutoGenerated = true;
            return this;
        }

        public ClassDataMapInfo Default<T>(T DefaultValue)
        {
            this.DefaultValue = DefaultValue;
            this._defaultValueType = typeof(T);
            return this;
        }

        public ClassDataMapInfo SchemaName(string SchemaMapName)
        {
            this.SchemaMapName = SchemaMapName;
            return this;
        }
    }
}

就如你所看到的,由 Id() 或 Map() 傳回 ClassDataMapInfo 後,其他的事情就由 ClassMapDataInfo 來做了,而每個設定子都傳回 ClassMapDataInfo 本身,所以我們就能這樣寫:

Id(c => c.Id).AsAutoGenerated().SchemaName("CustomerID");
Map(c => c.Name).SchemaName("CompanyName");
Map(c => c.Phone).SchemaName("Phone");

而且在設定時,還可以享有 Intellisense 的好處:

image

然後,我們使用了 ClassMap<TDTO, TClassMap> 來將DTO和Class Map整合起來,其原始碼如下:

namespace CodedMapDAL
{
    public abstract class ClassMap<TClassObject, TClassMapObject>
        where TClassObject: class 
        where TClassMapObject: IClassDataMap
    {
        private IDataContextProvider _context = null;
        
        protected IDataContextProvider GetContext()
        {
            return this._context;
        }

        public ClassMap()
        {
            this._context = DataContextProviderFactory.GetDataContextProvider(
                ClassMapConfiguration.GetDefaultClassMapDataProvider(),
                Activator.CreateInstance<TClassMapObject>(),
                ClassMapConfiguration.GetDefaultClassMapDataProviderConnectionString());
        }

        public ClassMap(IClassMapDataConfiguration Configuration)
        {
            this._context = DataContextProviderFactory.GetDataContextProvider(
                Configuration.GetClassMapDataProivder(), 
                Activator.CreateInstance<TClassMapObject>(),
                Configuration.GetConnectionString());
        }

        public ClassMap(Type ClassDataMapDataProvider, string ConnectionString)
        {
            this._context = DataContextProviderFactory.GetDataContextProvider(
                ClassDataMapDataProvider,
                Activator.CreateInstance<TClassMapObject>(),
                ConnectionString);
        }
    }
}

透過預設的設定檔,自行注入組態設定或是直接給予資料來源型別與連線字串等,就可以直接取用到資料庫內的資料。

其執行結果如下:

image

 

至於如何應用 ClassDataMap 來生成必要的 SQL 指令的作法,請待下回分解。