ClownFish:比寫程式碼還快的通用資料訪問層

wh7577發表於2021-09-09

閱讀目錄

  • 開始

  • ClownFish是什麼?

  • 比手寫程式碼還快的執行速度

  • 簡單,一個呼叫完成你要的全部功能

  • 方便,你需要的程式碼已經準備好了

  • 定義資料實體型別不再是費力的體力勞動

  • 通用,可以非常簡單地實現對多種資料庫的支援

  • 靈活,SQL語句放在哪裡隨便你

  • XmlCommand是什麼?

  • 可監控,圖形的工具會告訴你每個資料訪問的細節

  • 關於示例程式碼

最近花了二個月的業餘時間重寫了我以前的通用資料訪問層,由於是重寫,所以我給這個專案取了個新名字:ClownFish



ClownFish是什麼?

ClownFish 是我編寫的一個通用資料訪問層,設計它的目的是為了:
1. 方便在 .net 專案中執行資料訪問任務。
2. 避免直接使用ADO.NET帶來的一大堆高度類似的繁瑣程式碼。
3. 提供出色的效能滿足實際專案需要。

ClownFish 具有以下一些技術特色:
1. 高效能:比手寫程式碼還快的執行速度。
2. 簡單:執行查詢、將查詢結果轉成實體列表、獲取輸出引數。 一個呼叫完成三個步驟。
3. 方便:提供專用的程式碼生成器,直接生成呼叫程式碼或者實體型別定義程式碼。
4. 通用:可以非常簡單地實現對多種資料庫的支援。
5. 靈活:支援儲存過程,引數化SQL,或者將SQL語句儲存在XML配置檔案中。
6. 可監控:提供一個Profiler工具,讓您可以隨時瞭解詳細的資料庫訪問情況。

ClownFish不僅繼承了老版本的通用資料訪問層的全部優點,而且在高效能,方便性,靈活性,以及程式碼可讀性方面有了更出色的設計。

在最新的版本中,ClownFish不僅僅只是一個通用資料訪問層,還提供了:專用的程式碼生成器,XmlCommand管理工具,Profiler工具,它們都會為ClownFish提供更多功能。

ClownFish 是一個可免費的資料訪問元件,您可以把它應用在您的 .net 專案中,讓它簡化您的開發工作。


比手寫程式碼還快的執行速度

提高效能是建立ClownFish專案的重要原因之一,這次最佳化的主要目標是:比手寫程式碼還快的執行速度。

為了讓您對ClownFish的效能留有較深刻的印象,下面我將透過一個實際案例來測試它的速度。

首先,我定義了一個實體型別:

public class OrderInfo
{
    public int OrderID { get; set; }
    public DateTime OrderDate { get; set; }
    public decimal SumMoney { get; set; }
    public string Comment { get; set; }
    public bool Finished { get; set; }
    public int ProductID { get; set; }
    public decimal UnitPrice { get; set; }
    public int Quantity { get; set; }
    public string ProductName { get; set; }
    public int CategoryID { get; set; }
    public string Unit { get; set; }
    public string Remark { get; set; }
    
    // 注意:客戶資訊有可能會是DBNull
    public int? CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string Address { get; set; }
    public string PostalCode { get; set; }
    public string Tel { get; set; }
}

定義這個實體型別,我關注的是它所包含的資料成員的數量,而不是那些資料成員的含義。
我認為這個型別的資料成員數量應該還是比較接近多數實際場景的。
下面再來看看如何從資料庫中載入它們:

public static class TestHelper
    {
        public static readonly string QueryText = @"
select top (@TopN) d.OrderID, d.OrderDate, d.SumMoney, d.Comment, d.Finished,
dt.ProductID, dt.UnitPrice, dt.Quantity, 
p.ProductName, p.CategoryID, p.Unit, p.Remark,
c.CustomerID, c.CustomerName, c.ContactName, c.Address, c.PostalCode, c.Tel
from Orders d 
inner join [Order Details] dt on d.OrderId = dt.OrderId
inner join Products p on dt.ProductId = p.ProductId
left join Customers c on d.CustomerId = c.CustomerId
";

        public static List ExecuteQuery(SqlConnection conn, UiParameters uiParam)
        {
            SqlCommand command = new SqlCommand(QueryText, conn);
            command.Parameters.Add("TopN", SqlDbType.Int).Value = uiParam.PageSize;

            List list = new List(uiParam.PageSize);

            using( SqlDataReader reader = command.ExecuteReader() ) {
                while( reader.Read() )
                    list.Add(LoadOrderInfo(reader));
            }

            return list;
        }
        
        private static OrderInfo LoadOrderInfo(SqlDataReader reader)
        {
            OrderInfo info = new OrderInfo();
            info.OrderID = (int)reader["OrderID"];
            info.OrderDate = (DateTime)reader["OrderDate"];
            info.SumMoney = (decimal)reader["SumMoney"];
            info.Comment = (string)reader["Comment"];
            info.Finished = (bool)reader["Finished"];
            info.ProductID = (int)reader["ProductID"];
            info.UnitPrice = (decimal)reader["UnitPrice"];
            info.Quantity = (int)reader["Quantity"];
            info.ProductName = (string)reader["ProductName"];
            info.CategoryID = (int)reader["CategoryID"];
            info.Unit = (string)reader["Unit"];
            info.Remark = (string)reader["Remark"];

            object customerId = reader["CustomerID"];
            if( customerId != DBNull.Value ) {
                info.CustomerID = (int)customerId;
                info.CustomerName = (string)reader["CustomerName"];
                info.ContactName = (string)reader["ContactName"];
                info.Address = (string)reader["Address"];
                info.PostalCode = (string)reader["PostalCode"];
                info.Tel = (string)reader["Tel"];
            }
            return info;
        }

程式碼中,首先定義了一個引數化的SQL查詢語句,然後就是純手工的ADO.NET程式碼,迴圈呼叫SqlDataReader.Read()方法,再從SqlDataReader中根據欄位名稱獲取資料寫入到實體物件的屬性中。

這段程式碼夠【手工化】吧? 由於這段程式碼沒用使用反射程式碼,所以如果想追求效能,我想大家都會這樣寫。

在我的測試程式中,上面那段程式碼被下面這段程式碼呼叫:

public interface IPerformanceTest : IDisposable
{
    List Run();
}


[TestMethod("手工程式碼,SQLSERVER", 1)]
public class Test_Adonet_ShareConnection : IPerformanceTest
{
    private UiParameters uiParam;
    private SqlConnection conn;

    public Test_Adonet_ShareConnection(UiParameters param)
    {
        this.uiParam = param;
        this.conn = new SqlConnection(Program.ConnectionString);
        this.conn.Open();
    }

    public List Run()
    {
        return TestHelper.ExecuteQuery(conn, uiParam);
    }

    public void Dispose()
    {
        conn.Dispose();
    }
}

測試程式碼應該沒有問題吧?


再來看一下使用ClownFish的等效測試程式碼:

[TestMethod("ClownFish,SQLSERVER", 2)]
public class Test_ClownFish_ShareConnection : IPerformanceTest
{
    private UiParameters uiParam;
    private ClownFish.DbContext db;

    public Test_ClownFish_ShareConnection(UiParameters param)
    {
        this.uiParam = param;
        this.db = new ClownFish.DbContext(false);
    }

    public List Run()
    {
        var parameter = new { TopN = uiParam.PageSize };
        return ClownFish.DbHelper.FillList(
                    TestHelper.QueryText, parameter, db, ClownFish.CommandKind.SqlTextWithParams);
    }

    public void Dispose()
    {
        db.Dispose();
    }
}

比較這二段程式碼,不難看出:使用 ClownFish 所需的程式碼量要 少很多

如果我以下面的測試引數執行效能測試:

圖片描述

可以得到下面的測試結果:

圖片描述

圖形反映的結果很直觀:ClownFish 完成測試所需時間比手寫程式碼要略快點。

或許有些人認為:快這麼一點,意義不大!
但您想過沒有:ClownFish PK的物件是【手工版的專用程式碼】啊!

其實這個測試並沒有把 ClownFish 效能很好的體現出來,因為中間有SQLSERVER的執行時間,以及跨程式的呼叫開銷。這二個因素所花的成本影響了ClownFish的效能優勢。

下面,我又做了一組測試:直接從DataTable中載入資料。
我之所以選擇這個測試方法,是因為它也是一種常見的使用方案:
我們可以將原始匯出到XML檔案中,然後使用XML檔案做離線資料,這樣可以減少對資料庫的訪問壓力。
老版本的通用資料訪問層也一直支援這個功能,所以這次就選擇了這個測試方法。
說明:實際使用時,我會從XML讀出資料到DataTable,供後面使用(轉成實體型別只是其中的一種使用資料的方式)。

在測試之前,我們來看一下手工程式碼是什麼樣的:

public static class TestHelper
{
    // 省略前面貼過的程式碼

    public static List LoadFromDataTable(DataTable table)
    {
        List list = new List(table.Rows.Count);
        foreach(DataRow dataRow in table.Rows)
            list.Add(LoadOrderInfo(dataRow));
        
        return list;
    }

    private static OrderInfo LoadOrderInfo(DataRow dataRow)
    {
        OrderInfo info = new OrderInfo();
        info.OrderID = (int)dataRow["OrderID"];
        info.OrderDate = (DateTime)dataRow["OrderDate"];
        info.SumMoney = (decimal)dataRow["SumMoney"];
        info.Comment = (string)dataRow["Comment"];
        info.Finished = (bool)dataRow["Finished"];
        info.ProductID = (int)dataRow["ProductID"];
        info.UnitPrice = (decimal)dataRow["UnitPrice"];
        info.Quantity = (int)dataRow["Quantity"];
        info.ProductName = (string)dataRow["ProductName"];
        info.CategoryID = (int)dataRow["CategoryID"];
        info.Unit = (string)dataRow["Unit"];
        info.Remark = (string)dataRow["Remark"];

        object customerId = dataRow["CustomerID"];
        if( customerId != DBNull.Value ) {
            info.CustomerID = (int)customerId;
            info.CustomerName = (string)dataRow["CustomerName"];
            info.ContactName = (string)dataRow["ContactName"];
            info.Address = (string)dataRow["Address"];
            info.PostalCode = (string)dataRow["PostalCode"];
            info.Tel = (string)dataRow["Tel"];
        }
        return info;
    }


    private static DataTable s_OrderInfoTable;

    public static DataTable GetOrderInfoTable()
    {
        // 把結果用靜態變數快取起來,避免影響測試時間
        // 由於在執行測試前,會有一次單獨的呼叫,所以並沒有執行緒安全問題。

        if( s_OrderInfoTable == null ) {
            s_OrderInfoTable = ClownFish.DbHelper.FillDataTable(
                        TestHelper.QueryText, new { TopN = 50 }, ClownFish.CommandKind.SqlTextWithParams);
        }

        return s_OrderInfoTable;
    }
}

看到這個版本的LoadOrderInfo方法,您會有什麼感覺?是不是很無奈?
沒辦法,ADO.NET就是這樣設計的。寫這樣的程式碼會讓人心煩(這是我的感受)!

測試呼叫程式碼:

[TestMethod("手工程式碼,DataTable", 5)]
public class Test_Adonet_LoadDataTable : IPerformanceTest
{
    public Test_Adonet_LoadDataTable(UiParameters param) { }
    public void Dispose() { }

    public List Run()
    {
        return TestHelper.LoadFromDataTable(TestHelper.GetOrderInfoTable());
    }        
}

測試程式碼應該沒有問題吧?


再來看一下使用ClownFish的等效測試程式碼:

[TestMethod("ClownFish,DataTable", 6)]
public class Test_ClownFish_LoadDataTable : IPerformanceTest
{
    public Test_ClownFish_LoadDataTable(UiParameters param) { }
    public void Dispose() { }

    public List Run()
    {
        return ClownFish.DbHelper.FillListFromTable(TestHelper.GetOrderInfoTable());
    }        
}

使用 ClownFish 的程式碼仍然要短很多!

下面繼續使用前面的測試引數來執行測試程式,得到以下測試結果:

圖片描述

Excel圖形直觀反映出 ClownFish 的速度要 比手工程式碼 快一倍 還不止。

看完這二個測試,ClownFish 有沒有給您留下二個印象?
1. 程式碼量很少。
2. 效能很好。


簡單,一個呼叫完成你要的全部功能

呼叫下面這個儲存過程,您需要多少行C#程式碼?

create procedure InsertProduct( 
    @ProductName nvarchar(50), 
    @CategoryID int, 
    @Unit nvarchar(10), 
    @UnitPrice money, 
    @Quantity int, 
    @Remark nvarchar(max),
    @ProductID int output
) 
as
begin

insert into Products (ProductName, CategoryID, Unit, UnitPrice, Quantity, Remark) 
values( @ProductName, @CategoryID, @Unit, @UnitPrice, @Quantity, @Remark);

set @ProductID = scope_identity();

end

在這裡,我不想再想寫那種手工版本的ADO.NET程式碼,我懶得寫。

來看一下我從示例程式碼中摘選出來的呼叫程式碼吧:

Product product = CreateTestProduct();

// 插入一條新記錄
DbHelper.ExecuteNonQuery("InsertProduct", product);
sb.AppendFormat("InsertProduct OK, ProductId is : {0}rn", product.ProductID);

看到了吧,其實只有一行程式碼(中間那行)。

再來看一個有分頁的吧:

create procedure GetProductByCategoryId(
    @CategoryID int,
    @PageIndex int = 0,
    @PageSize int = 20,
    @TotalRecords int output
)
as
begin
   
declare @ResultTable table
(
    RowIndex int,
    ProductID int,
    ProductName nvarchar(50),
    CategoryID int,
    Unit nvarchar(10),
    UnitPrice money,
    Quantity int
);
   
insert into @ResultTable
select row_number() over (order by ProductID asc) as RowIndex,
       p.ProductID, p.ProductName, p.CategoryID, p.Unit, p.UnitPrice, p.Quantity
from   Products as p
where CategoryID = @CategoryID;
     
select  @TotalRecords = count(*) from  @ResultTable;
   
select *
from   @ResultTable
where  RowIndex > (@PageSize * @PageIndex) and RowIndex 

相應的呼叫程式碼:

// 查詢一個分頁列表
var parameters = new GetProductByCategoryIdParameters {
    CategoryID = 1,
    PageIndex = 0,
    PageSize = 3,
    TotalRecords = 0
};
List list = DbHelper.FillList("GetProductByCategoryId", parameters);

sb.AppendFormat("存在 {0} 條符合條件的記錄。條件:CategoryID = {1}rn", 
                        parameters.TotalRecords, parameters.CategoryID);

還是 一個呼叫 就能完成全部的資料庫訪問工作。

老版本的通用資料訪問層有這樣一個設計目標:
呼叫儲存過程,不管輸入引數多麼複雜,不管有多少輸出引數,包含轉換結果集到實體列表,只需要一行C#程式碼。

這一優點在ClownFish中得到了繼承:簡單,一個呼叫完成你要的全部功能。


方便,你需要的程式碼已經準備好了

看到前面的那段演示程式碼,你會不會想:我還要定義一個GetProductByCategoryIdParameters型別啊,太麻煩了!

真是那樣嗎? 請看下面的截圖:

圖片描述

注意:ClownFish並非僅僅支援儲存過程,後面將要介紹的XmlCommand也有同樣好的支援。

在工具中,不僅可以直接檢視儲存過程程式碼,還能生成呼叫程式碼。
工具生成二個呼叫程式碼,你可以選擇其中的一個(因為我不知道你需要哪個)。

說明:呼叫GetProductByCategoryId需要定義一個型別是因為它包含了輸出引數,如果沒有輸出引數,工具會生成匿名型別:

圖片描述

接下來的事情我想大家都知道該怎麼做:把工具生成的程式碼COPY到專案中,修改一下呼叫引數就好了。


定義資料實體型別不再是費力的體力勞動

對於喜歡使用資料實體型別的人來說,手工定義這些型別是件費力且枯燥的體力勞動。
現在好了,ClownFish 的生成器可以減輕你的工作負擔:

圖片描述



還記得本文一開始的那個OrderInfo型別嗎,它是根據一個查詢語句生成的:

圖片描述

說明:根據查詢生成的型別,工具不知道如何命名,需要你在使用時改名。


ClownFish 不僅僅能生成資料實體型別,還能生成對應的增刪改命令及其對應的命令引數:

圖片描述

這裡涉及到另一個名詞:XmlCommand。它是什麼,我後面再說。
這個截圖是想告訴你:以後不必再為增刪改命令以及那些命令引數煩惱了。
執行圖片中那個選單操作,然後再到ClownFish的另一個輔助工具(XmlCommandTool)去貼上就可以了。


通用,可以非常簡單地實現對多種資料庫的支援

ClownFish已經從前輩版本中繼承了對多資料庫種類的支援。
本文的結尾處,我提供了可供下載的完整示例,其中就包含有:SQLSERVER, MySql, ACCESS的示例程式碼。以下是其中一個示例的截圖:

圖片描述

ClownFish 支援多種資料庫的原因在於:
ClownFish 在內部使用了DbConnection, DbCommand, DbTransaction這些抽象型別。
ClownFish 提供的API是不區分資料庫種類的,您在註冊連線時,指定什麼型別,就是什麼型別。

因此,如果您使用儲存過程,或者XmlCommand的話,完全可以做到:一份C#程式碼同時支援多種資料庫的。


靈活,SQL語句放在哪裡隨便你

對於資料庫的訪問方式目前有以下5種方案:
1. 有些人喜歡使用儲存過程。
2. 有些人不喜歡儲存過程也不喜歡把SQL語句放在C#程式碼中。
3. 有些人會在C#中嵌入引數化的SQL語句。
4. 有些人就是喜歡在C#程式碼中拼接SQL語句。
5. 還有些人不寫SQL語句而在使用ORM工具。
當然了,還有些人同時混合使用多種方案。
我不知道您屬於哪一類,如果是最後一類,那麼我只能說:ClownFish不適合你。
除此之外,ClownFish完全可以滿足您的需要。

對於前4類方式,ClownFish在內部定義一個列舉來表示:

/// 
/// 表示要執行什麼型別的命令
/// 
public enum CommandKind
{
    /// 
    /// 表示要執行一個儲存過程或者是一個XmlCommand
    /// 
    SpOrXml,
    /// 
    /// 表示要執行一個儲存過程
    /// 
    StoreProcedure,
    /// 
    /// 表示要執行一個XmlCommand
    /// 
    XmlCommand,
    /// 
    /// 表示要執行一條沒有引數的SQL語句
    /// 
    SqlTextNoParams,
    /// 
    /// 表示要執行一條包含引數的SQL語句
    /// 
    SqlTextWithParams
}

ClownFish提供了一個工具類:DbHelper,可以讓您方便地訪問資料庫,它的許多資料庫訪問方法可以指定一個CommandKind列舉表示要執行的命令型別:

public static class DbHelper
{
    public static int ExecuteNonQuery(
            string nameOrSql, object inputParams);
    
    public static int ExecuteNonQuery(
            string nameOrSql, object inputParams, CommandKind cmdKind);
    
    public static int ExecuteNonQuery(
            string nameOrSql, object inputParams, DbContext dbContext);
    
    public static int ExecuteNonQuery(
            string nameOrSql, object inputParams, DbContext dbContext, CommandKind cmdKind);
    
    // 其它的資料庫訪問方法就不一一列出了,可以查閱API文件。
}

DbHelper的所有資料庫訪問方法都提供與以上類似的4個過載方法。
那些方法中,第一個引數可以傳入一個SQL語句,或者一個儲存過程的名字,或者一個XmlCommand的名稱。
引數 cmdKind 的意義就是解釋第一個引數的含義。


XmlCommand是什麼?

前面已經多次提到了XmlCommand,這裡來解釋一下XmlCommand是什麼。

上一小節中的那些資料訪問方法中,有一類較為特殊:不使用儲存過程,也不把SQL語句混在C#程式碼中。
對於這種方案,就需要把SQL語句放在配置檔案中。

或許有些人知道:我是喜歡用儲存過程的。我之所以喜歡儲存過程,是因為儲存過程的程式碼與專案程式碼是獨立的,我可以單獨修改儲存過程。而且,我還喜歡引數化的查詢,反對使用拼接SQL語句。我認為我是將資料庫做為我的SQL語句儲存容器在使用,儲存過程只是這一種方式而已(因為它有引數管理功能)。

除了儲存過程之外,使用配置檔案也能完成同樣功能:1. SQL語句的儲存容器,2. 提供引數管理功能。

雖然ClownFish的前輩版本也可以支援這種方案,但是卻沒有定義一種配置檔案的存放格式,因此,需要自己設計配置檔案格式,以及提供自己的簡化包裝方法。對於這種方案來說,本身是沒有任何技術難度的,只是需要定義一種配置檔案的存放格式。現在,ClownFish已經正式提供這種支援,並提供了一個管理工具。

我把儲存在配置檔案中的SQL語句稱為 XmlCommand ,一個XML檔案中可以儲存多個XmlCommand,每個XmlCommand都有一個名稱以便在執行時區分。下面是一個XmlCommand的程式碼片段:

    
        
            
            
            
            
            
            
            
        
        
    

看到這段程式碼,您會不會想:維護它們是不是很麻煩?

其實不是的,ClownFish提供了一個工具,可以輕鬆地管理它們:

圖片描述

圖片描述

在這個對話方塊中,每個XmlCommand的屬性都提供了控制元件編輯功能,完全不需要手工維護那段XML文字。

雙擊某個XmlCommand節點,還可以檢視呼叫程式碼:

圖片描述

工具生成的呼叫程式碼會告訴你那個XmlCommnad有多少個引數,您根本不用記住它們。

還有一點:
XmlCommand的XML檔案可以是多個,可以按照不同的業務邏輯來組織它們,例如:

圖片描述

對於一個ASP.NET專案來說,ClownFish 還會監視這個已載入的目錄,如果目錄中的配置檔案有修改,會自動重新載入。


可監控,圖形的工具會告訴你每個資料訪問的細節

由於ClownFish是個資料訪問層,因此,它知道每次執行資料庫操作的所有資訊。所以,我提供了一個監控工具,用於檢視解詳細的資料庫訪問情況。

圖片描述

上圖反映了在執行MySql,Access的訪問情況。

圖片描述

上圖反映了事務的執行情況,以及某個語句執行失敗的情況。

根據工具視窗,我們可以直觀的檢視到每個連線中執行了多少次命令,它們是否屬於同一個事務,它們的型別是儲存過程,XmlCommand,SQL語句,全都一目瞭然。而且,還可以知道每個命令的執行引數是什麼:

圖片描述



或許有些人看了這個工具後,會認為它是多餘的,他總認為有個【SQL Server Profiler】,其它的東西都是沒有意義的。
我不知道他有沒有想過:用SQL Server Profiler去監視一個區域網的SQL Server例項,你還能分辨哪些操作是由你引發的嗎??

我的工具則不同,它是配合我的資料訪問層一起工作的,由我的資料訪問層告訴工具當前使用者觸發了什麼資料庫的操作。
因此在分析本機程式訪問資料庫時,可以很直觀的知道:
您的某個操作引發了什麼樣的資料庫呼叫,以及呼叫的各種細節引數。

更何況我的工具不止針對SQL Server有效,理論上,.net支援的,我的資料訪問層應該都支援。
前面也說了,它是配合我的資料訪問層一起工作的,
所以,只要是使用ClownFish哪怕是訪問Access資料庫,也能有這樣的監控效果。(前面有圖片證明)


關於示例程式碼

為了能讓更多的人選擇ClownFish,這次我為大家準備了大量的示例程式碼。

圖片描述

1. ClownFishDEMO是一個綜合示例,網站型專案。執行效果圖:

圖片描述

2. MultiAccountDEMO是一個多帳套的演示網站。演示了一套程式碼處理多帳套的功能。

3. PerformanceTestApp是前面介紹的效能測試專案,你也可以將其它的資料訪問層加入其中,一起做效能測試。

不僅僅是以上三個專案演示瞭如何使用ClownFish,我還為以前的 MyMVC框架 的示例程式增加了資料庫訪問功能(以前只支援XML檔案)。
現在,MyMVC框架 的演示程式也能透過ClownFish來訪問SQLSERVER。
可以修改 MyMVC框架 演示網站中的web.config來切換資料訪問方法:


    <!--DataSoureceKind 表示不同的資料訪問方式,目前支援三個可選的配置值(注意大小寫):
        1. XmlFile ,表示使用XML檔案中的資料,此選項為預設值。
        2. XmlCommand ,表示使用配置檔案中的SQL命令去訪問SQLSERVER
        3. StoreProcedure ,表示呼叫SQLSERVER中的儲存過程去訪問資料庫
        你可以修改下面的配置,並結合ClownFishSQLProfiler.exe工具去觀察它們的差別。        
    --&gt
    

考慮到有些人在配置SQLSERVER時,會遇到一些問題,MyMVC框架的演示程式仍然預設使用XML檔案,而不是SQLSERVER資料庫。

但是,由於ClownFish是一個資料訪問層,它的示例只能訪問資料庫了。
如果不知道如何配置示例,請參考我的部落格:如何在IIS6,7中部署ASP.NET網站

還有一件讓我憂慮的事情:示例中還演示了 MySql的訪問!
我建議:如果你平時不使用MySql,那麼就跳過示例中的MySql演示部分吧。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2480/viewspace-2808426/,如需轉載,請註明出處,否則將追究法律責任。

相關文章