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中也看得到,例如:
(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 的好處:
然後,我們使用了 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);
}
}
}
透過預設的設定檔,自行注入組態設定或是直接給予資料來源型別與連線字串等,就可以直接取用到資料庫內的資料。
其執行結果如下:
至於如何應用 ClassDataMap 來生成必要的 SQL 指令的作法,請待下回分解。