(一)分析
DIscuz!NT 支持2種數據源,SqlServer和MSAccess,但其數據庫訪問層實際上已經支持了 MySQL,只是安裝程序還未提供基於 MySQL 的。
Discuz!NT采用了"頁面類 -> 業務類 -> 數據庫訪問類 -> DbHelper -> 數據庫"這樣的分層方式。數據庫訪問類有1個大接口3個大實現。所謂大接口就是 IDataProvider 接口,定義了900 多個方法。3個大類實現了 IDataProvider 方法,用 partial class 的方式共分了7 個文件。這種大粒度的類設計方式本人覺得不利於以後維護。
3個大類分別為 MySQL SqlServer Access 對 IDataProvider 的實現,輸出3個DLL。在配置文件 DNT.config 裡可以設置采用哪個實現。
總體來說分層還是比較嚴謹清晰的,但也有一些不合理之處:
1)把事務控制放到了數據庫訪問層。由於業務層無法控制事務的發起和終止,這樣它們有一些大的業務實際上是在多個事務裡處理的,容易因為一些意外導致數據不完整或不一致。我個人認為應該把數據庫事務的發起和終止放在業務層。
2)數據庫訪問代碼對SQL語句做了硬編碼,類文件裡SQL和C#語句遍地糾纏在一起,SQL層次結構不清晰,C#代碼也不清晰,導致可維護性降低。
3)代碼重復量太大。本來一些比較標准的SQL語句完全可以不需要多套實現,而Discuz基本上不管差異大小統統重寫。其耐力令人欽佩,但法不可取。
4)過多使用存儲過程,就連發帖子這樣簡單的事情,也寫了個存儲過程來實現,增加了數據庫遷移工作量。
(二)修改
我的想法是用 AppFramework 跨數據庫的特性,簡單地重寫數據庫訪問層。由於時間有限,我只花了3個晚上時間對 ForumManage.cs 進行了。
1、首先把配置文件 DBAccess.config 復制到 Discuz.Web 的 Config 目錄下,設置了正確的數據庫連接串。然後創建了一個新的工程 Discuz.Data.AppFramework,實現 IDataProvider。
2、在新工程裡建了個 GenerateCode目錄,放了7個.DaoGen文件和一個CodeGenPlugin.config。7個.DaoGen文件對應Discuz的7個模塊。每個配置文件負責集中對模塊內的表進行讀寫訪問代碼的生成。花了我1個小時的時間才把那些表分別歸類配置到7個.DaoGen文件中,很快就生成了基本訪問代碼。例如:ForumManage.DaoGen內容:
<?XML version="1.0" encoding="gb2312" ?>
<!-- OR映射配置文件,請參考《AppFramework數據庫訪問中間件使用說明.doc》"入門 -> 安裝-> 添加.DaoGen文件" 章節進行配置 -->
<Map>
<Head
Namespace="Discuz.Data.AppFramework.ForumManage"
Using=""
>
</Head>
<MapItem TableName="dnt_forumfIElds" PrimaryKey="fid" />
<MapItem TableName="dnt_forUMLinks" PrimaryKey="id" />
<MapItem TableName="dnt_forums" PrimaryKey="fid" />
<MapItem TableName="dnt_myposts" PrimaryKey="uid" />
<MapItem TableName="dnt_mytopics" PrimaryKey="uid" />
<MapItem TableNam
$False$
e="dnt_posts1" PrimaryKey="pid" />
<MapItem TableName="dnt_topicidentify" PrimaryKey="identifyid" />
<statement id="GetForumIndexListTable">
<![CDATA[
SELECT CASE WHEN DATEDIFF(n, lastpost, GETDATE())<600 THEN ''new'' ELSE ''old'' END AS havenew,
dnt_forums.*, dnt_forumfIElds.*
FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid
WHERE dnt_forums.parentid NOT IN (
SELECT fid FROM dnt_forums
WHERE status < 1 AND layer = 0)
AND dnt_forums.status > 0
AND layer <= 1
ORDER BY displayorder
]]>
</statement>
<statement id="GetForumIndexList">
<![CDATA[
SELECT CASE WHEN DATEDIFF(n, lastpost, GETDATE())<600 THEN ''new'' ELSE ''old'' END AS havenew,dnt_forums.*, dnt_forumfields.* FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid WHERE dnt_forums.parentid NOT IN (SELECT fid FROM dnt_forums WHERE status < 1 AND layer = 0) AND dnt_forums.status > 0 AND layer <= 1 ORDER BY displayorder
]]>
</statement>
<statement id="GetArchiverForumIndexList">
<![CDATA[
SELECT dnt_forums.fid, dnt_forums.name, dnt_forums.layer, dnt_forumfields.viewperm FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid WHERE dnt_forums.status > 0 ORDER BY displayorder
]]>
</statement>
<statement id="GetSubForum">
<![CDATA[
SELECT CASE WHEN DATEDIFF(n, lastpost,GETDATE())<600 THEN ''new'' ELSE ''old'' END AS havenew,dnt_forums.*, dnt_forumfields.* FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid WHERE parentid = #fid# AND status > 0 ORDER BY displayorder
]]>
</statement>
<statement id="GetForums">
<![CDATA[
SELECT dnt_forums.*, dnt_forumfields.* FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid ORDER BY displayorder
]]>
</statement>
<statement id="SetRealCurrentTopics">
<![CDATA[
UPDATE dnt_forums SET curtopics = (SELECT COUNT(tid) FROM dnt_topics WHERE displayorder >= 0 AND fid=#fid#) WHERE fid=#fid#
]]>
</statement>
<statement id="GetForUMList">
<![CDATA[
SELECT name, fid FROM dnt_forums WHERE dnt_forums.parentid NOT IN (SELECT fid FROM dnt_forums WHERE status < 1 AND layer = 0) AND status > 0 AND displayorder >=0 ORDER BY displayorder
]]>
</statement>
<statement id="UpdateForum">
<![CDATA[
UPDATE dnt_forums SET name=#name#,subforumcount=#subforumcount#,displayorder=#displayorder# WHERE fid=#fid#
]]>
</statement>
<statement id="GetForumInformation">
<![CDATA[
SELECT dnt_forums.*, dnt_forumfields.* FROM dnt_forums LEFT JOIN dnt_forumfields ON dnt_forums.fid=dnt_forumfIElds.fid WHERE dnt_forums.fid=#fid#
]]>
</statement>
<statement id="SetForumsPathList">
<![CDATA[
UPDATE dnt_forums SET pathlist=#pathlist# WHERE fid=#fid#
]]>
</statement>
<statement id="DeletePolls">
<![CDATA[
DELETE FROM dnt_polls WHERE tid IN(SELECT tid FROM dnt_topics WHERE fid=#fid#)
]]>
</statement>
<statement id="DeleteAttachments">
<![CDATA[
DELETE FROM $TablePrefix$attachments WHERE tid IN (SELECT tid FROM $TablePrefix$topics WHERE fid=#fid#) OR pid IN (SELECT pid FROM #postname# WHERE fid=#fid#)
]]>
</statement>
<statement id="GetUserByName">
<![CDATA[
SELECT TOP 1 uid FROM $TablePrefix$users WHERE groupid<>7 AND groupid<>8 AND username=#username#
]]>
</statement>
<statement id="UpdateForumfIEldsModerators">
<![CDATA[
UPDATE $TablePrefix$forumfIElds SET moderators=#moderators# WHERE fid =#fid#
]]>
</statement>
<statement id="ClearForumfIEldsModerators">
<![CDATA[
UPDATE $TablePrefix$forumfIElds SET moderators=NULL WHERE fid =#fid#
]]>
</statement>
<statement id="GetFidInForumsByParentID">
<![CDATA[
SELECT fid FROM $TablePrefix$forums WHERE parentid=#parentid# ORDER BY displayorder ASC
]]>
</statement>
<statement id="CombinationForums1">
<![CDATA[
UPDATE $TablePrefix$topics SET fid=#targetfid# WHERE fid=#sourcefid#
]]>
</statement>
<statement id="CombinationForums2">
<![CDATA[
SELECT COUNT(tid) FROM $TablePrefix$topics WHERE fid IN($fidlist$)
]]>
</statement>
<statement id="GetForumsByParentID">
<![CDATA[
SELECT * FROM $TablePrefix$forums WHERE parentid=#parentid# ORDER BY DisplayOrder
]]>
</statement>
<statement id="GetTopForumIDs">
<![CDATA[
SELECT TOP $statcount$ fid FROM $TablePrefix$forums WHERE fid > #lastfid#
]]>
</statement>
<statement id="TopicTypeExists">
SELECT typeid FROM $TablePrefix$topictypes WHERE name=#typename#
<dynamic>
<isNotNull property="not_typeid">
<![CDATA[
typeid<>#not_typeid#
]]>
</isNotNull>
</dynamic>
</statement>
<statement id="TopicNameExists">
SELECT COUNT(1) FROM $TablePrefix$topicidentify WHERE name=#name#
<dynamic>
<isNotNull property="not_typeid">
<![CDATA[
typeid<>#not_typeid#
]]>
</isNotNull>
</dynamic>
</statement>
</Map>
3、修改 ForumManage.cs。Discuz的業務類向數據庫訪問類提交數據有些是用方法參數來傳,有些是用業務實體來傳,而數據訪問層也多用DataTable 的方式向業務層返回數據。這導致 AppFramework 生成的實體類實際上最業務層用不到了,也用不到 ORMap 的一些優點,導致AppFramework的特能在這裡沒法全部發揮出來。
4、簡單的數據庫增刪改的代碼的修改,以下舉幾個例子,注釋掉的部分是Discuz源代碼,後面是基於 AppFramework 的實現。
/// <summary>
/// 添加友情鏈接
/// </summary>
/// <param name="displayorder">顯示順序</param>
/// <param name="name">名稱</param>
/// <param name="url">鏈接地址</param>
/// <param name="note">備注</param>
/// <param name="logo">Logo地址</param>
/// <returns></returns>
public int AddForUMLink(int displayorder, string name, string url, string note, string logo)
{
//DbParameter[] parms = {
// DbHelper.MakeInParam("@displayorder", (DbType)SqlDbType.Int, 4, displayorder),
// DbHelper.MakeInParam("@name", (DbType)SqlDbType.NVarChar, 100, name),
// DbHelper.MakeInParam("@url", (DbType)SqlDbType.NVarChar, 100, url),
// DbHelper.MakeInParam("@note", (DbType)SqlDbType.NVarChar, 200, note),
// DbHelper.MakeInParam("@logo", (DbType)SqlDbType.NVarChar, 100, logo)
//};
//string sql = "INSERT INTO " + BaseConfigs.GetTablePrefix + "forUMLinks] (displayorder, name,url,note,logo) VALUES (@displayorder,@name,@url,@note,@logo)";
//return DbHelper.ExecuteNonQuery(CommandType.Text, sql, parms);
DntForumlinksParam link = new DntForUMLinksParam();
link.Displayorder.Value = displayorder;
link.Name.Value = name;
link.Url.Value = url;
link.Note.Value = note;
link.Logo.Value = logo;
using (IDBSession ss = DBSessionManager.Default.GetSession())
{
return _forUMLinksDao.Insert(ss, link);
}
}
/// <summary>
/// 刪除指定友情鏈接
/// </summary>
/// <param name="forUMLinkid"></param>
/// <returns></returns>
public int DeleteForumLink(string forUMLinkidlist)
{
//string sql = "DELETE FROM " + BaseConfigs.GetTablePrefix + "forumlinks WHERE id IN (" + forUMLinkidlist + ")";
//return DbHelper.ExecuteNonQuery(CommandType.Text, sql);
using (IDBSession ss = DBSessionManager.Default.GetSession())
{
QueryFilter filter = new QueryFilter();
filter.Add(DntForumlinksDef.ID_FIEldName, DBOperator.In, forUMLinkidlist);
return _forUMLinksDao.Delete(ss, filter);
}
}
/// <summary>
/// 更新指定友情鏈接
/// </summary>
/// <param name="id">友情鏈接Id</param>
/// <param name="displayorder">顯示順序</param>
/// <param name="name">名稱</param>
/// <param name="url">鏈接地址</param>
/// <param name="note">備注</param>
/// <param name="logo">Logo地址</param>
/// <returns></returns>
public int UpdateForUMLink(int id, int displayorder, string name, string url, string note, string logo)
{
//DbParameter[] parms = {
// DbHelper.MakeInParam("@id", (DbType)SqlDbType.Int, 4, id),
// DbHelper.MakeInParam("@displayorder", (DbType)SqlDbType.Int, 4, displayorder),
// DbHelper.MakeInParam("@name", (DbType)SqlDbType.NVarChar, 100, name),
// DbHelper.MakeInParam("@url", (DbType)SqlDbType.NVarChar,100, url),
// DbHelper.MakeInParam("@note", (DbType)SqlDbType.NVarChar, 200, note),
// DbHelper.MakeInParam("@logo", (DbType)SqlDbType.NVarChar, 100, logo)
//};
//string sql = "UPDATE " + BaseConfigs.GetTablePrefix + "forUMLinks SET displayorder=@displayorder,name=@name,url=@url,note=@note,logo=@logo WHERE id=@id";
//return DbHelper.ExecuteNonQuery(CommandType.Text, sql, parms);
DntForumlinksParam link = new DntForUMLinksParam();
link.Displayorder.Value = displayorder;
link.Name.Value = name;
link.Url.Value = url;
link.Note.Value = note;
link.Logo.Value = logo;
link.ID.Value = (short)id;
using (IDBSession ss = DBSessionManager.Default.GetSession())
{
return _forUMLinksDao.Update(ss, link);
}
}
當然也可以用 SqlMap 的方式更新實體:
public int UpdateForum(int fid, string name, int subforumcount, int displayorder)
{
//DbParameter[] parms = {
// DbHelper.MakeInParam("@fid", (DbType)SqlDbType.Int, 4, fid),
// DbHelper.MakeInParam("@name", (DbType)SqlDbType.NChar, 50, name),
// DbHelper.MakeInParam("@subforumcount", (DbType)SqlDbType.Int, 4, subforumcount),
// DbHelper.MakeInParam("@displayorder", (DbType)SqlDbType.Int, 4, displayorder)
//};
//string sql = "UPDATE " + BaseConfigs.GetTablePrefix + "forums SET name=@name,subforumcount=@subforumcount ,displayorder=@displayorder WHERE fid=@fid";
//return DbHelper.ExecuteNonQuery(CommandType.Text, sql, parms);
UpdateForum map = new UpdateForum();
map.displayorder = displayorder;
map.fid = fid;
map.name = name;
map.subforumcount = subforumcount;
using (IDBSession ss = DBSessionManager.Default.GetSession())
{
return map.ExecCmd(ss);
}
}
其中 UpdateForum 在 ForumManage.DaoGen 中的定義如下:
<statement id="UpdateForum">
<![CDATA[
UPDATE dnt_forums SET name=#name#,subforumcount=#subforumcount#,displayorder=#displayorder# WHERE fid=#fid#
]]>
</statement>
5、復雜的查詢代碼的修改。復雜的查詢基本上都放在 ForumManage.DaoGen 裡生成代碼,然後用面向對象的方式來調用。
6、SQL的標准化。把SQL語句中所有的變量或對象名兩端 "[" "]" 去掉。當然這樣還是不徹底的,一些特有的不可移植的函數還要進一步想辦法屏蔽。
(三)總結
1、用 AppFramework修改後,只要是標准的SQL語句都不會有跨數據庫移植的問題。如果全部訪問層代碼完成用 AppFramework 重寫,可以少許多代碼量。
2、用面向對象的方式訪問數據庫,使得SQL語句從C#程序中分離出來,變得清晰。
3、由於業務類不怎麼使用業務實體,AppFramework 生成的代碼在這裡並沒有得到充分利用,其性能優勢也未發揮出來。
4、通過這次實踐,IDBSession 增加了一些功能,特別是查詢返回 IDataReader,使一些人的編程習慣可以得到延續。
5、性能測試發現,性能接近源代碼,只降低1%、2% 左右。如果采用AppFramework生成的和DataReader的方法實體實現業務層,就可以免去不必要的業務數據傳遞開銷和低效的 DataTable 操作,其性能必然大有提高。