陸陸續續用Xcode組件將近一年了,作為一個業余開發者,很感謝大石頭和他的團隊。不僅感謝他們創造如此藝術的組件,更感謝他們耐心的指點,我才學會了使用模板,來開發始於自己風格和功能的通用組件。作為了老的動軟代碼生成器的使用者,但我接觸並學會使用Xcode后,以及2年來在博客園看到的各類開發框架和ORM,我不得不說Xcode是我見過最強大的,小巧,精悍。很早就想寫一篇教程,可能是基礎比較差,寫不出什么高質量的文章,畢竟是業余開發者。可能我的表述很多不專業,只有我自己理解,我也只需要對自己有用的功能。今天寫這篇博文主要是受大石頭的啟發,因為越來越多的人使用Xcode,群里面的人都爆了,但是他們又沒有太多時間來重復回答這些新手的問題,所以需要大家自力更生,看源碼去學會使用。為了貢獻自己對Xcode的理解和使用經驗,使得后來學習的人更加容易,所以才有了我的這篇文章,寫得不好,請拍磚。
1.事情起因
NewLife.CommonEntity里面封裝了一些通用實體類,今天自己做一個小工具(個人喜好,玩玩的),想得到這些通用實體類的數據字典,雖然知道有數據庫反向工程的功能,但就是懶得動手。所以一開始手抄抄,然后在群里面哄了一下,然后群主大石頭提示了一個
CheckTables方法,其他的當然只能靠自己看源碼了。其實往往很多幫助也只需要這么關鍵的一個提示,然后自己去學習。
2.關於數據庫方向工程:就是通過配置文件切換數據庫連接字符串,不需要任何操作,系統會根據實體類自動生成數據庫及數據表。很多ORM雖然可以通過配置文件修改數據庫連接字符串,但還是需要在目標數據庫中新建好表,因此多少有些繁瑣,而Xcode徹底屏蔽了這些東西,不必要關心數據庫。因此從給一個數據庫轉化到另外一個數據庫,是再方便不過。
3.解決過程
在石頭的提示下(CheckTables),我找到了這個方法所在的類DAL(數據訪問層),看看這個方法:

1
///
<summary>
檢查數據表架構,不受反向工程啟用開關限制
</summary>
2 public void CheckTables()
3 {
4 WriteLog( " 開始檢查連接[{0}/{1}]的數據庫架構…… ", ConnName, DbType); 5
6 Stopwatch sw = new Stopwatch();
7 sw.Start();
8 try
9 {
10 List<IDataTable> list = EntityFactory.GetTables(ConnName);
11 if (list != null && list.Count > 0)
12 {
13 // 全都標為已初始化的
14 foreach (IDataTable item in list)
15 {
16 if (!HasCheckTables.Contains(item.Name)) HasCheckTables.Add(item.Name);
17 }
18
19 // 過濾掉被排除的表名
20 if (NegativeExclude.Count > 0)
21 {
22 for ( int i = list.Count - 1; i >= 0; i--)
23 {
24 if (NegativeExclude.Contains(list[i].Name)) list.RemoveAt(i);
25 }
26 }
27 // 過濾掉視圖
28 list.RemoveAll(dt => dt.IsView);
29 if (list != null && list.Count > 0)
30 {
31 WriteLog(ConnName + " 待檢查表架構的實體個數: " + list.Count); 32
33 Db.CreateMetaData().SetTables(list.ToArray());
34 }
35 }
36 }
37 finally
38 {
39 sw.Stop(); 40
41 WriteLog( " 檢查連接[{0}/{1}]的數據庫架構耗時{2} ", ConnName, DbType, sw.Elapsed);
42 }
43 }
2 public void CheckTables()
3 {
4 WriteLog( " 開始檢查連接[{0}/{1}]的數據庫架構…… ", ConnName, DbType); 5
6 Stopwatch sw = new Stopwatch();
7 sw.Start();
8 try
9 {
10 List<IDataTable> list = EntityFactory.GetTables(ConnName);
11 if (list != null && list.Count > 0)
12 {
13 // 全都標為已初始化的
14 foreach (IDataTable item in list)
15 {
16 if (!HasCheckTables.Contains(item.Name)) HasCheckTables.Add(item.Name);
17 }
18
19 // 過濾掉被排除的表名
20 if (NegativeExclude.Count > 0)
21 {
22 for ( int i = list.Count - 1; i >= 0; i--)
23 {
24 if (NegativeExclude.Contains(list[i].Name)) list.RemoveAt(i);
25 }
26 }
27 // 過濾掉視圖
28 list.RemoveAll(dt => dt.IsView);
29 if (list != null && list.Count > 0)
30 {
31 WriteLog(ConnName + " 待檢查表架構的實體個數: " + list.Count); 32
33 Db.CreateMetaData().SetTables(list.ToArray());
34 }
35 }
36 }
37 finally
38 {
39 sw.Stop(); 40
41 WriteLog( " 檢查連接[{0}/{1}]的數據庫架構耗時{2} ", ConnName, DbType, sw.Elapsed);
42 }
43 }
一開始看到這里,感覺不是我想要的,因為DAL里面需要傳入ConnName,這里有點費解,既然要反向工程,那么數據庫肯定不存在表結構,那傳入ConnName有什么用?
但是看到這但代碼,我知道了,反向工程主要是通過獲取實體類的IDataTable來完成的,就是上面代碼第33行 Db.CreateMetaData().SetTables(list.ToArray());
這里才是最關鍵的,呵呵。既然上面這個函數不滿足我的要求,那就重新寫一個滿足要求的,所以我在DAL里面給CheckTables加了一個重載方法,如下所示:
這里才是最關鍵的,呵呵。既然上面這個函數不滿足我的要求,那就重新寫一個滿足要求的,所以我在DAL里面給CheckTables加了一個重載方法,如下所示:

1
public
void CheckTables(List<IDataTable> list)
2 {
3 WriteLog( " 開始檢查連接[{0}/{1}]的數據庫架構…… ", ConnName, DbType);
4 Stopwatch sw = new Stopwatch();
5 sw.Start();
6 try
7 {
8 if (list != null && list.Count > 0)
9 {
10 WriteLog(ConnName + " 待檢查表架構的實體個數: " + list.Count);
11 Db.CreateMetaData().SetTables(list.ToArray());
13 }
14 }
15 finally
16 {
17 sw.Stop();
18 WriteLog( " 檢查連接[{0}/{1}]的數據庫架構耗時{2} ", ConnName, DbType, sw.Elapsed);
19 }
20 }
2 {
3 WriteLog( " 開始檢查連接[{0}/{1}]的數據庫架構…… ", ConnName, DbType);
4 Stopwatch sw = new Stopwatch();
5 sw.Start();
6 try
7 {
8 if (list != null && list.Count > 0)
9 {
10 WriteLog(ConnName + " 待檢查表架構的實體個數: " + list.Count);
11 Db.CreateMetaData().SetTables(list.ToArray());
13 }
14 }
15 finally
16 {
17 sw.Stop();
18 WriteLog( " 檢查連接[{0}/{1}]的數據庫架構耗時{2} ", ConnName, DbType, sw.Elapsed);
19 }
20 }
有了上面的這個函數,只需要把自己想要反向工程的實體類列表傳入進去就可以了,下面是我的調用方法:

1 List<IDataTable> list =
new List<IDataTable>();
2 list.Add(Administrator.Meta.Table.DataTable);
3 list.Add(Area.Meta.Table.DataTable);
4 list.Add(Category.Meta.Table.DataTable);
5 DAL dal = DAL.Create( " Common ");
6 dal.CheckTables(list);
2 list.Add(Administrator.Meta.Table.DataTable);
3 list.Add(Area.Meta.Table.DataTable);
4 list.Add(Category.Meta.Table.DataTable);
5 DAL dal = DAL.Create( " Common ");
6 dal.CheckTables(list);
簡單的幾行代碼,就將 NewLife.CommonEntity中的幾個表的結構自動生成到數據庫了。我印象中記得,可以一下子獲取 NewLife.CommonEntity所有的 List<IDataTable> ,一時也想不起來,所以就只能一個個添加,呵呵,先解決實際問題,其他的再慢慢來搞。
4.繼續分析,雖然上面解決了的實際問題,但沒有對反向工程的整個過程有更深入的了解。而且也萌生了念頭,如此方便,那豈不是很容易的將數據庫進行遷移,表結構和數據都很容易遷移到其他數據庫平台了,正好閑着沒事,寫了一段簡單的代碼,調試看看【剛開始調試了半個小時,馬馬虎虎,有點暈,晚上回來繼續奮戰3個小時,才有點眉目,也難怪,大石頭的團隊花了好多年的成果,我怎么可能這么短時間就消化掉】。代碼很簡單,新建一個管理員,賦值並插入數據,沒有數據庫:

1 Administrator user =
new Administrator();
2 user.Name = " admin ";
3 user.Password = DataHelper.Hash( " admin ");
4 user.DisplayName = " 超級管理員 ";
5 user.RoleID = 1;
6 user.IsEnable = true;
7 user.Insert();
2 user.Name = " admin ";
3 user.Password = DataHelper.Hash( " admin ");
4 user.DisplayName = " 超級管理員 ";
5 user.RoleID = 1;
6 user.IsEnable = true;
7 user.Insert();
斷點調試上述代碼,記下主要內容吧:
1.在new對象的過程中,會調用基類的構造函數 TEntity entity = new TEntity();然后字段賦值
2.在對象插入到數據庫中的過程中(user.Insert())會調用基類的 Insert()方法,在此方法中,有一個Valid(Boolean isNew)方法用來驗證實體的數據是否符合要求,在檢查實體數據過程中,會到數據庫中獲取一定的信息,如果數據表不存在,就會新建數據庫和相應的表結構。這一部分涉及到的類挺多的,我也有些暈。大體如下吧,可能會有理解不到位:
2.1 為了驗證數據,需要判斷該數據是否在數據表中已經存在,所以有了Entity.cs中的CheckExist方法,其中有一個FindCount(names, values)去數據庫獲取滿足記錄的條數。
2.2 然后獲取查詢條件,調用Entity.Meta中的QueryCount方法,然后QueryCount調用WaitForInitData方法進行 檢查並初始化數據,這時會對實體類進行檢查。
2.3 實體類第一次檢查模型時,會調用Entity.Meta中的CheckModel()方法,這其中會對表結構的ModelCheckModes進行檢查,ModelCheckModes分為2種情況,1種是初始化時反向工程,一種是使用時反向工程。Xcode連這一點多考慮到了,說明是多么的細致,因為在我看來即使是初始化時進行,也沒有多少時間。TableItem有一個ModelCheckMode屬性,會去獲取實體類的ModelCheckMode,這個實現用到了Attribute,如果實體類聲明了ModelCheckModeAttribute,那么獲取到的就是這個值,如果沒有聲明,就是CheckAllTablesWhenInit。
2.4 會把需要初始化化的表結構添加到List中,然后調用DBO.Db.CreateMetaData().SetTables(Table.DataTable),這就是最上面提到的那段關鍵代碼,下面進這個方法看看。
2.5 SetTables方法中先對數據庫進行檢查( CheckDatabase()),然后檢查指定的數據表(CheckAllTables(tables))
2.5.1 數據庫檢查時,如果數據庫不存在,會調用SetSchema(DDLSchema.CreateDatabase, null, null)創建數據庫(杯具的我,一邊調試一邊看數據庫里面有沒有生成東西,來回看,到這里才真的生成數據庫了,表還沒有)。
2.5.2 數據表檢查時CheckAllTables(tables),這里面會去判斷對應的表是否存在數據庫,如果不存在就創建表,否則修改表結構。調用CheckTable對單個表進行檢查,創建表主要就是CreateTable方法了,運行完CreateTable,數據表Administrator也在數據庫中出來了。
5.改進。寫了這么多,雖然對整個過程的很多細節還不是很透徹,這篇博客開始寫的時候已經調試了3個小時左右,等寫完又基本上調試了2,3個小時,一遍調試一邊寫。寫到最后,我也對知道應該如何更好的調用方向工程方法了,我最先的調用方法也可以,但是不具有通用性。上面也提到我記得可以一次獲取所有的實體類,可能是開始太粗心,對有些過程不了解,認為EntityFactory.LoadEntities方法不能獲取到所有的實體類,但實際上應該是可以的。這其中有點小風波:
剛開始石頭提醒我用CheckTable,我就發現了EntityFactory有GetTables方法,所以我寫了這句
List<IDataTable> list = EntityFactory.GetTables("Common");
但奇怪的是這樣獲取到的List是空的,納悶了很久,最終放棄了,然后用上面的方法,先解決問題再說。等我快寫完博客,我才發現其中的原因,因為當初沒繼續往下面調試啊。因為默認是“不從未加載程序集中獲取類型”,因為我項目應用的是dll,那么應該需要加載程序集才行。所以我寫了下面的一個方法:

1
///
<summary>
2 /// 獲取指定連接名下的所有實體數據表
3 /// </summary>
4 /// <param name="connName"> 數據庫連接名稱 </param>
5 /// <param name="isLoadAssembly"> 是否加載外部程序集 </param>
6 /// <returns> IDataTable集合 </returns>
7 public static List<IDataTable> GetTables(String connName, bool isLoadAssembly)
8 {
9 var tables = new List<IDataTable>();
10 // 記錄每個表名對應的實體類
11 var dic = new Dictionary<String, Type>(StringComparer.OrdinalIgnoreCase);
12 var list = new List<String>();
13 IEnumerable<Type> Cur = AssemblyX.FindAllPlugins( typeof(IEntity),isLoadAssembly).Where(t => TableItem.Create(t).ConnName == connName);
14 foreach (Type item in Cur )
15 {
16 list.Add(item.Name);
17 // 過濾掉第一次使用才加載的
18 var att = ModelCheckModeAttribute.GetCustomAttribute(item);
19 if (att != null && att.Mode != ModelCheckModes.CheckAllTablesWhenInit) continue;
20 var table = TableItem.Create(item).DataTable;
21 // 判斷表名是否已存在
22 Type type = null;
23 if (dic.TryGetValue(table.Name, out type))
24 {
25 // 兩個實體類,只能要一個當前實體類是,跳過
26 if (IsCommonEntity(item))
27 continue;
28 // 前面那個是,排除
29 else if (IsCommonEntity(type))
30 {
31 dic[table.Name] = item;
32 // 刪除原始實體類
33 tables.RemoveAll((tb) => tb.Name == table.Name);
34 }
35 // 兩個都不是,報錯吧!
36 else
37 {
38 String msg = String.Format( " 設計錯誤!發現表{0}同時被兩個實體類({1}和{2})使用! ", table.Name, type.FullName, item.FullName);
39 XTrace.WriteLine(msg);
40 throw new XCodeException(msg);
41 }
42 }
43 else
44 {
45 dic.Add(table.Name, item);
46 }
47
48 tables.Add(table);
49 }
50
51 if (DAL.Debug) DAL.WriteLog( " [{0}]的所有實體類({1}個):{2} ", connName, list.Count, String.Join( " , ", list.ToArray()));
52
53 return tables;
54 }
2 /// 獲取指定連接名下的所有實體數據表
3 /// </summary>
4 /// <param name="connName"> 數據庫連接名稱 </param>
5 /// <param name="isLoadAssembly"> 是否加載外部程序集 </param>
6 /// <returns> IDataTable集合 </returns>
7 public static List<IDataTable> GetTables(String connName, bool isLoadAssembly)
8 {
9 var tables = new List<IDataTable>();
10 // 記錄每個表名對應的實體類
11 var dic = new Dictionary<String, Type>(StringComparer.OrdinalIgnoreCase);
12 var list = new List<String>();
13 IEnumerable<Type> Cur = AssemblyX.FindAllPlugins( typeof(IEntity),isLoadAssembly).Where(t => TableItem.Create(t).ConnName == connName);
14 foreach (Type item in Cur )
15 {
16 list.Add(item.Name);
17 // 過濾掉第一次使用才加載的
18 var att = ModelCheckModeAttribute.GetCustomAttribute(item);
19 if (att != null && att.Mode != ModelCheckModes.CheckAllTablesWhenInit) continue;
20 var table = TableItem.Create(item).DataTable;
21 // 判斷表名是否已存在
22 Type type = null;
23 if (dic.TryGetValue(table.Name, out type))
24 {
25 // 兩個實體類,只能要一個當前實體類是,跳過
26 if (IsCommonEntity(item))
27 continue;
28 // 前面那個是,排除
29 else if (IsCommonEntity(type))
30 {
31 dic[table.Name] = item;
32 // 刪除原始實體類
33 tables.RemoveAll((tb) => tb.Name == table.Name);
34 }
35 // 兩個都不是,報錯吧!
36 else
37 {
38 String msg = String.Format( " 設計錯誤!發現表{0}同時被兩個實體類({1}和{2})使用! ", table.Name, type.FullName, item.FullName);
39 XTrace.WriteLine(msg);
40 throw new XCodeException(msg);
41 }
42 }
43 else
44 {
45 dic.Add(table.Name, item);
46 }
47
48 tables.Add(table);
49 }
50
51 if (DAL.Debug) DAL.WriteLog( " [{0}]的所有實體類({1}個):{2} ", connName, list.Count, String.Join( " , ", list.ToArray()));
52
53 return tables;
54 }
然后在主程序中調用
List<IDataTable> list = EntityFactory.GetTables("Common");然后一句話我要的東東都有了。呵呵,其他的就不說了。
上面主要是增加了一個參數:IEnumerable<Type> Cur = AssemblyX.FindAllPlugins(typeof(IEntity),isLoadAssembly).Where(t => TableItem.Create(t).ConnName == connName);
呵呵,建議石頭把這個參數補充上去,因為也不費事,要是在4.0里面,來個默認參數。
下面是大石頭對反向工程的說明:
所有實體,默認情況下,根據連接名分組,只要用到這個實體,那么跟這個實體在同一個連接名旗下的所有實體,都會開始進行反向工程,建立數據庫數據表。
除了CheckAllTablesWhenInit的實體,標記CheckAllTablesWhenInit的實體,主要位於CommonEntity,它們是用到之后,只會為自己這個實體創建數據表。
因為CommonEntity里面所有實體都在Common這個連接名下,然后很多時候只需要用到CommonEntity里面的部分表,而不是全部表。
除了CheckAllTablesWhenInit的實體,標記CheckAllTablesWhenInit的實體,主要位於CommonEntity,它們是用到之后,只會為自己這個實體創建數據表。
因為CommonEntity里面所有實體都在Common這個連接名下,然后很多時候只需要用到CommonEntity里面的部分表,而不是全部表。
附上大石頭的博客和Xcode開源網址: