架構模式數據源模式之:數據映射器(Data Mapper)


一:數據映射器

關系型數據庫用來存儲數據和關系,對象則可以處理業務邏輯,所以,要把數據本身和業務邏輯糅雜到一個對象中,我們要么使用 活動記錄,要么把兩者分開,通過數據映射器把兩者關聯起來。

數據映射器是分離內存對象和數據庫的中間軟件層,下面這個時序圖描述了這個中間軟件層的概念:

image

在這個時序圖中,我們還看到一個概念,映射器需能夠獲取領域對象(在這個例子中,a Person 就是一個領域對象)。而對於數據的變化(或者說領域對象的變化),映射器還必須要知道這些變化,在這個時候,我們就需要 工作單元 模式(后議)。

從上圖中,我們仿佛看到 數據映射器 還蠻簡單的,復雜的部分是:我們需要處理聯表查詢,領域對象的繼承等。領域對象的字段則可能來自於數據庫中的多個表,這種時候,我們就必須要讓數據映射器做更多的事情。是的,以上我們說到了,數據映射器要能做到兩個復雜的部分:

1:感知變化;

2:通過聯表查詢的結果,為領域對象賦值;

為了感知變化以及與數據庫對象保持一致,則需要 標識映射(架構模式對象與關系結構模式之:標識域(Identity Field)),這通常需要有 標識映射的注冊表,或者為每個查找方法持有一個 標識映射,下面的代碼是后者:

void Main()
{
    SqlHelper.ConnectionString = "Data Source=xxx;Initial Catalog=xxx;Integrated Security=False;User ID=sa;Password=xxx;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False";
    var user1 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
    var user2 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
    (user1 == user2).Dump();
    "END".Dump();
}

    public abstract class BaseMode
    {
        public string Id {get; set;}

        public string Name {get; set;}
    }

    public class User : BaseMode
    {
        static UserMap map = new UserMap();
        public static User FindUser(string id)
        {
            var user = map.Find(id);
            return user;
        }
    }

    public class UserMap : AbstractMapper<User>
    {
        public User Find(string id)
        {
            return (User)AbstractFind(id);
        }
       
        protected override User AbstractFind(string id)
        {
            var user = base.AbstractFind(id);
            if( user == null )
            {
                "is Null".Dump();
                string sql = "SELECT * FROM [EL_Organization].[User] WHERE ID=@Id";
                var pms = new SqlParameter[]
                {
                    new SqlParameter("@Id", id)
                };
               
                var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
                user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();
                if(user == null)
                {
                    return null;
                }
               
                user = Load(user);
                return user;
            }
           
            return user;
        }
       
        public List<User> FindList(string name)
        {
            // SELECT * FROM USER WHERE NAME LIKE NAME
            List<User> users = null;
            return LoadAll(users);
        }
       
        public void Update(User user)
        {
            // UPDATE USER SET ....
        }
    }
   
    public abstract class AbstractMapper<T> where T : BaseMode
    {
        // 這里的問題是,隨着對象消失,loadedMap就被回收
        protected Dictionary<string, T> loadedMap = new Dictionary<string, T>();
       
        protected T Load(T t)
        {
            if(loadedMap.ContainsKey(t.Id) )
            {
                return loadedMap[t.Id];
            }
            else
            {
                loadedMap.Add(t.Id, t);
                return t;
            }
        }
       
        protected List<T> LoadAll(List<T> ts)
        {
            for(int i=0; i < ts.Count; i++)
            {
                ts[i] = Load(ts[i]);
            }
           
            return ts;
        }
       
        protected virtual T AbstractFind(string id)
        {
            if(loadedMap.ContainsKey(id))
            {
                return loadedMap[id];
            }
            else
            {
                return null;
            }
        }
    }
   

上面是一個簡單的映射器,它具備了 標識映射 功能。由於有標識映射,所以我們運行這段代碼得到的結果是:

image

回歸本問實質,問題:什么叫 “數據映射”

其實,這個問題很關鍵,

UserMap 通過 Find 方法,將數據庫記錄變成了一個 User 對象,這就叫 “數據映射”,但是,真正起到核心作用的是 user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();  這行代碼。更進一步的,DataTableHelper.ToList<T> 這個方法完成了 數據映射 功能。

那么,DataTableHelper.ToList<T> 方法具體干了什么事情,實際上,無非就是根據屬性名去獲取 DataTable 的字段值。這是一種簡便的方法,或者說,在很多業務不復雜的場景下,這也許是個好辦法,但是,因為業務往往是復雜的,所以實際情況下,我們使用這個方法的情況並不是很多,大多數情況下,我們需要像這樣編碼來完成映射:

someone.Name = Convert.ToString(row["Name"])

不要懷疑,上面這行代碼,就叫數據映射,任何高大上的概念,實際上就是那條你寫了很多遍的代碼。

 

1.1 EntityFramework 中的數據映射

這是一個典型的 EF 的數據映射類,

    public class CourseMap : EntityTypeConfiguration<Course>
    {
        public CourseMap()
        {
            // Primary Key
            this.HasKey(t => t.CourseID);

            // Properties
            this.Property(t => t.CourseID)
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
            this.Property(t => t.Title)
                .IsRequired()
                .HasMaxLength(100);
            // Table & Column Mappings
            this.ToTable("Course");
            this.Property(t => t.CourseID).HasColumnName("CourseID");
            this.Property(t => t.Title).HasColumnName("Title");
            this.Property(t => t.Credits).HasColumnName("Credits");
            this.Property(t => t.DepartmentID).HasColumnName("DepartmentID");

            // Relationships
            this.HasMany(t => t.People)
                .WithMany(t => t.Courses)
                .Map(m =>
                    {
                        m.ToTable("CourseInstructor");
                        m.MapLeftKey("CourseID");
                        m.MapRightKey("PersonID");
                    });
            this.HasRequired(t => t.Department)
                .WithMany(t => t.Courses)
                .HasForeignKey(d => d.DepartmentID);
        }
    }

我們可以看到,EF 的數據映射,那算是真正的數據映射。最基本的,其在內部無非是干了一件這樣的事情:

數據庫是哪個字段,對應的內存對象的屬性是哪個屬性。

最終,它都是通過一個對象工廠把領域模型生成出來,其原理大致如下:

internal static Course BuildCourse(IDataReader reader)
{
    Course course = new Course(reader[FieldNames.CourseId]);
    contract.Title = reader[FieldNames.Title].ToString();
    …
    return contract;
}

 

二:倉儲庫

UserMap 關於 數據映射器 的概念是不是覺得太重了?因為它干了 映射 和 持久化 的事情,它甚至還得持有 工作單元。那么,如果我們能不能像 EF 一樣,映射器 只干映射的事情,而把其余事情分出去呢?可以,分離出去的這部分就叫做 倉儲庫。

 

三:再多說一點 DataTableHelper.ToList<T>,簡化的數據映射器

其實就是 DataTable To List 了。如果你在用 EF 或者 NHibernate 這樣的框架,那么,就用它們提供的映射器好了(嚴格來說,你不是在使用它們的映射器。因為這些框架本身才是在使用自己的映射器,我們只是在配置映射器所要的數據和關系而已,有時候,這些配置是在配置文件中,有時候是在字段或屬性上加 Attribute,有時候則是簡單但龐大的單行代碼)。我們當然也可以創建自己的 標准的 映射器,Tim McCarthy 在 《領域驅動設計 C# 2008 實現》 中就實現了這樣的映射器。但是,EF 和 NHibernate  固然很好,但是很多時候我們還是不得不使用 手寫SQL,因為:

1:EF 和 NHibernate 是需要學習成本的,這代表者團隊培訓成本高,且易出錯的;

2:不應放棄 手寫SQL 的高效性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM