ORM 原理走到第六步,核心只會愈來愈複雜,但用戶端相對會變得簡單,會寫這一系列文的用意,是告訴大家 ORM 的核心大概的作法,像 NHibernate 或 Entity Framework,核心做法其實差不多,當然這些著名的 ORM Framework 一定沒那麼簡單,還有很多的配套功能要做,只是我想告訴大家的是,ORM 本身不會因為它看起來像物件就可當沒有 SQL 這回事,當物件存取和關聯愈來愈複雜的時候,Object 和 SQL 之間的互動複雜度就會成等比級數一樣。
ORM 原理走到第六步,核心只會愈來愈複雜,但用戶端相對會變得簡單,會寫這一系列文的用意,是告訴大家 ORM 的核心大概的作法,像 NHibernate 或 Entity Framework,核心做法其實差不多,當然這些著名的 ORM Framework 一定沒那麼簡單,還有很多的配套功能要做,只是我想告訴大家的是,ORM 本身不會因為它看起來像物件就可當沒有 SQL 這回事,當物件存取和關聯愈來愈複雜的時候,Object 和 SQL 之間的互動複雜度就會成等比級數一樣。
OK,回到主題,在原理 (5) 結束時,我們的用戶端程式碼長度其實還蠻長的,有 94 行這麼多,其中有 90% 以上的程式碼都在做資料存取,而且是公式化的工作,如果這些工作可以移到元件由元件自己做的話,對開發人員來說會是一件很幸褔的事,所以本文就來做這件事,當完成元件化的工作時,我們可以把用戶端的程式由 94 行降到只要 28 行,省下了 70% 以上的程式碼撰寫量。不過元件的程式碼量要 200 多行。
我們在元件化的過程中,會用到介面以及工廠方法模式 (Factory Method Pattern),也用到提供者模式 (Provider Pattern),如果對這幾種不了解的,請去找些書或文章來看一下,否則我怕看到一半就放棄了。
首先,我們定義兩個介面,一個是給 DBMS 用的 IDBProvider 介面,另一個是給 DBMS 設定連線資訊的 ISchemaConfiguration 介面:
public interface IDBProvider
{
void Open();
void Close();
void ExecuteCmd(string Statement, Dictionary<string, object> Parameters);
IDataReader ExecuteQuery(string Statement, Dictionary<string, object> Parameters);
}
public interface ISchemaConfiguration
{
string GetConnectionString();
Type GetProviderType();
}
接著加入一個抽象類別 DataProvider,實作 IDBProvider 介面,並標記抽象方法給實作的子類別自行實作用:
public abstract class DataProvider : IDBProvider
{
public abstract void Open();
public abstract void Close();
public abstract void ExecuteCmd(string Statement, Dictionary<string, object> Parameters);
public abstract IDataReader ExecuteQuery(string Statement, Dictionary<string, object> Parameters);
}
有了這個抽象類別後,我們就可以為 SQL Server 寫一個 DataProvider,而實作本身其實沒有很難,只是把前面寫在主程式的公式化作業移到這個 Provider 來做而已:
public class SqlDBProvider : Contracts.DataProvider
{
private SqlConnection _db = null;
public SqlDBProvider(Contracts.ISchemaConfiguration SchemaConfiguration)
{
this._db = new SqlConnection(SchemaConfiguration.GetConnectionString());
}
public override void Open()
{
this._db.Open();
}
public override void Close()
{
this._db.Close();
}
public override void ExecuteCmd(string Statement, Dictionary<string, object> Parameters)
{
SqlCommand cmd = new SqlCommand(Statement, this._db);
cmd.CommandText = Statement;
cmd.CommandType = CommandType.Text;
if (Parameters != null)
{
foreach (KeyValuePair<string, object> param in Parameters)
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
if (this._db.State == ConnectionState.Closed)
this._db.Open();
cmd.ExecuteNonQuery();
this._db.Close();
}
public override IDataReader ExecuteQuery(string Statement, Dictionary<string, object> Parameters)
{
SqlCommand cmd = new SqlCommand(Statement, this._db);
SqlDataReader reader = null;
cmd.CommandText = Statement;
cmd.CommandType = CommandType.Text;
if (Parameters != null)
{
foreach (KeyValuePair<string, object> param in Parameters)
cmd.Parameters.AddWithValue(param.Key, param.Value);
}
if (this._db.State == ConnectionState.Closed)
this._db.Open();
reader = cmd.ExecuteReader(CommandBehavior.CloseConnection | CommandBehavior.SingleResult);
return reader;
}
}
再來,我們要撰寫一個可以自組態檔讀取 DBMS 連線字串組態的程式,也就是實作 ISchemaConfiguration 介面,這件事我們交給 SchemaConfiguration 來負責:
public class SchemaConfiguration : ConfigurationElement, Contracts.ISchemaConfiguration
{
[ConfigurationProperty("name", IsRequired = true, IsKey = true)]
public string Name { get { return base["name"].ToString(); } }
[ConfigurationProperty("type", IsRequired = true, IsKey = true)]
public string Type { get { return base["type"].ToString(); } }
[ConfigurationProperty("connectionstring", IsRequired = true, IsKey = true)]
public string ConnectionString { get { return base["connectionstring"].ToString(); } }
public string GetConnectionString()
{
return this.ConnectionString;
}
public Type GetProviderType()
{
return System.Type.GetType(this.Type);
}
}
而為了符合 Configuration 的規則,我們還實作了 SchemaSetConfiguration 以及 SchemaConfigurationCollection 類別,不過在這就不列出來,請自行打開原始程式碼來看。
到這一步,資料存取的功能已算完成,但我們缺將資料讀入後和物件的對應,所以我們還要加一個核心類別 DataContext,用戶端的呼叫就由它來處理,它會和 DataProvider 實作來溝通,取回資料庫中的資料後,再繫結到物件內。這段程式碼除了繫結的程式外,還包含了一個自動產生 SQL 指令的部份。
public class DataContext
{
private Contracts.IDBProvider _provider = null;
private Configuration.EntitySetConfiguration _entityConfiguration = null;
private DataContext()
{
this._entityConfiguration = ConfigurationManager.GetSection("entitySetConfiguration") as Configuration.EntitySetConfiguration;
}
public DataContext(string SchemaConfigurationName)
: this()
{
this._provider = DBProviderFactory.CreateDBProvider(SchemaConfigurationName) as Contracts.IDBProvider;
}
public T GetEntity<T>()
where T : class
{
T entity = Activator.CreateInstance(typeof(T)) as T;
Configuration.EntityConfiguration entityConfiguration =
this._entityConfiguration.EntityConfigurations.GetConfigurationFromType(typeof(T).FullName);
IDataReader reader = this._provider.ExecuteQuery("SELECT * FROM " + entityConfiguration.SchemaName, null);
while (reader.Read())
{
MethodInfo bindingMethod = this.GetType().GetMethod("DoObjectBinding", BindingFlags.Instance | BindingFlags.NonPublic);
bindingMethod = bindingMethod.MakeGenericMethod(typeof(T));
bindingMethod.Invoke(this, new object[] { reader, entity, entityConfiguration });
}
reader.Close();
return entity;
}
public TCollection GetEntities<TCollection, T>()
where TCollection : ICollection<T>
where T : class
{
TCollection entityCollection = (TCollection)Activator.CreateInstance(typeof(TCollection));
Configuration.EntityConfiguration entityConfiguration =
this._entityConfiguration.EntityConfigurations.GetConfigurationFromType(typeof(T).FullName);
IDataReader reader = this._provider.ExecuteQuery("SELECT * FROM " + entityConfiguration.SchemaName, null);
while (reader.Read())
{
T entity = Activator.CreateInstance(typeof(T)) as T;
MethodInfo bindingMethod = this.GetType().GetMethod("DoObjectBinding", BindingFlags.Instance | BindingFlags.NonPublic);
bindingMethod = bindingMethod.MakeGenericMethod(typeof(T));
bindingMethod.Invoke(this, new object[] { reader, entity, entityConfiguration });
entityCollection.Add(entity);
}
reader.Close();
return entityCollection;
}
private void DoObjectBinding<T>(ref IDataReader reader, ref T entity, Configuration.EntityConfiguration entityConfiguration)
{
PropertyInfo[] properties = entity.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
int ordinal = -1;
Type propType = property.PropertyType;
// get attribute.
Configuration.EntitySchemaMap schemaMap = entityConfiguration.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(entity,
Convert.ChangeType(typeConverter.Convert(reader.GetValue(ordinal)), propType), null);
}
else
{
TypeConverters.EnumConverter converter = typeConverter as TypeConverters.EnumConverter;
property.SetValue(entity,
Convert.ChangeType(converter.Convert(propType, reader.GetValue(ordinal)), propType), null);
}
}
}
}
}
做到這裡,我們還欠缺一個物件,就是 DataContext 如何生成 DataProvider 實作,由於我們已經將組態設定交給了組態檔處理,所以我們需要的是一個由組態檔生成 DataProvider 物件的實作,也就是 DBProvidrFactory:
public class DBProviderFactory
{
public static Contracts.IDBProvider CreateDBProvider(string ConfigurationName)
{
Configuration.SchemaSetConfiguration schemaSetConfiguration =
ConfigurationManager.GetSection("schemaSetConfiguration") as Configuration.SchemaSetConfiguration;
Configuration.SchemaConfiguration schemaConfiguration =
schemaSetConfiguration.Schemas.GetConfigurationFromName(ConfigurationName);
Contracts.IDBProvider provider =
Activator.CreateInstance(schemaConfiguration.GetProviderType(), schemaConfiguration) as Contracts.IDBProvider;
return provider;
}
}
這些都做完後,我們就可以輕鬆的改寫用戶端程式:
public class ProgramStep5
{
static void Main(string[] args)
{
DB.DataContext db = new DB.DataContext("sql");
EmployeeCollection employees = db.GetEntities<EmployeeCollection, Employee>();
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();
}
}
讀者可以和原理 (5) 的主程式比較,是不是簡化了很多?SQL 指令也不見了,只要透過 DataContext.GetEntities<T1, T2>() 就能得到完整的資料庫資料,而且是透過物件來存取,開發人員也不用再寫 SQL。
不過,現實很難這麼美妙 ...
Sample Code: https://dotblogsfile.blob.core.windows.net/user/regionbbs/1111/20111124105026286.rar