【问题标题】:Slick 3.0.0 - update row with only non-null valuesSlick 3.0.0 - 仅使用非空值更新行
【发布时间】:2016-02-26 10:31:19
【问题描述】:

有一个包含列的表

class Data(tag: Tag) extends Table[DataRow](tag, "data") {
  def id = column[Int]("id", O.PrimaryKey)
  def name = column[String]("name")
  def state = column[State]("state")
  def price = column[Int]("price")

  def * = (id.?, name, state, price) <> ((DataRow.apply _).tupled, DataRow.unapply)
}

我想编写一个函数来选择单行,并更新提供的值不为空的列。

def update(id: Int, name: Option[String], state: Option[State], price: Option[Int])

例如。

update(1, None, None, Some(5)) 将仅更新数据行 1 的价格,保持名称和状态不变

update(1, Some("foo"), None, Some(6)) 会更新名称和价格,但保持其状态不变。

我想可以使用一些智能映射,但我很难表达它,不确定它如何根据输入(无论它们的值是否定义)吐出不同长度的元组,因为它们更多或减少“不相关”的类。

def update(id: Int, name: Option[String], state: Option[State], price: Option[Int]) = {
  table.fiter(_.id == id). ???? .update(name, state, price)
}

【问题讨论】:

标签: scala functional-programming slick slick-3.0


【解决方案1】:

我是通过以下方式解决的。

只有当它是一个 Product 对象时,下面的实现才有效。

执行更新语句,除了选项类型为None,对象类型为null。

package slick.extensions

import slick.ast._
import slick.dbio.{ Effect, NoStream }
import slick.driver.JdbcDriver
import slick.jdbc._
import slick.lifted._
import slick.relational.{ CompiledMapping, ProductResultConverter, ResultConverter, TypeMappingResultConverter }
import slick.util.{ ProductWrapper, SQLBuilder }

import scala.language.{ existentials, higherKinds, implicitConversions }

trait PatchActionExtensionMethodsSupport { driver: JdbcDriver =>

  trait PatchActionImplicits {
    implicit def queryPatchActionExtensionMethods[U <: Product, C[_]](
        q: Query[_, U, C]
    ): PatchActionExtensionMethodsImpl[U] =
      createPatchActionExtensionMethods(updateCompiler.run(q.toNode).tree, ())
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////// Patch Actions
  ///////////////////////////////////////////////////////////////////////////////////////////////

  type PatchActionExtensionMethods[T <: Product] = PatchActionExtensionMethodsImpl[T]

  def createPatchActionExtensionMethods[T <: Product](tree: Node, param: Any): PatchActionExtensionMethods[T] =
    new PatchActionExtensionMethodsImpl[T](tree, param)

  class PatchActionExtensionMethodsImpl[T <: Product](tree: Node, param: Any) {
    protected[this] val ResultSetMapping(_, CompiledStatement(_, sres: SQLBuilder.Result, _),
      CompiledMapping(_converter, _)) = tree
    protected[this] val converter = _converter.asInstanceOf[ResultConverter[JdbcResultConverterDomain, Product]]
    protected[this] val TypeMappingResultConverter(childConverter, toBase, toMapped) = converter
    protected[this] val ProductResultConverter(elementConverters @ _ *) =
      childConverter.asInstanceOf[ResultConverter[JdbcResultConverterDomain, Product]]
    private[this] val updateQuerySplitRegExp = """(.*)(?<=set )((?:(?= where)|.)+)(.*)?""".r
    private[this] val updateQuerySetterRegExp = """[^\s]+\s*=\s*\?""".r

    /** An Action that updates the data selected by this query. */
    def patch(value: T): DriverAction[Int, NoStream, Effect.Write] = {
      val (seq, converters) = value.productIterator.zipWithIndex.toIndexedSeq
        .zip(elementConverters)
        .filter {
          case ((Some(_), _), _) => true
          case ((None, _), _) => false
          case ((null, _), _) => false
          case ((_, _), _) => true
        }
        .unzip

      val (products, indexes) = seq.unzip

      val newConverters = converters.zipWithIndex
        .map(c => (c._1, c._2 + 1))
        .map {
          case (c: BaseResultConverter[_], idx) => new BaseResultConverter(c.ti, c.name, idx)
          case (c: OptionResultConverter[_], idx) => new OptionResultConverter(c.ti, idx)
          case (c: DefaultingResultConverter[_], idx) => new DefaultingResultConverter(c.ti, c.default, idx)
          case (c: IsDefinedResultConverter[_], idx) => new IsDefinedResultConverter(c.ti, idx)
        }

      val productResultConverter =
        ProductResultConverter(newConverters: _*).asInstanceOf[ResultConverter[JdbcResultConverterDomain, Any]]
      val newConverter = TypeMappingResultConverter(productResultConverter, (p: Product) => p, (a: Any) => toMapped(a))

      val newValue: Product = new ProductWrapper(products)
      val newSql = sres.sql match {
        case updateQuerySplitRegExp(prefix, setter, suffix) =>
          val buffer = StringBuilder.newBuilder
          buffer.append(prefix)
          buffer.append(
            updateQuerySetterRegExp
              .findAllIn(setter)
              .zipWithIndex
              .filter(s => indexes.contains(s._2))
              .map(_._1)
              .mkString(", ")
          )
          buffer.append(suffix)
          buffer.toString()
      }

      new SimpleJdbcDriverAction[Int]("patch", Vector(newSql)) {
        def run(ctx: Backend#Context, sql: Vector[String]): Int =
          ctx.session.withPreparedStatement(sql.head) { st =>
            st.clearParameters
            newConverter.set(newValue, st)
            sres.setter(st, newConverter.width + 1, param)
            st.executeUpdate
          }
      }
    }
  }
}

例子

// Model
case class User(
  id: Option[Int] = None,
  name: Option[String] = None,
  username: Option[String] = None,
  password: Option[String] = None
)

// Table
class Users(tag: Tag) extends Table[User](tag, "users") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def username = column[String]("username")
  def password = column[String]("password")
  override def * = (id.?, name.?, username.?, password.?) <>(User.tupled, User.unapply)
}

// TableQuery
object Users extends TableQuery(new Users(_))

// CustomDriver 
trait CustomDriver extends PostgresDriver with PatchActionExtensionMethodsSupport {
  override val api: API = new API {}
  trait API extends super.API  with PatchActionImplicits
}

// Insert
Users += User(Some(1), Some("Test"), Some("test"), Some("1234"))

// User patch
Users.filter(_.id === 1).patch(User(name = Some("Change Name"), username = Some("")))

https://gist.github.com/bad79s/1edf9ea83ba08c46add03815059acfca

【讨论】:

    【解决方案2】:

    JonasAnso's answer 为基础,将其转换为 slick v3.0+,并将其放入事务中:

      def partialUpdate(id: Int, name: Option[String], login: Option[String]): Future[Int] = {
        val selectQ = users.filter(_.id === id)
    
        val query = selectQ.result.head.flatMap { data =>
          selectQ.update(data.patch(name, login))
        }
    
        db.run(query)
      }
    

    【讨论】:

    • 干得好。我已经对其进行了测试,当然仍然执行了 2 个 SQL 语句,一个用于 SELECT,一个用于更新。但我很喜欢只有一个 db.run
    • 是的,仍然是两个语句,但现在它们被包装在一个事务中,所以它应该像数据库管理器一样安全地对抗竞争条件。
    • 只有在执行SELECT FOR UPDATE 查询时才对竞争条件安全,单独包装事务根本没有帮助(但似乎forUpdate 支持仅在此答案后添加到 Slick已发布github.com/slick/slick/pull/1454)
    【解决方案3】:

    正如我评论的那样,这个问题与现有问题相似,但您似乎没有任何额外要求。

    最简单的方法就是 SELECT + UPDATE。例如,您在 DataRow 类中添加一个补丁函数,定义您希望如何更新模型

          def patch(name: Option[String], state: Option[State], price: Option[Int]): Data {
             this.copy(name = name.getOrElse(this.name), ...)
          }
    

    然后在你的 repo 类中添加一个 partialUpdate 方法

    class DataRepo {
      private val Datas = TableQuery[Data]
      val db = ???
    
      def partialUpdate(id: Int, name: Option[String], state: Option[State], price: Option[Int]): Future[Int] = {
        val query = Datas.filter(_.id === id)
        for {
          data <- db.run(query.result.head)
          result <- db.run(query.update(data.patch(name, state, price)))
        } yield result
      }
    
    }
    

    如您所见,此解决方案的主要问题是有 2 个 SQL 语句,SELECT 和 UPDATE。

    其他解决方案是使用纯 SQL (http://slick.typesafe.com/doc/3.0.0/sql.html),但这当然会带来其他问题。

    【讨论】:

    • 谢谢,请参阅我上面的回答以获得一些改进。
    • 嗨@jonas-anso ?!我今天实际上需要这样做...我很高兴找到您的答案,即使它不是我希望的答案(两个查询还是原始 SQL?蹩脚...)
    • 在这里找到了一篇博客文章,概述了我们想要实现的目标missingfaktor.me/writing/2018/08/12/…,但不幸的是,它似乎有点复杂,你必须定义自己的灵活扩展?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-14
    • 2017-11-05
    相关资源
    最近更新 更多