[Architecture] Repository實作查詢功能

摘要:[Architecture Pattern] Repository實作查詢功能

[Architecture Pattern] Repository實作查詢功能

範例下載

範例程式碼:點此下載

問題情景

在系統的BLL與DAL之間,加入Repository Pattern的設計,能夠切割BLL與DAL之間的相依性,並且提供系統抽換DAL的能力。但在軟體開發的過程中,套用Repository Pattern最容易遇到的問題就是,如何在Repository中實作「查詢」這個功能。像是在下列這個查詢訂單的頁面,系統必須要依照使用者輸入的查詢條件,來從DAL中查詢出所有符合條件內容的Order物件集合,並且將這些Order物件逐一呈現給在系統頁面給使用者瀏覽。

問題情景01

因為系統頁面上的查詢條件是可填、可不填,這對應到提供資料的Repository上,就變成Repository必須依照各種查詢條件填或不填的各種組合,來提供對應的Method。但這樣的查詢功能設計,在查詢條件少的情景能夠正常的開發設計;但在查詢條件較多的情景,就會發現為每種查詢條件組合建立對應的Method,近乎是一項不可能的任務。(EX:每個條件內容可填可不填,3個查詢條件就需要2^3=8個Method、7個查詢條件就需要2^7=128個Method。)

public interface IOrderRepository
{
    // Methods
    IEnumerable<Order> GetAllByCondition(string userId, OrderState state, DateTime startDate, DateTime endDate);

    IEnumerable<Order> GetAllByCondition(OrderState state, DateTime startDate, DateTime endDate);

    IEnumerable<Order> GetAllByCondition(string userId, DateTime startDate, DateTime endDate);

    IEnumerable<Order> GetAllByCondition(DateTime startDate, DateTime endDate);

    IEnumerable<Order> GetAllByCondition(string userId, OrderState state);

    IEnumerable<Order> GetAllByCondition(OrderState state);

    IEnumerable<Order> GetAllByCondition(string userId);

    IEnumerable<Order> GetAllByCondition();
}

這時最直覺的做法,會在Repository上加入GetAllBySql這個Method,讓系統依照使用者輸入的查詢條件來組合SQL指令,再交由實作Repository的DAL去資料庫做查詢。

public interface IOrderRepository
{
    // Methods
    IEnumerable<Order> GetBySql(string sqlCommand, params object[] parameters);
}

Repository加入GetAllBySql的這個設計,的確可以滿足使用者需求、提供正確資訊給使用者。但仔細思考Repository加入GetAllBySql的這個設計,是讓DAL的職責汙染到了BLL,BLL必須要知道DAL所使用的資料表名稱、資料庫欄位才能組合出SQL指令,也就是在程式碼中隱性的讓BLL相依於DAL,這大幅降低了BLL的內聚力。而一般來說只有關聯式資料庫能夠剖析SQL指令來提供資料,也就是DAL實作被綁死在關聯式資料庫上,這也就大大降低了BLL的重用性。

接著,以下列這個開發情景:「系統的資料來源,需要依照網路連線狀態來決定使用本地資料庫還是使用外部API」,來思考Repository加入GetAllBySql的這個設計。當外部API不支援SQL指令查詢,系統就無法建立外部API的GetAllBySql實作,這也就限制了BLL抽換DAL成為外部API的能力。(感謝91提供範例~^^)

問題情景02

解決方案

IRepository設計

為了解決Repository實作查詢功能的問題,回過頭思考一般函式庫、Web服務提供查詢功能的方式。會發現很多查詢功能的設計,會在查詢功能中提供所有的查詢條件,在這些條件內容中填null代表忽略這個條件、填值代表加入這個條件。

遵循這個設計原則,開發人員可以為Repository上加入GetAllByCondition這個Method,接著把每個查詢條件都設計為這個Method的函式參數;最後替不可為null的數值型別參數(enum、DateTime...)加上「?」關鍵字,將這些數值型別改為可輸入null的Nullable類別。

public interface IOrderRepository
{
    // Methods
    IEnumerable<Order> GetAllByCondition(string userId, OrderState? state, DateTime? startDate, DateTime? endDate);
}

IRepository使用

完成GetAllByCondition的設計之後,系統就可以將使用者在表單中所輸入的查詢條件,對應到GetAllByCondition的每個函式參數。(表單中條件內容有填的對應為函式參數內容、表單中條件內容沒填的對應為函式參數null。)

// UserId
string userId = null;
if(string.IsNullOrEmpty(this.UserIdTextBox.Text) == false) 
{
    userId = this.UserIdTextBox.Text.Trim();
}

// State
OrderState? state = null;
if (this.StateComboBox.SelectedValue != null)
{
    if (this.StateComboBox.SelectedValue.ToString() != "All")
    {
        state = Enum.Parse(typeof(OrderState), this.StateComboBox.SelectedValue.ToString()) as OrderState?;
    }
}

// StartDate
DateTime? startDate = null;
if(string.IsNullOrEmpty(this.StartDateTextBox.Text) == false)
{
    startDate = DateTime.Parse(this.StartDateTextBox.Text) as DateTime?;
}

// EndDate
DateTime? endDate = null;
if (string.IsNullOrEmpty(this.EndDateTextBox.Text) == false)
{
    endDate = DateTime.Parse(this.EndDateTextBox.Text) as DateTime?;
}

// Query
var orderCollection = _orderRepository.GetAllByCondition(userId, state, startDate, endDate);

// Display
this.OrderGridView.DataSource = orderCollection;
  • 執行範例(All)

    解決方案01

    解決方案02

  • 執行範例(userId=A123)

    解決方案03

    解決方案04

  • 執行範例(userId=A123, state=Completed)

    解決方案05

    解決方案06

SqlRepository實作

接著設計封裝本地資料庫的Repository實作,GetAllByCondition函式就可以依照這些函式參數是否為null、不為null的參數內容,來組合查詢條件的SQL指令、提交給本地資料庫並且回傳查詢結果。

  • 依照條件內容是否為null,來組合SQL指令的Where條件。

    // CommandText
    command.CommandText = @"SELECT USER_ID, STATE, DATE FROM Orders";
    
    // ConditionText
    var conditionList = new List<string>();
    if (string.IsNullOrEmpty(userId) == false) conditionList.Add("USER_ID = @USER_ID");
    if (state.HasValue == true) conditionList.Add("STATE = @STATE");
    if (startDate.HasValue == true && endDate.HasValue == true) conditionList.Add("Date >= @START_DATE");
    if (startDate.HasValue == true && endDate.HasValue == true) conditionList.Add("Date <= @END_DATE");
    var conditionString = string.Join(" AND ", conditionList);
    if (string.IsNullOrEmpty(conditionString) == false) command.CommandText += " WHERE " + conditionString;
    
  • 依照條件內容是否為null,來加入Command.Parameters。

    // CommandParameters
    if (string.IsNullOrEmpty(userId) == false) command.Parameters.Add(new SqlParameter("@USER_ID", userId));
    if (state.HasValue == true) command.Parameters.Add(new SqlParameter("@STATE", state.ToString()));
    if (startDate.HasValue == true && endDate.HasValue == true) command.Parameters.Add(new SqlParameter("@START_DATE", startDate.Value));
    if (startDate.HasValue == true && endDate.HasValue == true) command.Parameters.Add(new SqlParameter("@END_DATE", endDate.Value));
    
  • 執行範例(All)

    解決方案07

    解決方案08

  • 執行範例(userId=A123)

    解決方案09

    解決方案10

  • 執行範例(userId=A123, state=Completed)

    解決方案11

    解決方案12

IRepository查詢

完成上列這些步驟之後,也就完成了Repository實作查詢功能的開發工作,使用者就能在系統頁面上填寫查詢條件,來從系統中查詢所有符合條件內容的資料物件集合。

  • 執行範例(All)

    解決方案13

  • 執行範例(userId=A123)

    解決方案14

  • 執行範例(userId=A123, state=Completed)

    解決方案15

後記

Repository實作查詢功能的開發工作套用本篇的解決方案,能在BLL中完全不需要牽扯DAL的資訊,只需要單純傳遞C#類別來做為查詢條件,這部分提高了BLL的內聚力。而GetAllByCondition的設計,因為單純使用C#類別來傳遞查詢條件,這讓DAL實作不會被綁死在特定資料來源上,也大幅提高了BLL的重用性。開發人員設計系統時遇到需要Repository實作查詢功能的開發工作,參考本篇提供的解決方案應該就能滿足大部分的開發需求。

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。