2010-05-07 65 views
0

我正在使用F#訪問數據庫,我最初嘗試創建一個函數來創建更新查詢是有缺陷的。更多FP正確的方式來創建更新SQL查詢

let BuildUserUpdateQuery (oldUser:UserType) (newUser:UserType) = 
    let buf = new System.Text.StringBuilder("UPDATE users SET "); 
    if (oldUser.FirstName.Equals(newUser.FirstName) = false) then buf.Append("SET first_name='").Append(newUser.FirstName).Append("'") |> ignore 
    if (oldUser.LastName.Equals(newUser.LastName) = false) then buf.Append("SET last_name='").Append(newUser.LastName).Append("'") |> ignore 
    if (oldUser.UserName.Equals(newUser.UserName) = false) then buf.Append("SET username='").Append(newUser.UserName).Append("'") |> ignore 
    buf.Append(" WHERE id=").Append(newUser.Id).ToString() 

這並不正確放置任何更新部分之間的,後的第一個,例如:

UPDATE users SET first_name='Firstname', last_name='lastname' WHERE id=... 

我可以把一個可變變量來跟蹤時set的第一部分子句被追加,但這似乎是錯誤的。

我可以創建一個元組列表,其中每個元組都是oldtext,newtext,columnname,這樣我就可以遍歷列表並構建查詢,但似乎應該將StringBuilder傳遞給一個遞歸函數,返回一個boolean然後作爲參數傳遞給遞歸函數。

這似乎是最好的方法,還是有更好的?

UPDATE:

下面是我在用我的當前的解決方案,因爲我想讓它更廣義的,所以我只需要編寫一個抽象類,我的實體從獲得和他們能使用相同的功能。我選擇分離我如何執行此功能,以便我可以傳遞如何創建更新的SET部分,以便我可以測試不同的想法。

let BuildUserUpdateQuery3 (oldUser:UserType) (newUser:UserType) = 
    let properties = List.zip3 oldUser.ToSqlValuesList newUser.ToSqlValuesList oldUser.ToSqlColumnList 
    let init = false, new StringBuilder() 
    let anyChange, (formatted:StringBuilder) = 
     properties |> Seq.fold (fun (anyChange, sb) (oldVal, newVal, name) -> 
      match(oldVal=newVal) with 
      | true -> anyChange, sb 
      | _ -> 
       match(anyChange) with 
       | true -> true, sb.AppendFormat(",{0} = '{1}'", name, newVal) 
       | _ -> true, sb.AppendFormat("{0} = '{1}'", name, newVal)      
      ) init 
    formatted.ToString() 

let BuildUserUpdateQuery (oldUser:UserType) (newUser:UserType) (updatequery:UserType->UserType->String) = 
    let buf = StringBuilder("UPDATE users SET "); 
    buf.AppendFormat(" {0} WHERE id={1}", (updatequery oldUser newUser), newUser.Id) 

let UpdateUser conn (oldUser:UserType) (newUser:UserType) = 
    let query = BuildUserUpdateQuery oldUser newUser BuildUserUpdateQuery3 
    execNonQuery conn (query.ToString()) 

回答

4

這是您想到的元組解決方案嗎?

let BuildUserUpdateQuery (oldUser:UserType) (newUser:UserType) = 
    let buf = StringBuilder("UPDATE users set ") 
    let properties = 
     [(oldUser.FirstName, newUser.FirstName, "first_name") 
     (oldUser.LastName, newUser.LastName, "last_name") 
     (oldUser.UserName, newUser.UserName, "username")] 
     |> Seq.map (fun (oldV, newV, field) -> 
         if oldV <> newV 
          then sprintf "%s='%s'" field newV 
          else null) 
     |> Seq.filter (fun p -> p <> null) 
     |> Seq.toArray 
    if properties.Length = 0 
     then None 
     else 
      bprintf buf "%s" (String.Join(", ", properties)) 
      bprintf buf " where id=%d" newUser.Id 
      Some <| buf.ToString() 

我看不到遞歸解決方案如何能比這更簡單...

BTW,我會強烈建議使用適當的SQL參數,而不是僅僅串接值,你可能會變得脆弱注射攻擊...

+0

另外請注意,這將返回'string option'而不是'string',就像在原始函數中一樣,以覆蓋無需更新的情況。 – 2010-05-07 03:01:43

+0

如果使用Seq.fold而不是Seq.map,則可以累積結果,之後不需要過濾器,並且可以推遲Seq.toArray,直到最後一個else子句,然後僅測試properties.Any。 – 2010-05-07 06:47:32

+0

謝謝你的回答。我會將其更改爲使用準備好的語句,但實際上,一旦我得到更新語句,這就是一個細節。 – 2010-05-07 10:22:23

1

只是爲了完整性,這裏是一個版本,直接使用fold函數做同樣的事情。這可以非常優雅地完成,因爲StringBuilder的方法返回StringBuilder(它允許您使用C#鏈接它們)。這也可以很好地用於摺疊。

讓我們假設我們從解元組由毛名單:

let properties = 
    [ (oldUser.FirstName, newUser.FirstName, "first_name") 
    (oldUser.LastName, newUser.LastName, "last_name") 
    (oldUser.UserName, newUser.UserName, "username") ] 

現在,你可以寫下面的代碼(它也返回一個標誌,是否有任何改變):

let init = false, new StringBuilder() 
let anyChange, formatted = 
    properties |> Seq.fold (fun (anyChange, sb) (oldVal, newVal, name) -> 
     if (oldVal = newVal) anyChange, sb 
     else true, sb.AppendFormat("{0} = '{1}'", name, newVal)) init 

摺疊過程中保留的狀態爲bool * StringBuilder,我們從包含空字符串構建器和false的初始值開始。在每一步中,我們要麼返回原始狀態(如果值與之前的值相同),要麼返回true的新狀態和由AppendFormat返回的StringBuilder的新版本。

顯式使用遞歸也可以,但是當你可以使用一些內置的F#函數時,通常使用這種方法更容易。如果需要處理每個實體的嵌套實體,則可以使用Seq.collect函數和遞歸一起獲取需要使用fold處理的屬性列表。僞代碼可能是這樣的:

let rec processEntities list = 
    seq { for entity, name in List.zip list names do 
      yield (entity.OldValue, entity.NewValue, name) 
      yield! processEntities entity.Nested } 

然後,你可以簡單地調用processEntities返回實體的平面列表以及使用過程中的實體:

let rec processEntities list names = 
    // Pair matching entity with the name from the list of names 
    List.zip list names 
    |> List.collect (fun (entity, name) -> 
    // Current element containing old value, new value and property name 
    let current = (entity.OldValue, entity.NewValue, name) 
    // Recursively proces nested entitites 
    let nested = processEntities entity.Nested 
    current::nested) 

這可以使用序列表達式更文筆優美與第一種情況一樣,可以使用fold

+0

非常感謝。我需要仔細研究你做了什麼,我可以看到它會如何工作。您的評論似乎很有幫助 – 2010-05-07 13:00:06

1

我喜歡Mauricio's和Tomas的解決方案,但也許這更像您最初設想的?

let sqlFormat (value:'a) = //' 
    match box value with 
    | :? int | :? float -> value.ToString() 
    | _ -> sprintf "'%A'" value // this should actually use database specific escaping logic to make it safe 

let appendToQuery getProp (sqlName:string) (oldEntity,newEntity,statements) = 
    let newStatements = 
    if (getProp oldEntity <> getProp newEntity) then (sprintf "%s=%s" sqlName (sqlFormat (getProp newEntity)))::statements 
    else statements 
    (oldEntity, newEntity, newStatements) 

let createUserUpdate (oldUser:UserType) newUser = 
    let (_,_,statements) = 
    (oldUser,newUser,[]) 
    |> appendToQuery (fun u -> u.FirstName) "first_name" 
    |> appendToQuery (fun u -> u.LastName) "last_name" 
    |> appendToQuery (fun u -> u.UserName) "username" 
    // ... 

    let statementArr = statements |> List.toArray 
    if (statementArr.Length > 0) then 
    let joinedStatements = System.String.Join(", ", statementArr) 
    Some(sprintf "UPDATE users SET %s WHERE ID=%i" joinedStatements newUser.ID) 
    else 
    None 

如果您有很多需要檢查的屬性,這可能會更簡潔一些。這種方法的一個好處是它即使在檢查多種類型的屬性時也能正常工作,而其他方法要求所有屬性具有相同的類型(因爲它們存儲在列表中)。

+0

今晚我會忙着看這些,學習,並決定如何接近它。我有更多要了解FP。謝謝。 – 2010-05-07 14:59:50

+0

不會在更新內生成重複的'SET'嗎? – 2010-05-07 15:39:04

+0

@Mauricio - 謝謝,修正。 – kvb 2010-05-07 15:45:19