Slick 3.0 delete and returning values - scala

If I want a returning value when inserting a new row, I can do something like
val insertQuery = myTable returning myTable.map(_.id) += SomeData(someString)
How can I achieve the same effect when deleting?
I tried
val deleteQuery = myTable filter (_.id ===id) returning myTable.map(_.someColumn) delete
But apparently this does not compile.
I can resort to the for comprehension but I wonder if there is a shorter way.

The best way I know of to do this is to do something like:
val query = db.filter(....)
val action = for {
results <- query.result
_ <- query.delete
} yield results
db.run(action.withTransactionIsolation(TransactionIsolation.RepeatableRead))
Wish it was shorter.

Related

Doobie. Compose .update.withGeneratedKeys() and .update.run

Referencing to this question.
I want to insert some entity by some condition. It can either be inserted or not. If the condition is true the entity is inserted. I want to insert some other data in various tables. It looks like this:
val q = sql"insert into some_table (some_field) select 42 where ...(some condition)"
val inserts = List(
sql"insert ...",
sql"insert ...",
sql"insert ..."
)
for {
id <- q.update.withGeneratedKeys[Long]("id")
_ <- inserts.reduce(_ ++ _).update.run
} yield id
The problem is this does not compile because the first insert is a fs2.Stream but the second one is not.
I was trying to replace _ <- inserts.reduce... with _ = inserts.reduce. The app can compile but inserts in the second line does not occur.
UPD
My possible way to solve this problem:
...
for {
idOpt <- q.update.withGeneratedKeys[Long]("id").compile.last
_ <- idOpt.fold(0.pure[ConnectionIO])(_ => inserts.reduce(_ ++ _).update.run)
} yield idOpt
This works, but IMHO this is not pretty. Is there a better way to do it?
One way to perform your batch inserts - if you have similar data - is to use updateMany - see doc:
import doobie._
type PersonInfo = (String, Option[Short])
def insertMany(ps: List[PersonInfo]): ConnectionIO[Int] = {
val sql = "insert into person (name, age) values (?, ?)"
Update[PersonInfo](sql).updateMany(ps)
}
// Some rows to insert
val data = List[PersonInfo](
("Frank", Some(12)),
("Daddy", None))
Also, if you remove.compile.last, you can use the fact that if your resulting Stream q.update.withGeneratedKeys[Long]("id") is empty, you 'exit early' the for-comprehension.
So all in all, here is what you could do:
import fs2.Stream
val result =
// Now the for-comprehension operates on a Stream instead of an Option
for {
r <- q.update.withGeneratedKeys[Long]("id")
_ <- Stream.eval(insertMany(data)) // insertMany(data) is defined like in the snippet above
} yield r
result.compile.last

How can I run inter-dependent queries alongside a non-DB operation in the same transaction using slick

Given the data model (<- indicating a foreign key dependency)
TableA <- TableB <- TableC
^ v
-----------------
I need to execute an api DELETE operation that soft-deletes a row in TableC. This delete must also trigger a call to another service (requiring values from TableA and TableB) if there are no more undeleted TableC entries that reference that row's parent in TableB. If the external call fails, I want to rollback the soft-delete. I want to do all of this in an idiomatic fashion (I'm effectively brand new to scala/slick), and use transactions for the rollback
Based on what I've read, I need to be using for comprehension to assemble the queries, but I'm having issues getting the database operations to gel nicely with the external service call. My original plan was:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (count > 1) DBIO.successful(httpRequestService.deleteOtherResource(tableARow.someValue, tableBRow.someValue))
} yield ()
db.run(select.result)
But this had problems because I couldn't pass Slick's Rep[T] values to my httpRequestService method. I then tried to break it down into two portions - SELECT first, then DELETE, like so:
val select = for {
tableCRow <- tableBDao.TableQueryC.filter(_.id === idParam)
tableBRow <- tableBDao.TableQueryB if tableCRow.tableBForeignKey === tableBRow.id
tableARow <- TableADao.TableQueryA if tableCRow.tableAForeignKey === tableARow.id
count <- tableBDao.TableQueryC.filter(_.tableBForeignKey === tableBRow.id).map(_.id).countDefined
} yield (tableBRow.date.formatted("yyyy-MM-DD"), tableARow.externalServiceId, count)
val result: Future[Option[(String, Long, Integer)]] = db.run(select.result.headOption)
result.map {
case None => throw new IllegalArgumentException("exception message")
case Some(data) =>
val delete = for {
_ <- tableBDao.softDeleteRow(idParam)
_ <- if (data._3 > 1) DBIO.successful(httpRequestService.cancelSchedulerJob(data._2, data._1))
} yield numRows
db.run(delete.transactionally)
}
But, despite this actually passing IntelliJ IDEA checks, it won't compile as my count query returns a Rep[Int], which lacks a map function. Additionally, each of my table(A|B|C)Row maps raises an error because they're expecting slick.lifted.Query[Nothing,Nothing,Seq] and they're getting slick.lifted.Query[Nothing,T,Seq]. Finally, the db.run statement doesn't want to use headOption, and apparently returns Any which doesn't support map
halp
Solved this by finally understanding how slick puts things together in a for-comprehension. I had to pull the count out of the query and into a followup groupby->map function, which accumulated my list of things I wanted to count and THEN counted them as opposed to counting as part of the query. This fixed all the rest of the problems too, as the count query was throwing off the expected return types of everything else.
basically, the solution looked like (thing1 was for a join):
val select = (for {
thing1 <- query1
thing2 <- query2
thing3 <- query3
listToCount <- query4 selecting everything I wanted to count
} yield (thing2, thing3, listToCount))
.groupBy({
case (thing2, thing3, listToCount) =>
(thing2, thing3)
})
.map({
case ((thing2, thing3), list) =>
(thing2.deliveryDate, thing3.schedulerJobId, list.map(_._3).length)
})

Slick Scala rows in database

I am starting to work with slick and scala and it seems that I still do not have the basics iron out:
I am using slick 3.0.0 and scala 2.11.7 and I am connecting to an Oracle database.
I want to get the number of rows in my table so I went and did a search and found the following:
1) This one tells me that .run does not exist:
mytable.length.run
2)This one tells me that there is a type mismatch found: slick.lifted.Rep[Int] and expected String:
var q = for(u <- mytable) yield u.id
print(q.size)
3)This one compiles and runs but prints Rep(Pure $#309962262):
var q = for{row <- mytable} yield row
println(Query(q.length))
So I am not sure if it is because I do not understand how this works but I was imagining that the following should happen:
A) constructQuery
b) "run" query.
So the other queries that I am using are as follow:
val db = Database.forConfig("Oracle")
try{
val f: Future[Unit] = {
val query: StreamingDBIO[Seq[String], String] = participants.map(_.id).result // A)"Construct query"
val stremQuery: DatabasePublisher[String] = db.stream(query) //B) "Run query"
streamQuery.foreach(println)
}
Await.result(f, Duration.Inf)
}
finally db.close
What am I missing? is number 3 not giving me what I want because is not under a db.stream/db.run/db.something command? Or am I just lost =)
Thanks in advance
Tona
Querying a database in slick basically consists of these three steps:
Create a Query
Convert Query to an Action
Execute the Action on a database
So your example would look something like this (types are optional and added for clarity):
val query = mytable.length // length is an aggregation
val action = query.result
val result: Future[Int] = db.run(action)
// Access result in a non blocking way (recommended):
result.map(count: Int => ...)
// or for completeness use Await (not recommended):
val count: Int = Await.result(result, Duration.Inf)
Further reading:
Slick documentation, Queries
Futures and Promises in scala

Slick 3.0 how to update variable column list, which number is know only in Runtime

Is it possible to update variable column list, which number is know only in runtime by slick 3.0?
Below is example what I want to do (won't compile)
var q: Query[UserTable, UserTable#TableElementType, Seq] = userTable
var columns = List[Any]()
var values = List[Any]()
if (updateCommands.name.isDefined) {
columns = q.name :: columns
values = updateCommands.name.get :: values
}
if (updateCommands.surname.isDefined) {
columns = q.surname :: columns
values = updateCommands.surname.get :: values
}
q = q.filter(_.id === updateCommands.id).map(columns).update(values)
Here is what I've done in Slick 3.1. I wasn't sure what worse, editing plain SQL statement or multiple queries. So I decided to go with latter assuming Postgres optimizer would see same WHERE clause in update queries of single transaction. My update method looks like this
def updateUser(user: User, obj: UserUpdate): Future[Unit] = {
val actions = mutable.ArrayBuffer[DBIOAction[Int, NoStream, Write with Transactional]]()
val query = users.withFilter(_.id === user.id)
obj.name.foreach(v => actions += query.map(_.name).update(v))
obj.email.foreach(v => actions += query.map(_.email).update(Option(v)))
obj.password.foreach(v => actions += query.map(_.pwdHash).update(Option(encryptPassword(v))))
slickDb.run(DBIO.seq(actions.map(_.transactionally): _*))
}
In Slick 3.0 they adopted slightly different approach, instead of having updateAll methods, as far as I userstand path of combinators was adopted.
So main idea is to define some actions on the data and then combine them ont he database to make a single run.
Example:
// let's assume that you have some table classes defined somewhere
// then let's define some actions, they might be really different
val action: SqlAction = YourTable.filter(_id === idToAssert)
val anotherAction = AnotherTable.filter(_.pets === "fun")
// and then we can combine them on a db.run
val combinedAction = for {
someResult <- action
anotherResult <- anotherAction
} yeild (someResult,anotherResult)
db.run(combinedAction) // that returns actual Future of the result type
In the same way you can deal with lists and sequences, for that please take a look here: http://slick.typesafe.com/doc/3.1.0-M1/dbio.html
DBIO has some functions that allows you to combine list of actions to one action.
I hope that idea is clear, if you have questions you are wellcome to the comments.
to update a variable number of columns you may use this way as I used for slick 3:
def update(id: Long, schedule: Schedule, fieldNames: Seq[String]): Future[_] = {
val columns = schedules.baseTableRow.create_*.map(_.name).toSeq.filter(fieldNames.map(_.toUpperCase).contains)
val toBeStored = schedule.withDefaults
val actions = mutable.ArrayBuffer[DBIOAction[Int, NoStream, Write with Transactional]]()
val query = schedules.withFilter(_.id === id)
//this is becasue of limitations in slick, multiple columns are not possible to be updated!
columns.find("NAME".equalsIgnoreCase).foreach(x => actions += query.map(_.name).update(toBeStored.name))
columns.find("NAMESPACE".equalsIgnoreCase).foreach(x => actions += query.map(_.namespace).update(toBeStored.namespace))
columns.find("URL".equalsIgnoreCase).foreach(x => actions +=
db.run(DBIO.seq(actions: _ *).transactionally.withPinnedSession)
}

How do you change lifted types back to Scala types when using Slick lifted embedding?

How do you 'un-lift' a value inside a query in Slick when using lifted embedding? I was hoping a 'get', 'toLong' or something like that may do the trick, but no such luck.
The following code does not compile:
val userById = for {
uid <- Parameters[Long]
u <- Users if u.id === uid
} yield u
val userFirstNameById = for {
uid <- Parameters[Long]
u <- userById(uid)
---------------^
// type mismatch; found : scala.slick.lifted.Column[Long] required: Long
} yield u.name
You can't, for 2 reasons:
1) with val this is happening at compile time, there is no Long
value uid. userById(uid) binds a Long uid to the compile time
generated prepared statement, and then .list, .first, etc. invoke
the query.
2) the other issue is as soon as you Parameterize a query,
composition is no longer possible -- it's a limitation dating back to
ScalaQuery.
Your best bet is to delay Parameterization until the final composed query:
val forFooBars = for{
f <- Foos
b <- Bars if f.id is b.fooID
} yield(f,b)
val allByStatus = for{ id ~ active <- Parameters[(Long,Boolean)]
(f,b) <- forFooBars if (f.id is id) && (b.active is active)
} yield(f,b)
def findAllByActive(id: Long, isActive: Boolean) = allByStatus(id, isActive).list
At any rate, in your example you could just as well do:
val byID = Users.createFinderBy(_.id)
The only way that I know to get this kind of thing to work is wrap the query val in a def and pass in a runtime variable, which means Slick has to re-generate the sql on every request, and no prepared statement is sent to underlying DBMS. In some cases you have to do this, like passing in a List(1,2,3) for inList.
def whenNothingElseWorks(id: Long) = {
val userFirstNameById = for {u <- userById(id.bind)} yield u.name
}