【问题标题】:Insert if not exists in Slick 3.0.0如果 Slick 3.0.0 中不存在则插入
【发布时间】:2015-08-22 17:53:12
【问题描述】:

如果不存在,我正在尝试插入,我找到了 1.0.1、2.0 的 this post

我发现 sn-p 在the docs of 3.0.0 中使用事务性

val a = (for {
  ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
  _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

val f: Future[Unit] = db.run(a)

如果这种结构不存在,我正在努力编写插入的逻辑。我是 Slick 的新手,对 Scala 几乎没有经验。如果事务之外不存在,这是我尝试插入...

val result: Future[Boolean] = db.run(products.filter(_.name==="foo").exists.result)
result.map { exists =>  
  if (!exists) {
    products += Product(
      None,
      productName,
      productPrice
    ) 
  }  
}

但是如何将它放入事务块中?这是我能走的最远:

val a = (for {
  exists <- products.filter(_.name==="foo").exists.result
  //???  
//    _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

提前致谢

【问题讨论】:

  • 如果记录已经存在,您可以重写记录,slick 支持通过products.insertOrUpdate进行更新插入

标签: scala slick


【解决方案1】:

可以使用单个insert ... if not exists 查询。这避免了多次数据库往返和竞争条件(事务可能不够,具体取决于隔离级别)。

def insertIfNotExists(name: String) = users.forceInsertQuery {
  val exists = (for (u <- users if u.name === name.bind) yield u).exists
  val insert = (name.bind, None) <> (User.apply _ tupled, User.unapply)
  for (u <- Query(insert) if !exists) yield u
}

Await.result(db.run(DBIO.seq(
  // create the schema
  users.schema.create,

  users += User("Bob"),
  users += User("Bob"),
  insertIfNotExists("Bob"),
  insertIfNotExists("Fred"),
  insertIfNotExists("Fred"),

  // print the users (select * from USERS)
  users.result.map(println)
)), Duration.Inf)

输出:

Vector(User(Bob,Some(1)), User(Bob,Some(2)), User(Fred,Some(3)))

生成的 SQL:

insert into "USERS" ("NAME","ID") select ?, null where not exists(select x2."NAME", x2."ID" from "USERS" x2 where x2."NAME" = ?)

Here's the full example on github

【讨论】:

  • 这看起来很棒,+1。你能否详细说明一下这个(name.bind, None) &lt;&gt; (User.apply _ tupled, User.unapply),它的作用很清楚,但语法不是很多(最好有一个包含多个字段的示例,比如Product),谢谢!
  • 如果您明确地将类型添加到 vals,您的答案会更有用。这样我就可以更清楚地了解您正在编写什么样的查询。
  • @dwickern 很好的答案。你知道是否可以编译insertIfNotExists 查询或者查询编译器是否必须在每个方法调用上运行?
  • @dwickern,我认为您的示例中的“val exists”是一个 Rep[Boolean]
  • 这个解决方案对我不好!如果在查询过程中插入具有相同主键的列,则会导致 SQLException: ERROR: duplicate key value违反唯一约束。
【解决方案2】:

这是我想出的版本:

val a = (
    products.filter(_.name==="foo").exists.result.flatMap { exists => 
      if (!exists) {
        products += Product(
          None,
          productName,
          productPrice
        ) 
      } else {
        DBIO.successful(None) // no-op
      }
    }
).transactionally

虽然有点欠缺,例如返回插入的或现有的对象会很有用。

为了完整起见,这里是表定义:

case class DBProduct(id: Int, uuid: String, name: String, price: BigDecimal)
class Products(tag: Tag) extends Table[DBProduct](tag, "product") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc) // This is the primary key column
  def uuid = column[String]("uuid")
  def name = column[String]("name")
  def price = column[BigDecimal]("price", O.SqlType("decimal(10, 4)"))

  def * = (id, uuid, name, price) <> (DBProduct.tupled, DBProduct.unapply)
}
val products = TableQuery[Products]

我使用的是映射表,该解决方案也适用于元组,只需稍作改动。

还请注意,没有必要将 id 定义为可选,根据documentation,它在插入操作中被忽略:

当您在插入操作中包含 AutoInc 列时,它会被静默忽略,以便数据库可以生成正确的值

这里是方法:

def insertIfNotExists(productInput: ProductInput): Future[DBProduct] = {

  val productAction = (
    products.filter(_.uuid===productInput.uuid).result.headOption.flatMap { 
    case Some(product) =>
      mylog("product was there: " + product)
      DBIO.successful(product)

    case None =>
      mylog("inserting product")

      val productId =
        (products returning products.map(_.id)) += DBProduct(
            0,
            productInput.uuid,
            productInput.name,
            productInput.price
            )

          val product = productId.map { id => DBProduct(
            id,
            productInput.uuid,
            productInput.name,
            productInput.price
          )
        }
      product
    }
  ).transactionally

  db.run(productAction)
}

(感谢Google group thread 的 Matthew Pocock 引导我了解此解决方案)。

【讨论】:

  • 小心使用这种方法,如果一个线程调用此方法并且在等待第一个过滤器查询的结果时,另一个线程调用该方法并被授予临时权限,则可能会导致死锁情况锁,然后第一个线程将返回并尝试更新表,但处于死锁状态。
【解决方案3】:

我遇到了看起来更完整的解决方案。 Essential Slick 书的Section 3.1.7 More Control over Inserts 有示例。

最后你会得到类似的东西:

  val entity = UserEntity(UUID.random, "jay", "jay@localhost")

  val exists =
    users
      .filter(
        u =>
          u.name === entity.name.bind
            && u.email === entity.email.bind
      )
      .exists
  val selectExpression = Query(
    (
      entity.id.bind,
      entity.name.bind,
      entity.email.bind
    )
  ).filterNot(_ => exists)

  val action = usersDecisions
    .map(u => (u.id, u.name, u.email))
    .forceInsertQuery(selectExpression)

  exec(action)
  // res17: Int = 1

  exec(action)
  // res18: Int = 0

【讨论】:

    【解决方案4】:

    根据 slick 3.0 手动插入查询部分 (http://slick.typesafe.com/doc/3.0.0/queries.html),插入的值可以返回 id 如下:

    def insertIfNotExists(productInput: ProductInput): Future[DBProduct] = {
    
      val productAction = (
        products.filter(_.uuid===productInput.uuid).result.headOption.flatMap { 
        case Some(product) =>
          mylog("product was there: " + product)
          DBIO.successful(product)
    
        case None =>
          mylog("inserting product")
    
          (products returning products.map(_.id) 
                    into ((prod,id) => prod.copy(id=id))) += DBProduct(
                0,
                productInput.uuid,
                productInput.name,
                productInput.price
                )
        }
      ).transactionally
    
      db.run(productAction)
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-09-17
      • 2017-03-07
      • 1970-01-01
      • 2014-06-11
      • 1970-01-01
      • 2017-04-24
      • 1970-01-01
      • 2016-05-15
      相关资源
      最近更新 更多