更多FP-correct方法来创建更新sql查询

时间:2010-05-07 02:00:50

标签: sql f# functional-programming

我正在使用F#访问数据库,我最初尝试创建一个创建更新查询的函数是有缺陷的。

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,这样我就可以遍历列表并构建查询,但似乎我应该传入{{1返回一个递归函数,返回一个StringBuilder,然后作为参数传递给递归函数。

这似乎是最好的方法,还是有更好的方法?

更新

这是我当前使用的解决方案,因为我想让它更通用化,所以我只需要为我的实体编写一个抽象类来派生,他们可以使用相同的函数。我选择拆分我如何执行该功能,以便我可以传递如何创建更新的boolean部分,以便我可以测试不同的想法。

SET

3 个答案:

答案 0 :(得分: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参数而不是仅仅连接值,你可能会受到注入攻击的攻击......

答案 1 :(得分:1)

为了完整起见,这是一个直接使用fold函数执行相同操作的版本。这可以非常优雅地完成,因为StringBuilder的方法返回StringBuilder(允许您在C#中链接它们)。这也可以很好地用于折叠。

让我们假设我们有Mauricio解决方案中的元组列表:

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的新状态和StringBuilder返回的AppendFormat的新版本。

显式使用递归也可以,但是当你可以使用一些内置的F#函数时,通常更容易使用这种方法。如果需要处理每个实体的嵌套实体,可以将Seq.collect函数与递归一起使用,以获取需要使用fold处理的属性列表。伪代码可能如下所示:

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)

使用序列表达式可以更优雅地编写:

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,它返回一个实体的平面列表,并使用fold处理实体,如第一种情况。

答案 2 :(得分:1)

我喜欢毛里西奥和托马斯的解决方案,但也许这更像你原先设想的那样?

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

如果您要检查许多属性,这可能会更简洁一些。这种方法的一个好处是,即使您正在检查多种类型的属性,它也可以工作,而其他方法要求所有属性具有相同的类型(因为它们存储在列表中)。