前言:說來慚愧,我們的SharePoint內網門戶跑了2年,不堪重負,數據量也不是很大,庫有60GB左右,數據量幾萬條,總之由於各種原因吧,網站速度非常慢,具體問題研究了很久,也無從解決,所有考慮用Net重新搭網站,進行數據遷移,也就帶來了數據遷移這個問題。
思路:由於SharePoint的架構和Net有着不一樣的特點,而且SharePoint的數據庫設計是不為人所知的(當然我們可以了解一些,但不完全),雖然也是基於Net架構的,但是我們很難做到Sql To Sql的方式。所以,只能考慮服務器端對象模型,插入到數據庫中的方式,其間,經理給的建議非常合理,就是將SharePoint的數據整理好插入中間庫,然后統一插入到新網站數據庫中。在后來的實踐中,發現這一方法對數據遷移和檢查,都有着非常好的幫助,避免了很多SharePoint對象模型中出錯,但是不好更正的現象。
中間庫設計:
考慮到原內網門戶有列表、文檔庫、圖片庫三種主要類型(特殊列表特殊對待),所以創建了兩個數據庫表,分別用來存List和DocLib,同時再創建兩個表Image和Attachment用來存列表正文中的圖片和列表附件(文檔庫文檔當做列表附件)。
一、用來存儲列表內容的表 -- TABLE [dbo].[List]
[ID] [int] IDENTITY(1,1) NOT NULL,--主鍵ID [WebID] [nvarchar](max) NULL,--所在網站的Guid [ListID] [nvarchar](max) NULL,--所在列表的Guid [ListName] [nvarchar](max) NULL,--列表名稱 [ContentType] [nvarchar](max) NULL,--所屬內容類型 [ItemID] [nvarchar](max) NULL,-- 列表里面的ID [ApprovalState] [int] NULL,--審批狀態 [Title] [nvarchar](max) NULL,--標題 [SubTitle] [nchar](10) NULL,--副標題 [ItemContent] [nvarchar](max) NULL,--內容 [Creator] [nvarchar](max) NULL,--創建者LoginName [CreatorID] [nvarchar](max) NULL,--創建者UserID [DispCreator] [nvarchar](max) NULL,--創建者UserName [Modifier] [nvarchar](max) NULL,--修改者LoginName [ModifierID] [nvarchar](max) NULL,--修改者UserID [DispModifier] [nvarchar](max) NULL,--修改者UserName [CreatTime] [datetime] NULL,--創建時間 [ModifyTime] [datetime] NULL,--修改時間 [TransferDate] [datetime] NULL,--數據遷移時間
二、用來存儲文檔庫/圖片庫的表 -- TABLE [dbo].[DocLib]
[ID] [int] IDENTITY(1,1) NOT NULL,--主鍵ID [WebID] [nvarchar](max) NULL,--所在網站的Guid [ListID] [nvarchar](max) NULL,--所在列表的Guid [ListName] [nvarchar](max) NULL,--列表名稱 [ListType] [nvarchar](max) NULL,--列表類型(文檔庫/圖片庫) [ItemID] [nvarchar](max) NULL,-- 列表里面的ID [ApprovalState] [int] NULL,--審批狀態 [Title] [nvarchar](max) NULL,--標題 [Creator] [nvarchar](max) NULL,--創建者LoginName [CreatorID] [nvarchar](max) NULL,--創建者UserID [DispCreator] [nvarchar](max) NULL,--創建者UserName [Modifier] [nvarchar](max) NULL,--修改者LoginName [ModifierID] [nvarchar](max) NULL,--修改者UserID [DispModifier] [nvarchar](max) NULL,--修改者UserName [CreatTime] [datetime] NULL,--創建時間 [ModifyTime] [datetime] NULL,--修改時間 [Url] [nvarchar](max) NULL,--文檔的Url [TransferDate] [datetime] NULL,--數據遷移時間
三、用來存儲正文圖片的表 -- TABLE [dbo].[Image]
[ID] [int] IDENTITY(1,1) NOT NULL,--主鍵ID [WebID] [nvarchar](max) NULL,--所在Web的Guid [WebSubUrl] [nvarchar](max) NULL,--所在Web的相對WebUrl [ListID] [nvarchar](max) NULL,--所在列表的Guid [ListName] [nvarchar](max) NULL,--列表名稱 [ItemID] [nvarchar](max) NULL,-- 列表里面的ID [ImageUrl] [nvarchar](max) NULL,--內容圖片的Url,多張圖片,逗號分隔
四、用來存儲附件集的表 -- TABLE [dbo].[Attachment]
[ID] [int] IDENTITY(1,1) NOT NULL,--主鍵ID [WebID] [nvarchar](max) NULL,--所在Web的Guid [WebSubUrl] [nvarchar](max) NULL,--所在Web的相對WebUrl [ListID] [nvarchar](max) NULL,--所在列表的Guid [ListName] [nvarchar](max) NULL--列表名稱 [ItemID] [nvarchar](max) NULL,-- 列表里面的ID [AttachUrl] [nvarchar](max) NULL,--附件的Url,多個的時候,逗號分隔
代碼方法段:
首先就是對象模型讀取列表插入List表,然后是對象模型讀取文檔庫/圖片庫插入DocLib表,讀取字段的代碼比較簡單,我們就不過多介紹,就介紹下其間遇到的幾個問題,也避免代碼太多太繁雜。
問題一:正文亂碼
這是一個比較操心的問題,插入數據沒有問題,但是到新系統顯示,發現好多正文帶有雷系”?“之類的東西,這樣子肯定不行,首先想到RePlace,然后想想不太靠譜,因為正文里很有可能有正常的問號,這樣會被替換掉。后來想到可能是編碼問題,后來證實確實是編碼問題,將特別的空格處理替換為 即可,處理如下:
//Content替換空格為 byte[] space = new byte[] { 0xc2, 0xa0 }; string UTFSpace = System.Text.Encoding.GetEncoding("UTF-8").GetString(space); Content = Content.Replace(UTFSpace, " "); Content = DeleteHtmlImgTag(Content); Content = Content.Replace("'", "''");
問題二 處理中途報錯
插入過程中,我們會出現一些操作異常的情況,可能整個程序要運行4-5個小時,但是4個小時的時候,出現異常了,我們很惱火,調試也很困難,因為很難去調試問題,即使把斷點打在Catch里面,調試也是力不從心的,所以,我們必須一次成功,不容許中間出差錯。這樣,我采取了空跑程序(只走對象模型,不插入數據庫,因為Insert很慢,而且幾乎不報錯,錯誤多數出現在對象模型調用上,各種字段沒有、對象為空)和記錄錯誤補錄兩個方式,來避免這樣的問題。
public static void WriteErrorLog(string ErrorMessage) { try { using (StreamWriter sw = File.AppendText(@"log_error " + InsertTime.ToString("yyyy-MM-dd HHmmss") + ".txt")) { sw.WriteLine(ErrorMessage); sw.Dispose(); } } catch{ } Console.WriteLine(ErrorMessage); }
問題三 處理中間的小錯誤
操作過程中,對於代碼編寫的可靠性,要求很好,就像上面所說,一個要跑4-5個小時的程序,4個小時的時候報錯,我們基本就屬於前功盡棄,因為繼續插入是很困難的。所以中間的小問題,對於代碼段的可靠性要求,就非常高了。必要的時候,多加一些Try...Catch...可能會對於效率有一點點影響,但是對於整個程序來說,是非常必要的。
if (!web.Exists){}//判斷web是否存在 list = web.Lists[ListName];//打開的時候Try一下,避免不存在,ListName最好Trim一下 if (list.BaseTemplate == SPListTemplateType.Announcements)//判斷list類型 if (list.Fields.ContainsField("SubTitle"))//判斷是否有SubTitle這個字段 //副標題對象不為空,才賦值,否則賦值為空字符串(下面那行的注釋…) SubTitle = (item["SubTitle"] == null) ? string.Empty : item["SubTitle"].ToString();
問題四 提取正文中的圖片URL
我們數據遷移過程,正文中會帶有圖片,這就要求我們把圖片保存下來,遷移過去,然后還要插入到相同的位置。這是個比較讓人頭疼的問題,首先說下邏輯,讀取正文的時候,用正則表達式獲取所有的圖片(不是絕對路徑的要拼成絕對路徑),然后插入到Image中間庫中,將原來圖片的位置,替換為一個圖片標志,因為之后我們還要把圖片插入到這里。
/// <summary> /// 取得HTML中所有圖片的 URL。 /// </summary> /// <param name="sHtmlText">HTML代碼</param> /// <returns>圖片的URL列表</returns> public static string[] GetHtmlImageUrlList(string sHtmlText) { // 定義正則表達式用來匹配 img 標簽 Regex regImg = new Regex(@"<img\b[^<>]*?\bsrc[\s\t\r\n]*=[\s\t\r\n]*[""']?[\s\t\r\n]*(?<imgUrl>[^\s\t\r\n""'<>]*)[^<>]*?/?[\s\t\r\n]*>", RegexOptions.IgnoreCase); // 搜索匹配的字符串 MatchCollection matches = regImg.Matches(sHtmlText); int i = 0; string[] sUrlList = new string[matches.Count]; // 取得匹配項列表 foreach (Match match in matches) sUrlList[i++] = match.Groups["imgUrl"].Value; return sUrlList; }
問題五 將正文中的圖片Url換為標識<ImgType>
同樣使用正則表達式,將圖片標簽<img.../>替換為我們特定的標識,為將來replace回來做准備,代碼附下:
/// <summary> /// 去處HTML中所有圖片的img標簽。 /// </summary> /// <param name="sHtmlText">HTML代碼</param> /// <returns>去除img標簽后的Html</returns> public static string DeleteHtmlImgTag(string sHtmlText) { string result = Regex.Replace(sHtmlText, @"<img.*?src=(['""]?)(?<url>[^'"" ]+)(?=\1)[^>]*>", delegate(Match m) { return "<ImgType>"; }); if (result.IndexOf("</img>") > 0) { result = result.Replace("</img>", ""); } if (result.IndexOf("</IMG>") > 0) { result = result.Replace("</IMG>", ""); } return result; }
中間庫到新系統:
經過將SharePoint中數據,整理插入到中間庫的過程,我們等於已經完成80%的工作,因為剩下的內容,就是Sql To Sql的問題了,對於net開發人員,甚至不需要設計,你只需要了解新系統的數據庫結構,相應字段插入就可以了。唯一要提到的就是附件/圖片處理的問題,下面我說下我的處理方式:
附件/圖片處理
這也是一個比較棘手的問題,因為眾所周知的原因,SharePoint的附件/圖片是BLOB的形式,存儲在數據庫中的(我嘗試去數據庫中找這個字段,沒找到);所以我們只能用對象模型,當然SPFile是我們第一時間想到的,但是效率可想而知(效率太慢放棄);所以考慮先將附件/圖片的Url地址拼接好,插入到Images/Attachment的中間庫中,然后采取WebClient的對象去下載為Byte[],然后直接上傳,測試結果還是很客觀的,100個附件1分鍾左右(與附件大小有關)。
using (WebClient wc = new WebClient()) { NetworkCredential networkCredential = new NetworkCredential("用戶名", "密碼", "域"); wc.Credentials = networkCredential; byte[] ss = wc.DownloadData(url); return ss; }
總結:數據遷移過程比較繁雜,需要考慮的東西比較多,前期的規划很重要,因為數據一旦遷移過去,修修補補會很讓人郁悶,所以對應關系一定一定要先做好,避免后期修改。而且,兩邊系統的開發人員對接非常重要,避免出現少插入字段等現象,造成新系統出問題。基本上就是以上這些,寫出來給有需要的人們參考下,就這樣了。