【问题标题】:More FP-correct way to create an update sql query更多 FP-正确的方法来创建更新 sql 查询
【发布时间】:2011-02-16 15:53:20
【问题描述】:

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

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

更新:

这是我目前使用的解决方案,因为我想让它更通用,所以我只需要为我的实体编写一个抽象类来派生,它们可以使用相同的功能。我选择拆分我如何执行该功能,以便我可以传递如何创建更新的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())

【问题讨论】:

    标签: sql f# functional-programming


    【解决方案1】:

    我喜欢 Mauricio 和 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
    

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

    【讨论】:

    • 今晚我将忙着看这些,学习并决定如何处理它。我还有很多关于 FP 的知识要学习。谢谢。
    【解决方案2】:

    为了完整起见,这里有一个直接使用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,我们从包含空字符串builder和false的初始值开始。在每一步中,我们要么返回原始状态(如果值与之前的相同),要么返回包含true 的新状态和AppendFormat 返回的StringBuilder 的新版本。

    显式使用递归也可以,但是当您可以使用一些内置的 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 处理实体。

    【讨论】:

    • 非常感谢。我需要仔细看看你做了什么,我可以看到它是如何工作的。您的 cmets 看起来很有帮助。
    【解决方案3】:

    这是您心目中的元组解决方案吗?

    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()
    

    我看不出递归解决方案怎么会比这更简单......

    顺便说一句,我强烈建议使用正确的 SQL 参数,而不是仅仅连接值,您可能会容易受到注入攻击...

    【讨论】:

    • 另请注意,这将返回 string option 而不是原始函数中的 string,以涵盖没有任何更新的情况。
    • 如果您使用 Seq.fold 而不是 Seq.map 您可以累积结果并且之后不需要过滤器,您可以将 Seq.toArray 推迟到最后的 else 子句并仅针对属性进行测试.任何。
    • 感谢您的回答。我会将其更改为使用准备好的语句,但实际上,这是一个细节,一旦我完成了更新语句。
    • 我考虑递归的原因是我将有很多实体,如果每个实体都可以以相同的顺序返回列名列表及其值列表,那么我可以一个可以处理我的任何实体的函数,并且遍历三个列表(列名列表、旧值列表、新值列表)可能是递归的。
    • @James Black:如果每个实体都可以返回其列名列表,那么解决方案将非常不同......顺便看看 NHibernate + FunctionalNHibernate:strangelights.com/blog/archive/2009/12/20/1650.aspx
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多