[Data Access] ORM 原理 (6) : 單純化資料存取程式

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