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