Более FP-правильный способ создания обновления SQL-запроса - PullRequest
0 голосов
/ 07 мая 2010

Я работаю над доступом к базе данных, используя 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, который затем передается в качестве параметра рекурсивной функции.

Кажется ли это лучшим подходом или есть лучший?

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

Ответы [ 3 ]

4 голосов
/ 07 мая 2010

Это решение для кортежей, которое вы имели в виду?

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 вместо простого объединения значений, вы можете стать уязвимыми для атак с использованием инъекций ...

1 голос
/ 07 мая 2010

Мне нравятся как решения Маурисио, так и Томаса, но, возможно, это больше похоже на то, что вы изначально предполагали?

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

Если у вас есть много свойств для проверки, это может быть немного более кратким. Одним из преимуществ этого подхода является то, что он работает, даже если вы проверяете свойства нескольких типов, тогда как другие подходы требуют, чтобы все свойства имели одинаковый тип (поскольку они хранятся в списке).

1 голос
/ 07 мая 2010

Просто для полноты, вот версия, которая делает то же самое напрямую, используя функцию 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 и новую версию 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, как в первом случае.

...