浅谈.Net版(C#)的CMP模式
生活随笔
收集整理的這篇文章主要介紹了
浅谈.Net版(C#)的CMP模式
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
商城上線快2、3個月了,一直都懶得寫點東西,在加上雜七雜八的事情也比較忙,所以都沒有把這個系統當時做的整個架構思緒整理清,昨天才從深圳完了兩天回來,怎感覺是要做的事來著.剛開始接觸CMP模式的時候也是看了它幾天,到谷歌百度里面一搜,我們博客園里面就有蛙蛙池塘的提高軟件開發效率三板斧之二利用CMP模式一文里有它的詳細介紹,在這里我自己也對這個用CMP模式拿來真正上項目時候的問題來做個總結.
- 項目名稱:惠海IT商城
- 網 ? ? ?? 址:http://www.huihaimall.com/
- 開發環境:WinXP SP3、IIS5.0、Dreamweaver、VS 2005、SQL-Server 2000
- 項目描述:項目實現了商品的瀏覽篩選(主要是公司做的IT產品)、會員商品收藏、訂購(訂單)、發郵件推薦給朋友、會員積分、收貨地址薄、DIY自主裝機等,業務邏輯全部在本項目中用.NET(CMP)實現,而展示就不一定都是用.net的aspx頁面來做,如DIY裝機就是用Flex生成flash來實現的,但是它們都是同步的(同登陸同注銷,包括會員產品收藏等).還有一個最重要的就是后臺管理也是用Flex調用.net來實現的,由于要提供Flex調用的接口,所有還提供了幾個WebService的頁面(關與身份驗證請見:在WebService中使用Session或Cookie---實現WebService身份驗證(客戶端是Flex) ),另外在用JQuery發送Ajax請求的時候頁面傳輸數據時候還有用到Json數據(Flex好象有幾個地方也用到了).
- 項目解決方案截圖如下:
下面,我對上圖所示以我的了解進行簡要的介紹:
| 名稱 | 功能描述 | |
| CMPConfigurationHandler | 繼承自IConfigurationSectionHandler,用來讀取在Web.Config文件內的自定義CMP配置. | |
| CommandMapping | 命令映射類,用于某一個業務的容器,一般為對應Insert、Delete、Update、Select里某一個存儲過程名為CommandName,里面可能包含多個CommandParameter. | |
| CommandParameter | 存儲過程參數類,里面有ParameterName、Size、DbTypeHint等屬性,還有一個ClassMember的屬性,表示對應實體模型的屬性,一般ParameterName為@Name而ClassMember值就為Name(預先配置好的),因為一般存儲過程的參數名就對應數據庫實體模型的列字段. | |
| ContainerMapping | 容器映射類,一般為一個業務實體,比如用戶,它里面就有Insert、Delete、Update、Select這4個CommandMapping,而且它有個key在CMP里面的映射ID叫ContainerMappingId和ContainedClass對應為實體對象模型名. | |
| ContainerMappingSet | 多個ContainerMapping容器映射集合類,里面的Hashtable可根據ContainerMappingId映射ID的key來匹配ContainerMapping. | |
| PersistableObject | 持久對象基類,實體類繼承它能實現對數據的保存(一般為Insert、Delete、Update命令操作). | |
| PersistableObjectSet | 繼承自PersistableObject,實現數據持久化保存結果(一般為Select命令操作). | |
| SqlPersistenceContainer | 業務的容器基類,構造函數需ContainerMapping,包含Insert、Delete、Update、Select四個虛方法. | |
| StdPersistenceContainer | 業務的容器,構造函數ContainerMapping調用父類構造函數,根據ContainerMapping對Insert、Delete、Update、Select四個方法進行具體的實現. |
上圖是對會員收貨地址和優惠卷的實例,其中Item結尾的繼承自PersistableObject的實體類,Manager為所有操作方法的集合類(以靜態方式提供),Set繼承自PersistableObjectSet為數據集合的容器類,其實最初CMP里實體的命名不是這樣的,好象是加Entity后綴,這個就看你怎么決定了,但是整個項目一定要統一.
- 下面來說一下Web.Config文件的相關配置
- 在configuration下的configSections的第一個子節點,配置CMP讀取的自定義節點
<section name="GWConfig" type="Huihai.Mall.CMPServices.CMPConfigurationHandler, Huihai.Mall.CMPServices" /> - 再在configSections下增加CMP跟蹤監視等級及數據庫連接字符串,默認本地日志文件路徑等配置(以下為我項目里面的一些CMP配置,最下面的為商城的一些配置)
<system.diagnostics>
<switches>
<add name="GWTrace" value="4" />
</switches>
</system.diagnostics>
<appSettings>
<add key="ErrorViewUrlPrefix" value="/Wawacrm/ErrorLog/" />
<add key="ErrorLogBaseDir" value="E:\me\bak\oa\WAWACRM\Errors\" />
<add key="DefaultDataSource" value="server=127.0.0.1;database=HH_System;uid=sa;pwd=123" />
<add key="picurl" value="Upload/product/" />
<add key="score" value="1" />
<add key="isShowRunTime" value="true" />
<!-- 產品評論 -->
<add key="CommentIsAudit" value="Y" />
<!-- 產品評論回復 -->
<add key="ReCommentIsAudit" value="Y" />
<!-- 產品咨詢 -->
<add key="ConsultIsAudit" value="N" />
<!-- 商城咨詢反饋 -->
<add key="FeedbackIsAudit" value="Y" />
<!-- 后臺管理員的使用的郵箱名后綴 -->
<add key="EmailPostfix" value="@coreoa.cn" />
</appSettings> - 接下來最重要的也是最復雜的就是GWConfig自定義CMP元數據配置的節點了, 它里面主要是配置每一個存儲過程,對應容易實體類,及該業務實體的Insert、Update、Delete、Select四個方法的參數的詳細描述,已上面MallMemberDAL項目的Address和Coupon為例,它的配置為如下:
<GWConfig>
<ContainerMappingSet>
<ContainerMapping>
<ContainerMappingId>Address</ContainerMappingId>
<ContainedClass>AddressItem</ContainedClass>
<Insert>
<CommandName>MO_Address_Insert</CommandName>
<Parameter>
<ClassMember>Member_inner_code</ClassMember>
<ParameterName>@member_inner_code</ParameterName>
<DbTypeHint>Int</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>4</Size>
</Parameter>
<Parameter>
<ClassMember>Province</ClassMember>
<ParameterName>@province</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>20</Size>
</Parameter>
<Parameter>
<ClassMember>City</ClassMember>
<ParameterName>@city</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>20</Size>
</Parameter>
<Parameter>
<ClassMember>County</ClassMember>
<ParameterName>@county</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>20</Size>
</Parameter>
<Parameter>
<ClassMember>Zip</ClassMember>
<ParameterName>@zip</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>6</Size>
</Parameter>
<Parameter>
<ClassMember>Address</ClassMember>
<ParameterName>@address</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>100</Size>
</Parameter>
<Parameter>
<ClassMember>Name</ClassMember>
<ParameterName>@name</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>50</Size>
</Parameter>
<Parameter>
<ClassMember>Mobile</ClassMember>
<ParameterName>@mobile</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>15</Size>
</Parameter>
<Parameter>
<ClassMember>Tel</ClassMember>
<ParameterName>@tel</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>50</Size>
</Parameter>
<Parameter>
<ClassMember>IsDefault</ClassMember>
<ParameterName>@isDefault</ParameterName>
<DbTypeHint>Char</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>1</Size>
</Parameter>
</Insert>
</ContainerMapping>
<ContainerMapping>
<ContainerMappingId>Coupon</ContainerMappingId>
<ContainedClass>CouponItem</ContainedClass>
<Select>
<CommandName>MO_Coupon_Select</CommandName>
<Parameter>
<ClassMember>Sn</ClassMember>
<ParameterName>@sn</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>10</Size>
</Parameter>
<Parameter>
<ClassMember>Password</ClassMember>
<ParameterName>@password</ParameterName>
<DbTypeHint>Varchar</DbTypeHint>
<ParamDirection>Input</ParamDirection>
<Size>10</Size>
</Parameter>
</Select>
</ContainerMapping>
</ContainerMappingSet>
</GWConfig> - 以上配置中,為簡便示例Address只有配置Insert的方法,它調用名為MO_Address_Insert的存儲過程,Insert節點下第一個子節點CommandName即為存儲過程名,而同級別的還有Parameter節點,每一個都代表存儲過程的一個參數,下面有ClassMember、ParamDirection等子節點即是對它的描述,其他Update、Delete、Select依此類推也可以按這種格式來配置;而Coupon只有配置一個Select的節點(其他的也能按照上述增加配置,沒有配置的方法將不能調用),Coupon是為ContainerMappingId容器業務實體唯一Key,它對應名為CouponItem的業務實體模型類,而參數下面的ClassMember為實體下的成員名,一般就是實體類的屬性,因為它將根據這個名字反射獲得實體的屬性值.以上這么多配置但是卻只實現了2個業務的方法,是不是有點太麻煩了?沒得辦法,最開始的CMP模式就是這樣一個個把存儲過程配置好的!但是你的項目里面要用到的存儲過程肯定不只與2個,姑且就不說CMP模式這樣配置也只能用存儲過程的,但是存儲過程一多了就會很麻煩了,每一個存儲過程配置都要手寫添家進去!要是存儲過程上十來個參數又有增刪改查的方法豈不要配置很多東西?的確要配置的東西不少,但是我們可以自己寫的小程序對CMP的配置執行操作,而不需要我們去打開Web.Config文件一個個去手改,下圖為我自己寫的一個對CMP的配置進行操作的截圖:
- 上面主要是通過XPath表達試對Web.Config進行篩選讀取CMP的配置節點并在GridView里面顯示出來,并能直接對其進行修改等操作.如果假如我們要增加一個的時候,我們可以點瀏覽用sysobjects把數據庫里面所有的存儲過程名字等信息讀出來,選擇要配置的那個存儲過程后再用sp_sproc_columns把它所有的參數信息讀出來,如ParameterName、Size、ParamDirection等,注意ClassMember(實體模型的屬性名)讀出來默認是ParameterName,因為在我的項目里面ParameterName名就跟實體的屬性名一樣,你也可以改成其他的,但是你要根據你實體命名的約定好,因為它要用反射實體里找這個屬性,如果找不到就不好了.
- 是不是用上面那個小工具爽多了?你不需要打開Web.Config手寫一個配置,在數據庫里面寫一個存儲過程就到這里配置一下直接在增加一個CMP的配置節點到Web.Config文件內.,我這個商城的項目下來把所有業務的存儲過程配置完Web.Config文件總大小有140KB,把Web.Config文件撐得這么大對性能會不會有影響呢?我想應該不會有多大影響的,從CMP框架的運行來看,它只是在Application_Start的時候讀取一次配置文件到內存,以后就像讀AppSettings一樣直接在內存里面取,很方便的.Application_Start運行的代碼如下:
- // 在應用程序啟動時運行
System.Configuration.ConfigurationManager.GetSection("GWConfig");
SiteProfile.DefaultDataSource = System.Configuration.ConfigurationManager.AppSettings["DefaultDataSource"];
SiteProfile.DbTypeHints["Varchar"] = System.Data.SqlDbType.VarChar;
SiteProfile.DbTypeHints["Nvarchar"] = System.Data.SqlDbType.NVarChar;
SiteProfile.DbTypeHints["Int"] = System.Data.SqlDbType.Int;
SiteProfile.DbTypeHints["Date"] = System.Data.SqlDbType.DateTime;
SiteProfile.DbTypeHints["Text"] = System.Data.SqlDbType.Text;
SiteProfile.DbTypeHints["Bit"] = System.Data.SqlDbType.Bit;
SiteProfile.DbTypeHints["Money"] = System.Data.SqlDbType.Money;
SiteProfile.DbTypeHints["Datetime"] = System.Data.SqlDbType.DateTime;
//后新添加2種對應類型
SiteProfile.DbTypeHints["Char"] = System.Data.SqlDbType.Char;
SiteProfile.DbTypeHints["Numeric"] = System.Data.SqlDbType.Decimal;
SiteProfile.DbTypeHints["Smalldatetime"] = System.Data.SqlDbType.SmallDateTime;
//監聽配置文件的改變
System.IO.FileSystemWatcher fsw = new System.IO.FileSystemWatcher(Server.MapPath("~/Upload/special"));
fsw.Filter = "XMLFile.xml";
fsw.NotifyFilter = System.IO.NotifyFilters.LastWrite;
fsw.Changed += new System.IO.FileSystemEventHandler(ReadAdConfig);
fsw.EnableRaisingEvents = true;
ReadAdConfig(null, null); - 上面代碼主要是在程序啟動的時候Web.Config的CMP配置的元數據讀到CMP的一個靜態類里面,初始化數據連接,設置存儲過程參數對應的SQL數據類型,后面監聽和ReadAdConfig方法是我項目另外的東西,這里就不叉開話題說了. 配置搞清楚了,下面我們就來研究一下CMP這個架構到底是如何運行的呢? 還是以上面的那個例子為準,比如我要在Address里增加一個會員收貨地址.我就在AddressManager里面提供一個AddressInsert的靜態方法供調用.
- public static void AddressInsert(AddressItem item)
{
SqlPersistenceContainer spc = new SqlPersistenceContainer(CMPConfigurationHandler.ContainerMaps["Address"]);
spc.Insert(item);
} - 上面所示為CMP一個調用業務的過程,它實現的全過程大概為:
- ①第一步: 在Application_Start把CMP的所有配置讀取到CMPConfigurationHandler下面的ContainerMaps集合里面,它是一個Hashtable對象
- ②第二步: 根據Address這個ContainerMappingId的key在CMPConfigurationHandler.ContainerMaps匹配到Address這個業務對象的ContainerMapping映射容器對象
- ③第三步: 實例化SqlPersistenceContainer托管容器對象,ContainerMapping作為構造函數穿入,并使用: base(initCurrentMap)調用StdPersistenceContainer父類構造函數
- ④第四步: 執行具體方法(這里為Insert操作),而參數為繼承了PersistableObject類型的實體模型對象,為了更好的說明我也把Insert方法的代碼給帖出來
- /// <summary>
///
/// </summary>
/// <param name="insertObject"></param>
public override void Insert(PersistableObject insertObject)
{
GWTrace.EnteringMethod(MethodBase.GetCurrentMethod());
try
{
CommandMapping cmdMap = currentMap.InsertCommand;
SqlCommand insertCommand = BuildCommandFromMapping(cmdMap);
AssignValuesToParameters(cmdMap, ref insertCommand, insertObject);
insertCommand.Connection.Open();
insertCommand.ExecuteNonQuery();
insertCommand.Connection.Close();
AssignOutputValuesToInstance(cmdMap, insertCommand, ref insertObject);
insertCommand.Dispose();
}
catch (Exception dbException)
{
string s = insertObject.ToXmlString();
s+=currentMap.InsertCommand.CommandName;
throw new Exception("Persistance (Insert) Failed for PersistableObject", dbException);
}
} - 由于受空間限制沒能把全部代碼貼出來比較難看一點,大概原理是根據PersistableObject 對象(實際為保存在XML內的CMP的配置映射到的容器)和要執行的方法創建SqlCommand對象,設置它要執行的存儲過程名稱.再循環Parameter創建并添加參數,再在PersistableObject 根據反射ClassMember獲得實體里面的屬性值設置Parameter參數值.然后在執行存儲過程,并跟蹤記錄當前錯誤方法,處理異常信息,這樣便完成了CMP一個業務處理的全過程.
- 是不是感覺這樣調用有點'妙'呢?你要修改那個業務對象你只需要指定對應的實體模型的屬性值它就能作為參數Insert(Update或Delete)數據庫表里面對應的列,如果存儲過程參數對應的實體屬性值沒有指定的話將傳遞的為默認值,如String類型的將為NULL.這里在反射的時候還要注意一個問題,就是列為時間類型的時候,C#里面的DataTime為空的時候為初始值為0001-1-1 0:00:00,而當你使用反射的時候把這個時間更新到數據庫會報錯,提示什么數據溢出,因為SQL里時間類型是有一個時間段,所以在CMP調用AssignValuesToParameters方法使用發射賦值的時候要加上一個判斷,這樣修改時間類型字段指定為空就沒有問題了.
- object o = PropertyInfo.GetValue(persistObject, null);
if (o == null)
o = DBNull.Value;
if (o.GetType().Equals(typeof(System.DateTime)))
{
//時間默認值(即未給時間賦值),則為空
if (o.ToString() == "0001-1-1 0:00:00")
o = DBNull.Value;
}
- 而Select查詢操作略有一點不同, 因為它要返回結果解,這個時候就應該要用AddressSet類了,所有要實現查詢操作的類都必須繼承自PersistableObjectSet,因為它能有返回DataSet數據集的實現,可以把它看成一個特殊的PersistableObject對象,因為它除開有Insert、Delete、Update還有Select.AddressSet類其實也是一個實體類,但它跟AddressItem類不同的是它只管查詢,而查詢的存儲過程往往沒有增、刪、改的那么多的參數,因此它的里面只需要幾個查詢條件字段的屬性.調用起來跟增、刪、改的操作都差不多,也是根據反射它里面的屬性值在賦個對應的參數在執行,CMP默認返回的一個DataSet,我的覺得既然用到的實體,為什么不用它來代替DataSet呢?所以我在原來的基礎上新加一個用反射把DataSet轉換成實體的數組,方便再次操作,代碼如下:
- /// <summary>
/// 根據引用傳來的Object實體對象使用反射給它的屬性賦值
/// </summary>
/// <param name="obj">實體對象</param>
/// <returns>是否給表里面的記錄值填充實體里屬性成功,如何找不到實體屬性或記錄集為空返回false</returns>
public bool ResultSingleObject(ref Object obj)
{
//internalData是本類里返回的DataSet集合
if (internalData.Tables.Count == 0)
return false;
DataTable tab = internalData.Tables[0];
if (tab.Rows.Count == 0)
return false;
Type type = obj.GetType();
foreach (DataColumn column in tab.Columns)
{
string columnName = column.ColumnName;
object t = tab.Rows[0][columnName];
PropertyInfo property = type.GetProperty(columnName);
if (property == null)
property = type.GetProperty(columnName.Substring(0, 1).ToUpper() + columnName.Substring(1, columnName.Length - 1));
if (property == null)
property = type.GetProperty(columnName.ToUpper());
if (property != null)
{
if (t.GetType().FullName != "System.DBNull")//如果數據庫返回的值不等于NULL的情況下才給找到了實體的字段屬性賦值
property.SetValue(obj, t, null);
}
else
throw new Exception("表的列名為" + columnName + "不能與實體名為" + type.Name + "的屬性名一致,請修改過程的返回的列名稱或實體屬性名");//便與調試
}
return true;
} - 通過如上代碼for循環一下就能把DataSet轉換成AddressItem數組或是List<AddressItem>泛型.
- CMP模式差不多就這么多些吧,總結其中一些美中不足的地方:
- Insert、Delete、Update、Select這個四個方法還不能滿足需求,增刪改查這個四個方法是CMP的核心,但一個業務實體的操作只有這個4個方法往往是不夠的.因為這四個方法只能分別對應一個儲存過程,而查詢的存儲過程一般一個難得搞定,比如用戶表我除開根據用戶ID去查詢,還要根據用戶名和密碼,還有可能要根據用戶類型返回用戶列表等等,實際的需求是復雜的.用CMP的話那我還需要另外單獨配置2個業務實體來分別放根據用戶名和密碼和根據用戶類型的操作的存儲過程,而它們里面的Select都只有一個且對應的實體也是同一個,是不是感覺有點浪費?感覺Insert、Delete、Update、Select這個四個方法不夠用,后來想到增加一個List的方法,基本上每一個表都有一個根據ID去查詢記錄的時候,Select就對應這個操作,而新加的List就對應根據其他條件可能會返回多條記錄的操作,這樣就不需要當一個業務有2個查詢操作的時候而再去新建一個了,但是新增一個List查詢的方法Web.Config內CMP配置文件也要加List節點的配置,而且CMP的基類里面也要增加一個List方法并要解析對應的List配置節點,由于當時項目做得快差不多了,這人一懶呢就沒有去完成了.-_-!!!
- 連續寫了幾個晚上終于快要完了,之所以我要寫這些,是感覺自己最近好象都沒做什么東西一樣,白天在慢悠悠的上一天班,晚上就什么也不想動了,一坐到電腦前就是看玩傳奇世界或是看電視連續劇斗牛要不要、籃球火等啊,呵呵,等來得急看時間的時候已經凌晨過后了...第二天起來到公司上班,晚上又繼續,我心是想我不要每天就這樣過去了,但是這樣持續了好久一段時間...我有時候也自責自己到最后還是一事無成!既然來到了這行,就一定要做好這個職業的本職工作,IT行業這個技術每天都在不斷更新演變的領域你每天不去學習怎么能行呢?所以我不要再那么在'墮落'下去了,呵呵^_^,便決定寫點東西,至少別自己做過的項目都不記得去了.
- 源代碼就不要問了,放在公司的SVN服務器里面了,這里提供一個我原來的參考的CMP架構的源代碼下載, 好象還是蛙蛙池塘2005年寫的,有興趣的朋友可以下載去看下.
- CMP模式參考源代碼下載 特別申明:本文及內容如非特別注明,均為本人Jonllen原創,版權均歸原作者個人所有,轉載必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
轉載于:https://www.cnblogs.com/Jonllen/archive/2008/12/30/Jonllen.html
總結
以上是生活随笔為你收集整理的浅谈.Net版(C#)的CMP模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: http://www.tldp.org/
- 下一篇: 1G超爽网络硬盘