導言:
到目前為止,我們的教程圍繞的是text數據。然而,很多應用程序既需要處理text數據,也需要處理二進制數據。比如招聘網站可能需要用戶上傳Word或PDF格式的簡歷。
使用二進制數據面臨一項挑戰:在應用程序中如何存儲二進制數據。我們必須更新添加記錄的界面以支持用戶上傳本地電腦中的文件,並添加額外的功能以下載某條記錄的相關二進制數據。本章以及接下來的3章,我們探討如何處理這些問題。在本系列教程結束時,我們將創建一個功能完善的應用程序,它為每種類型的記錄提供相關的圖片和PDF小冊子。 在本系列教程,我們探討存儲二進制數據的各種方法,考察如何允許用戶從自己的電腦上傳文件並存儲在服務器的文件系統裡。
注意:二進制數據有時候被稱為“BLOB”(Binary Large OBject的縮寫)。本教程我選擇使用術語“binary data”,即使它和術語BLOB同意。
第1步: 添加Working with Binary Data教程頁
我們先花一點時間在網站裡創建一些頁,這些頁會在本教程裡用到.先添加一個名為BinaryData的文件夾,然後添加如下頁面.確保每頁都選擇了Site.master作為母板頁.
Default.aspx
FileUpload.aspx
DisplayOrDownloadData.aspx
UploadInDetailsView.aspx
UpdatingAndDeleting.aspx
圖1:添加所需要的頁面
象其它文件夾一樣,BinaryData文件夾裡的Default.aspx 用來列出教程章節.記得SectionLevelTutorialListing.ascx 這個用戶控件提供了這個功能.因此,從解決方案浏覽裡將這個用戶控件拖到頁面上.
圖2:添加SectionLevelTutorialListing.ascx 用戶控件 到Default.aspx
最後,將這些頁的地址加到 Web.sitemap 的條目裡.在Enhancing the GridView <siteMapNode>之後添加下面的標記.
<siteMapNode title="Working with Binary Data" url="~/BinaryData/Default.aspx" description="Extend the data model to include collecting binary data."> <siteMapNode title="Uploading Files" url="~/BinaryData/FileUpload.aspx" description="Examine the different ways to store binary data on the web server and see how to accept uploaded files from users with the FileUpload control." /> <siteMapNode title="Display or Download Binary Data" url="~/BinaryData/DisplayOrDownloadData.aspx" description="Let users view or download the captured binary data." /> <siteMapNode title="Adding New Binary Data" url="~/BinaryData/UploadInDetailsView.aspx" description="Learn how to augment the inserting interface to include a FileUpload control." /> <siteMapNode title="Updating and Deleting Existing Binary Data" url="~/BinaryData/UpdatingAndDeleting.aspx" description="Learn how to update and delete existing binary data." /> </siteMapNode>
修改完Web.sitemap後,在浏覽器裡看一下本教程站點。
圖3:Site Map包含了本教程
第2步:將二進制數據存儲在什麼地方
有2種方式存放二進制數據:一種是將其存儲在服務器的文件系統裡,並將文件路徑存儲在數據庫裡;第二種是直接將其存儲在數據庫裡(見圖4)。2種方法各有其優點和缺點。
圖4:可以將二進制數據存儲在文件系統或直接放在數據庫
假設我們對數據庫Northwind進行擴展,每個產品對應一幅圖片。一種方法是在服務器文件系統存儲這些圖片,然後在表Products裡記錄該圖片的文件路徑。為此,我們要在
Products表裡添加一列,名為ImagePath,類型是varchar(200)。假設用戶為產品Chai上傳一張圖片時,圖片可能存放在服務器文件系統的~/Images/Tea.jpg位置。這裡,~代表應用程序的物理位置。也就是說,如果該網站根植於C:/Websites/Northwind/的話,~/Images/Tea.jpg相當於C:/Websites/Northwind/Images/Tea.jpg 。上傳圖片後,我們應在表Products更新記錄Chai,使其ImagePath列引用圖片的路徑。如果我們決定將所有產品的圖片放在應用程序的Images文件夾,我們可以使用“~/Images/Tea.jpg”或“Tea.jpg”來表示。
將二進制數據放置在文件系統的主要優點在於:
1. 執行方便——就像我們即將看到的那樣,將二進制數據直接放置在數據庫和存儲於文件系統相比,當用戶需要存儲並獲取數據時需要更多的代碼。另外,為使用戶查看或下載數據,必須用到定位於該數據的URL(譯注:統一資源定位器)。如果數據存儲在文件系統,URL是直觀明了的;如果是存儲在數據庫裡,則必須創建一個頁面來獲取並返回數據。
2.訪問范圍寬——其它的服務或程序有時需要訪問二進制數據,但當二進制數據存儲在數據庫中時,這些服務或程序便無法訪問了。比如,用戶可能希望通過FTP來訪問每個產品對應的圖片,在這種情況下,最好將其放置在文件系統。
3.更好的執行效能——將二進制數據放置在文件系統和數據庫相比,在數據庫服務和服務器服務之間的查詢和網絡堵塞情況要少一些。
將二進制數據放置在文件系統的主要缺點在於削弱了數據的關聯性。比如我們從表Products刪除一條記錄時,放在文件系統中的相關文件不會自動刪除,因此我們必須手寫代碼將其刪除。不然的話,隨著文件碎片的慢慢積累,文件系統會變的混亂不堪。另外,對數據庫的任何改動,都要對在文件系統裡的相應二進制數據做修改。比如將數據庫轉移到另外的站點或服務器時便面臨這種挑戰。
做為選擇,你可以在Microsoft SQL Server 2005裡創建一個類型為varbinary的列,用於存儲二進制數據。你可以指定存儲數據的最大長度,比如你希望數據的最大長度不超過5000字節,指定類型為varbinary(5000); 而varbinary(MAX)是Microsoft SQL Server 2005能提供的最大存儲空間,大概 2 GB.
將二進制數據存儲在數據庫的主要優點是:將數據庫記錄和二進制數據關聯起來。它極大的簡化了數據庫的管理,比如將數據庫轉移到另一個網站或服務器。同樣,當刪除一條記錄時,同時自動的刪除了相關的二進制數據。
注意:在Microsoft SQL Server 2000及更早本版,varbinary類型最大支持8000字節,要支持2GB的二進制數據,就要使用image類型了。在SQL Server 2005裡引入MAX後,image類型已經開始被淡化了。盡管向後兼容,微軟宣稱將在SQL Server的後續版本中拋棄image類型。
如果你使用的是較早的數據類型,你可能看見過image類型。在數據庫Northwind裡的表Categories有一個Picture列,可用來存儲某個類的二進制圖像文件。由於數據庫Northwind起源於Microsoft Access以及SQL Server早期版本,所以Picture列的類型為image。
本章及接下來的3章,我們2種方法都要用。表Categories已經有列Picture來存儲類的二進制圖片文件,我們還要一個額外的列BrochurePath來存儲文件系統裡PDF的路徑。
第3步:為表Categories添加BrochurePath列
目前,表Categories只包含了4個列:CategoryID, CategoryName, Description以及 Picture。除此以外,我們還需要添加一列,指向該類的小冊子(如果存在的話)。打開服務器資源管理器,點擊表節點,右鍵點擊表Categories,選擇“打開表定義”(見圖5)。如果看不到服務器資源管理器,在視圖菜單裡選擇它,或按Ctrl+Alt+S.
在表Categories裡添加一個名為BrochurePath的列,類型為varchar(200),允許其值為NULL。點擊保存按鈕(或按Ctrl+S)。
圖5:表Categories裡添加BrochurePath的列
第4步:更新體系構架以使用Picture 和 BrochurePath列
當前,數據訪問層(Data Access Layer)裡的CategoriesDataTable定義了4個DataColumns:CategoryID, CategoryName, Description以及NumberOfProducts.我們最初在教程《創建一個數據訪問層》裡創建CategoriesDataTable時,其只包含了前3個列,而NumberOfProducts列是在第35章《使用Repeater和DataList單頁面實現主/從報表》裡創建的。
就像在教材《創建一個數據訪問層》裡討論的那樣,DataTables以類型DataSet的形式構成了業務對象。TableAdapters的作用在於連接數據庫,並以查詢結果組成
業務對象。CategoriesDataTable是由CategoriesTableAdapter構成的,它包含三種數據訪問方法:
GetCategories():執行該TableAdapter的主要查詢,返回記錄的CategoryID, CategoryName和Description這3項。自動生成的Insert和Update方法主要就是用的該方法。
GetCategoryByCategoryID(categoryID):也是返回記錄的CategoryID, CategoryName和Description這3項,前提是CategoryID值要匹配。
GetCategoriesAndNumberOfProducts():也是返回記錄的CategoryID, CategoryName和Description這3項,同時返回每種類別產品的數量。
我們注意到,這些方法沒有返回表Categories的Picture或BrochurePath;CategoriesDataTable裡也沒有展示這些列。為了能夠使用到Picture和BrochurePath,我們需要先在CategoriesDataTable裡添加它們,再更新類CategoriesTableAdapter,使其返回它們的值。
添加Picture和BrochurePath列
首先將它們添加到CategoriesDataTable.在CategoriesDataTable的頂部點右鍵,從菜單裡選“添加”,再選“列”選項。這將為DataTable 創建一個名為Column1的數據列(DataColumn),將其重命名為Picture。在屬性窗口裡將其DataType屬性設置為“System.Byte[]”(在下拉列表裡沒該選擇,故需手動輸入)
圖6:創建一個名為Picture的數據列,設數據類型為System.Byte[]
添加另一個名為BrochurePath的數據列,使用默認的數據類型(System.String)
從TableAdapter返回值Picture和BrochurePath
在CategoriesDataTable裡添加了上述2列後,我們准備更新CategoriesTableAdapter。我們可以在TableAdapter的主查詢裡返回這2個值,但每次調用GetCategories()都會返回二進制數據。怎麼才好呢?我們可以讓主查詢返回BrochurePath值,而另外創建一個方法來返回某個特定種類的Picture值。
為了更新主查詢,在CategoriesTableAdapter的頂部右鍵點擊,選“配置”項,這將啟動Table Adapter設置向導,更新查詢使其返回BrochurePath,點完成。
圖7:更新SELECT命令,返回BrochurePath
使用ad-hoc SQL語句更新TableAdapter裡的主查詢時,它將更新TableAdapter裡所有方法的查詢涉及的列。也即,更新GetCategoryByCategoryID(categoryID) 方法,使其返回BrochurePath值;更新GetCategoriesAndNumberOfProducts()方法,並刪除返回每種類別有幾個產品的查詢(也即刪除NumberOfProducts)因此,我們右鍵點擊GetCategoriesAndNumberOfProducts(),選擇“配置”,使其恢復為原來的查詢語句:
SELECT CategoryID, CategoryName, Description, (SELECT COUNT(*) FROM Products p WHERE p.CategoryID = c.CategoryID) as NumberOfProducts FROM Categories c
然後,創建一個返回某個特定種類Picture值的TableAdapter方法。在CategoriesTableAdapter的頂部點擊右鍵,選擇“添加查詢”,這將打開TableAdapter的查詢設置向導。首先選擇如何訪問數據庫,有3種方式:使用SQL語句、創建一個新的存儲過程、使用一個已有的存儲過程。我們選擇“使用SQL語句”,點下一步,在界面裡選“SELECT(返回行)(S)”。
圖8:選擇使用SQL語句
圖9:由於查詢要返回一條記錄,我們選“選擇行”。
點下一步,鍵入如下SQL查詢,點Next:
SELECT CategoryID, CategoryName, Description, BrochurePath, Picture FROM Categories WHERE CategoryID = @CategoryID
最後,為新方法命名。為填充DataTable的方法命名為FillCategoryWithBinaryDataByCategoryID;為返回DataTable的方法命名為GetCategoryWithBinaryDataByCategoryID。點Finish完成設置。
圖10:為TableAdapter的方法命名
注意:完成設置後,我們將看到一個對話框,提示你“新命令文本所返回數據的架構與主查詢的構架不同。如果不期望出現此情況,請檢查查詢的命令文本。”。簡而言之,向導意識到主查詢—GetCategories() ,和我們剛剛創建的方法返回的數據不同。這正是我們希望的結果,不用理會它。
同時,謹記,如果你在向導裡選用ad-hoc SQL語句,然後再改變TableAdapter的主查詢,它將改變GetCategoryWithBinaryDataByCategoryID 方法裡SELECT命令所涉及到的列,以包含主查詢使用的那些列(也就是從查詢裡移除Picture列)。我們必須手動更新代碼,使其返回Picture ,和我們在前面處理GetCategoriesAndNumberOfProducts() 方法類似。
當完成在CategoriesDataTable添加2個數據列,以及對CategoriesTableAdapter添加GetCategoryWithBinaryDataByCategoryID方法後,這些類在數據集設計器裡看起來應和圖11差不多。
圖11:DataSet Designer包含了新列和新方法
更新業務邏輯層(BLL)
更新數據訪問層後,接下來就要擴展業務邏輯層以應對新添加的CategoriesTableAdapter方法,在類CategoriesTableAdapter添加如下代碼:
[System.ComponentModel.DataObjectMethodAttribute (System.ComponentModel.DataObjectMethodType.Select, false)] public Northwind.CategoriesDataTable GetCategoryWithBinaryDataByCategoryID(int categoryID) { return Adapter.GetCategoryWithBinaryDataByCategoryID(categoryID); }
第5步:從客戶端上傳一個文件到服務器
通常情況下,用戶將本地計算機上的文件上傳到服務器,這就涉及到以何種形式來處理數據,是直接將它存儲在數據庫?還是將它存儲在文件系統,然後在數據庫存儲其文件路徑?在這裡,我們探討如何將文件上傳到服務器,在後面的教程我們專注於上傳文件的數據模式。
ASP.NET 2.0新增加的FileUpload Web控件提供了一種機制,供用戶上傳文件。它是一種type屬性為“file”的<input>元件。在浏覽器裡看起來就像是一個帶Browse按鈕的文本框。當點擊Browse按鈕後,便彈出一個對話框供用戶選擇文件。選定,回傳後,被選文件的內容一起被傳遞會服務器。在服務器端,我們可以通過FileUpload控件的屬性獲取上傳文件的信息。
為驗證上傳文件,打開BinaryData文件夾的FileUpload.aspx頁面並切換到設計模式。從工具箱拖一個FileUpload控件到頁面,設其ID為UploadTest。再添加一個Button控件,設其ID為UploadButton,Text屬性為“Upload Selected File”。最後,在Button控件下添加一個Label控件,設其ID為UploadDetails並清空Text屬性。
圖12:在ASP.NET頁面上添加一個FileUpload控件
圖13為在浏覽器查看該頁面的情形,我們注意到,當點擊Browse按鈕時,彈出一個對話框,供用戶選擇要上傳的文件。當選定一個文件,再點“Upload Selected File”按鈕時,頁面回傳,將文件的二進制數據傳到服務器。
圖13:用戶選擇上傳到服務器的文件
頁面回傳後,上傳的文件可以存儲於文件系統,也能以“流”(Stream)的形式被調用。本例,我們將其存儲在文件夾~/Brochures 裡。首先在根目錄創建一個Brochures文件夾,然後為UploadButton的Click事件創建一個事件處理器:
protected void UploadButton_Click(object sender, EventArgs e) { if (UploadTest.HasFile == false) { // No file uploaded! UploadDetails.Text = "Please first select a file to upload..."; } else { // Display the uploaded file's details UploadDetails.Text = string.Format( @"Uploaded file: {0}<br /> File size (in bytes): {1:N0}<br /> Content-type: {2}", UploadTest.FileName, UploadTest.FileBytes.Length, UploadTest.PostedFile.ContentType); // Save the file string filePath = Server.MapPath("~/Brochures/" + UploadTest.FileName); UploadTest.SaveAs(filePath); } }
FileUpload控件提供了多種處理上傳數據的屬性。比如,HasFile屬性指出用戶是否上傳了文件;FileBytes屬性以字節的形式訪問數據。Click事件處理器事先確定是否上傳文件,如果是,Label控件列出文件的名稱、字節大小和類型。
注意:為確保用戶上傳文件,可以使用RequiredFieldValidator控件或檢查HasFile屬性,若其為false,則提出警告信息。
FileUpload控件的SaveAs(filePath)屬性將文件存儲在一個指定的文件路徑(filePath)。路徑必須是一個物理路徑(如C:/Websites/Brochures/SomeFile.pdf)),而不是相對路徑(如:/Brochures/SomeFile.pdf). 方法Server.MapPath(virtPath)截取的是相對路徑,返回的是物理路徑。這裡,相對路徑是~/Brochures/fileName, 其fileName就是上傳文件的名字。
完成Click事件處理器後,花幾分鐘在浏覽器查看該頁,點Browse按鈕,從硬盤選擇一個文件供上傳。再點“Upload Selected File”按鈕,頁面回傳,將文件上傳到 ~/Brochures文件夾裡。上傳文件後,返回Visual Studio,在解決資源管理器裡點“刷新”按鈕,你將會在~/Brochures文件夾裡看到剛剛上傳的文件。
圖14:文件已經上傳到服務器
圖15:上傳文件保存在~/Brochures文件夾
將上傳文件保存在文件系統
當將上傳文件保存到文件系統時,有幾個要點需要注意。首先是安全問題,要將文件保存到文件系統,運行的頁面必須要有寫入權限。如果你使用Microsoft's Internet Information Services (IIS) 當服務器,其安全性取決於IIS的版本和設置。
另一個挑戰是對上傳文件的命名問題。一般來說,頁面將文件保存在~/Brochures 文件夾時,直接使用其在用戶電腦時的原名。比如,用戶A上傳一個名為Brochure.pdf的文件,上傳後保存為~/Brochure/Brochure.pdf,假如稍後用戶B也要上傳文件,且名稱也剛好為Brochure.pdf,會發生什麼情況呢?就我們目前的代碼而言,用戶B的文件將覆蓋用戶A的文件。
有幾種處理命名沖突的方法。一種是當存儲同名文件時,阻止上傳文件。比如當用戶B試圖上傳一個名為Brochure.pdf的文件時,提示其改名並重試。另一個方法是使用一種唯一標識的文件名,可以是globally unique identifier (GUID),或對應數據庫記錄的主鍵值。在下一節教程我們將詳細探討。
處理大容量二進制數據的面臨的問題
教程假設處理的二進制數據是適度大小的。處理大數據量的二進制數據時—比如幾MB,甚至更大,就超出本教程的范圍了。默認情況下, ASP.NET 最多處理4MB的數據,雖然可以在Web.config文件的<httpRuntime>元素裡設置能處理的最大值,但IIS同樣對上傳文件的大小做了限制,見文章《IIS Upload File Size》。
另外,上傳大數據量文件花費的時間很可能超過ASP.NET默認等待的110秒,還要面對處理大數據的內存、執行效率問題。
FileUpload控件不太適合上傳大的文件。當上傳大文件時,用戶需要耐心的等待,且無法確定上傳是否在進行。上傳小文件時問題不大,也許只要幾秒鐘;而大文件可能要幾分鐘。有很多第三方的文件上傳控件更適合上傳大文件且提供進度提示,提供更好的用戶體驗。
如果你的應用程序需要處理大文件,你要認真考慮面臨的挑戰,尋找適合自己的解決方案。
總結:
創建一個處理二進制數據的應用程序面臨一些問題。本教程探討了其中的2個問題:如何存儲數據以及允許用戶通過頁面上傳文件。接下來的教程我們探討如何處理數據庫記錄中的二進制數據,以及如何顯示它。
祝編程快樂!
作者簡介
本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創始人,自1998年以來一直應用 微軟Web技術。大家可以點擊查看全部教程《[翻譯]Scott Mitchell 的ASP.NET 2.0數據教程》,希望對大家的學習ASP.NET有所幫助。