Conditionally running Slick statements in a for comprehension - scala

Given the following Slick code:
val doUpdate = true
val table1 = TableQuery[Table1DB]
val table2 = TableQuery[Table2DB]
val table3 = TableQuery[Table3DB]
val action = (for {
_ <- table1.filter(_.id === id).update(table1Record)
_ <- table2.filter(_.id === id).update(table2Record)
_ <- table3.filter(_.id === id).update(table3Record)
} yield ()).transactionally
What I need is to update table2 and table3 ONLY if the variable doUpdate is true. This is my attempt:
val action = (for {
_ <- table1.filter(_.id === id).update(table1Record)
_ <- table2.filter(_.id === id).update(table2Record)
if (doUpdate == true)
_ <- table3.filter(_.id === id).update(table3Record)
if (doUpdate == true)
} yield ()).transactionally
This compiles, but when I run it I get the following error Action.withFilter failed. How to add the condition?

As you notices if you use if inside a for it will call withFilter underneath. In case of pipelines (DBIO, Futures, Tasks) that filtering boils down to whether or not continue with a pipeline (happy path) or fail it (generating Failure or similar).
If all you want is to update, then you can go with the if road and recover the Future. However, it would we ugly, since depending on how you write the case it might also recover from errors you would like to be informed about.
Alternative is to simply use if-else to provide a different DBIOAction:
(for {
_ <- {
if (doUpdate) conditionalAction.map(_ => ())
else DBIOAction.successful(())
}
} yield ()).transactionally
In your case it could look something like:
val action = (for {
_ <- table1.filter(_.id === id).update(table1Record)
_ <- {
if (doUpdate) DBIOAction.seq(
table2.filter(_.id === id).update(table2Record),
table3.filter(_.id === id).update(table3Record)
)
else DBIOAction.successful(())
}
} yield ()).transactionally

Related

How do you flatten a Sequence of Sequence in DBIOaction SLICK?

Hey guys i am new in slick, how can I flatten this sequence of sequence? so that can return the commented code
def insertIfNotExists(mapCountryStates: Map[String, Iterable[StateUtil]]): Future[Seq[Seq[StateTable]]] /*: Future[Seq[StateTable]]*/ = {
val interaction = DBIO.sequence(mapCountryStates.toSeq.map { case (alpha2Country, statesUtil) =>
val codes = statesUtil.map(_.alpha3Code)
for {
countryId <- Countries.filter(_.alpha2Code === alpha2Country).map(_.id).result.head
existing <- States.filter(s => (s.alpha3Code inSet codes) && s.countryId === countryId).result
stateTables = statesUtil.map(x => StateTable(0L, x.name, x.alpha3Code, countryId))
statesInserted <- StatesInsertQuery ++= stateTables.filter(s => !existing.exists(x => x.alpha3Code == s.alpha3Code && x.countryId == s.countryId))
} yield existing ++ statesInserted
})
db.run(interaction.transactionally)
}
if I write it here:
val interaction = DBIO.sequence(...).flatten
or here:
db.run(interaction.flatten.transactionally)
[error] Cannot prove that Seq[Seq[StateRepository.this.StateTableMapping#TableElementType]] <:< slick.dbio.DBIOAction[R2,S2,E2].
but when the application runs, because the IDE does not detect it as an error:
I update my definition with DBIO.fold:
It looks like you might be after DBIO.fold. This provides a way to take a number of actions and reduce them down to a single value. In this case, your single value is a Seq[StateTable] from a Seq[Seq[StateTable]].
A sketch of how this could look might be...
def insertIfNotExists(...): DBIO[Seq[StateTable]] = {
val interaction: Seq[DBIO[Seq[StateTable]]] = ...
val startingPoint: Seq[StateTable] = Seq.empty
DBIO.fold(interaction, startingPoint) {
(total, list) => total ++ list
}
}
It looks like the types will line up using fold. Hope it's of some use in your case.
There's some more information about fold in Chapter 4 of Essential Slick.
A viable solution should be to flatten the sequence once the Future has been completed:
def insertIfNotExists(mapCountryStates: Map[String, Iterable[StateUtil]]): Future[Seq[StateTable]] = {
val interaction = DBIO.sequence(mapCountryStates.toSeq.map { case (alpha2Country, statesUtil) =>
val codes = statesUtil.map(_.alpha3Code)
for {
countryId <- Countries.filter(_.alpha2Code === alpha2Country).map(_.id).result.head
existing <- States.filter(s => (s.alpha3Code inSet codes) && s.countryId === countryId).result
stateTables = statesUtil.map(x => StateTable(0L, x.name, x.alpha3Code, countryId))
statesInserted <- StatesInsertQuery ++= stateTables.filter(s => !existing.exists(x => x.alpha3Code == s.alpha3Code && x.countryId == s.countryId))
} yield existing ++ statesInserted
})
db.run(interaction.transactionally).map(_.flatten)
}

Scala syntax problems in for comprehension statement

Is it possible to make an if statement in a for block?
I have the following:
val process = for {
stepIds <- processTemplateDTO.getProcessStepTemplateIds(processTemplateId)
allApprovedProcessTemplates <- processTemplateDTO.getApprovedProcessTemplates(clientId) //Get all approved process templates
processTemplate <- processTemplateDTO.getProcessTemplate(processTemplateId, clientId) // Get the Process Template
prerequisites <- getProcessTemplateForEdit(processPrerequisitesDTO.getProcessPrerequisiteProcessTemplateIdsByProcessTemplateId(processTemplateId), clientId)
postConditions <- getProcessTemplateForEdit(processPostConditionsDTO.getProcessPostConditionProcessTemplateIdsByProcessTemplateId(processTemplateId), clientId)
approvedProcessTemplate <- processTemplateDTO.getProcessTemplate(processTemplate.get.approveprocess, clientId)
if (processTemplate.get.trainingsprocess.isDefined) {
trainedProcessTemplate <- processTemplateDTO.getProcessTemplate(processTemplate.get.trainingsprocess.get, clientId)
}
And I only want to call the processTemplateDTO.getProcessTemplate if processTemplate.get.trainingsprocess.isDefined is true
is this possible?
Thanks
also tried this way:
trainedProcessTemplate <- {
if (processTemplate.get.trainingsprocess.isDefined) {
processTemplateDTO.getProcessTemplate(processTemplate.get.trainingsprocess.get, clientId)
} else {
None
}
}
UPDATE
trainedProcessTemplate <- Nil
_ = if (processTemplate.get.trainingsprocess.get != null) {
processTemplateDTO.getProcessTemplate(processTemplate.get.trainingsprocess.get, clientId)
}
Yes, It is possible.
You can see the below code for reference -
val list = List(1,2,3,4)
val result = for {
id <- list
_ = if (id < 2) {
println(s"Hello I am $id")
}
} yield id
Yes it is possible to use if block in for block, there are two ways
for {
a <- somework1 // returns boolean
b <- somework2 if(a)
} yield (a, b) //anything you like
or you can try
for {
a <- somework1 // returns boolean
b <- if(a) somework2 else somework3
} yield (a, b) //anything you like
In 2nd way you have to give else block as well because without that it will return Any
Hope this helps!

How to mix select and delete in a Slick transaction

Why does it not work to combine a SELECT and a DELETE statement in a Slick query? as in:
val query = (for {
item <- SomeTable
_ <- OtherTable.filter(_.id === item.id).delete
} yield ()).transactionally
"Cannot resolve symbol 'transactionally'"
(without .transactionally, it is a Query[Nothing, Nothing, Seq], if that helps)
while the two actions work separately:
val query = (for {
item <- SomeTable
} yield ()).transactionally
,
val query = (for {
_ <- OtherTable.filter(_.id === 2).delete
} yield ()).transactionally
OK so this is a classic example of mixing DBIO with Query.
In your first case:
val query = (for {
item <- SomeTable // this is `Query`
_ <- OtherTable.filter(_.id === item.id).delete // this is `DBIO`
} yield ()).transactionally
Obviously for DML you can use only actions (Query is for DQL - being simply SELECT).
So first thing is - change your code to use only DBIOs. Below example is incorrect.
val query = (for {
item <- SomeTable.result // this is `DBIO` now
_ <- OtherTable.filter(_.id === item.id).delete // but this won't work !!
} yield ()).transactionally
OK, we are nearly there - the problem is that it doesn't compile. What you need to do is to be aware that now this part:
item <- SomeTable.result
returns Seq of your SomeTable case class (which among other things contains your id).
So let's take into account:
val query = (for {
items <- SomeTable.result // I changed the name to `items` to reflect it's plural nature
_ <- OtherTable.filter(_.id.inset(items.map(_.id))).delete // I needed to change it to generate `IN` query
} yield ()).transactionally

Filtering inside for-comprehension with futures

I am doing something like this:
(for {
data <- Future(getData)
updated = makeChanges(data) if updated != data
_ <- Future(saveUpdates(updated))
_ <- Future(recordTransaction)
} yield ()).recover { case e: NoSuchElementException => () }
When filter is not satisfied, it skips the remaining two steps (good!) by throwing an exception (not good), that I have to catch and handle at the end. Using exceptions for flow control does not feel too elegant to me though, I am wondering if there is a better way to do this, aside from the obvious - wrapping all remaining lines with an if statement:
_ <- if(updated != data) Future(saveUpdates(updated)) else Future.successful(())
_ <- if(updated != data) ...
I don't think you can avoid the exception for the flow control using for comprehension in that way, you could use a nested expression instead of the filter and handle the condition manually giving scala the return type it needs in case the condition is not satisfied:
for {
data <- Future(data)
updated = makeChanges(data)
res = {
if (updated != data) Future.successful(())
else for {
_ <- Future(saveUpdates(updated))
_ <- Future(recordTransaction)
} yield ()
}
} yield res
But for this example I'd go the simple way and drop the for comprehension which results in more readable code (probably your real use case is more complex though):
Future(data).flatMap(d => {
val updated = makeChanges(d)
if(updated == d) Future.successful(())
else Future(saveUpdates(updated)).map(_ => recordTransaction)
})

change function call sequence based on a request parameter

I have a situation similar to below where if request is coming from the UI then request must be validated first before doing anything else. However if request wasn't submitted from the UI - but say via EDI -> in this case there are some biz req how a child data "id" is populated in the request after persisting parent data and use parent data id into child data section. that detail isn't important for this question.
In order to change order of method calls inside a for comprehension I have something similar to below which looks bit repetitive and non idiomatic . is there a better way to achieve this?
def persistData(req : Request) = {
req.actionFromUI match{
case Some(_) => for{
validatedReq <- validateRequest(req) //1st thing
transformedReq <- transformRequest(validatedReq)
persitedReq <- persistRequestData(transformedReq)
}
case None => for{
transformedReq <- transformRequest(validatedReq)
persitedReq <- persistRequestData(transformedReq)
validatedReq <- validateRequest(persitedReq) //last thing
}
}
}
How about something like:
def persistData(req : Request) = {
val (c1, c2, c3) = req.actionFromUI match{
case Some(_) => (validateRequest(req),
transformRequest(validatedReq),
persistRequestData(transformedReq))
case _ => (transformRequest(validatedReq),
persistRequestData(transformedReq),
validateRequest(persitedReq))
}
for {
f1 <- c1
f2 <- c2
f3 <- c3
} // .. do something here with f1, f2, f3
}
Something like this maybe (assuming your validateRequest returns an Option[Request])?
val (preValidate, postValidate) = req.actionFromUI match {
case Some(_) => (validateRequest _, Option[Request].apply _)
case _ => (Option[Request].apply _, validateRequest _)
}
for {
pre <- preValidate(req)
transformed <- transformRequest(pre)
persisted <- persistRequest(transformed)
post <- postValidate(persisted)
}
Or alternatively
def validateRequest(req: Request, pre: Boolean) =
if(req.actionFromUI.isDefined == pre) validateRequest(req) else Some(req)
for {
pre <- validateRequest(req, true)
transformed <- transformRequest(pre)
persisted <- persistRequest(transformed)
post <- validateRequest(persisted, false)
}