Search code examples
scalaslick

Insert if not exists in Slick 3.0.0


I'm trying to insert if not exists, I found this post for 1.0.1, 2.0.

I found snippet using transactionally in 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)

I'm struggling to write the logic from insert if not exists with this structure. I'm new to Slick and have little experience with Scala. This is my attempt to do insert if not exists outside the transaction...

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

But how do I put this in the transactionally block? This is the furthest I can go:

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

Thanks in advance


Solution

  • This is the version I came up with:

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

    It's is a bit lacking though, for example it would be useful to return the inserted or existing object.

    For completeness, here the table definition:

    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]
    

    I'm using a mapped table, the solution works also for tuples, with minor changes.

    Note also that it's not necessary to define the id as optional, according to the documentation it's ignored in insert operations:

    When you include an AutoInc column in an insert operation, it is silently ignored, so that the database can generate the proper value

    And here the method:

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

    (Thanks Matthew Pocock from Google group thread, for orienting me to this solution).