Scala Playframework not all DB queries get executed - scala

I'm sending via HTTP Post Request a Json to my Playframework backend.
In my backend I validate the Json to a Model. After that, I want to save the entries in my Model to my DB.
def parseJSON: Action[AnyContent] = Action.async {
request =>
Future {
request.body.asJson.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
itemsToDBController.saveItems(items)
Ok("Success")
case JsError(err) =>
println(err)
BadRequest("Json Parse Error")
}).getOrElse(BadRequest("Error"))
}
}
One Item consists out of several objects. To save all objects to my DB, I need to get some values. Therefore I'm using a for(..) yield(...):
def saveItems(items: MyModel) = {
items.SomeObject.map(obj => {
if (obj.value1.isDefined &&
obj.value2.isDefined ) {
val result = for (
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
) yield(value1Entry, value2Entry)
result.map({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry)
Future.successful()
}
case _ => Future.failed(new Exception("Not all entries defined"))
})
}
else {
Future.successful("Not all objects defined - skipping")
}
})
}
My problem is, after all result.map({...}) have started, my parseJSON Action returns 200 - OK. But not all relevant items get stored to my DB. It seems like after the 200 - OK everything is stopped and it doesn't even throw an error.
I don't want to use Await.result or anything blocking in my Action.
Thanks in Advance

You are starting computations by calling itemsToDBController.saveItems(items) and then immediately return result with Ok("Success"). So exception may be thrown after request if completed.
To fix this issue you need to transform result of itemsToDBController.saveItems from List[Future[T]] to Future[List[T]] with help of Future.sequence. Then call map method on returned future. Call recover on this Future to find which error is thrown:
def parseJSON: Action[AnyContent] = Action.async { request =>
request.body.asJson
.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
Future
.sequence(itemsToDBController.saveItems(items))
.map(_ => Ok("Success"))
.recover {
case e: Exception => BadRequest(e.getMessage())
}
case JsError(err) =>
println(err)
Future.successful(BadRequest("Json Parse Error"))
})
.getOrElse(Future.successful(BadRequest("Error")))
}
Update
For running all inserts in one transaction you should combine DBIOAction instead of Future. For example you rewrite checkExists(name) as:
def checkExists(name: String): DBIO[Boolean] = {
Objects.filter(obj => obj.name === name).exists
}
getOrCreateValue(exists, obj) as:
def getOrCreateValue(exists: boolean, obj: Object): DBIO[Object] = {
if (exists) {
Objects.filter(o => o.name === name).result.head
} else {
(Objects returning Objects.map(_.id) into ((o, id) => o.copy(id = Some(id)))) += obj
}
}
Now you can run it in single transaction in the following way:
def saveItems(items: MyModel) = {
val insertActions = items.SomeObject.map(obj => {
if (obj.value1.isDefined && obj.value2.isDefined) {
val result = for {
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
} yield (value1Entry, value2Entry)
result.flatMap({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry) // This also returns instance of `DBIOAction`
}
case _ =>
DBIO.failed(new Exception("Not all entries defined"))
})
} else {
DBIO.successful("Not all objects defined - skipping")
}
})
db.run(DBIO.sequence(inserActions).transactionally)
}
For mo info how to work with DBIO actions check this official docs

Related

how should I optimise the scala code and make it more readable [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
I have this ugly massive piece of code. It does many things, mostly around querying databases. Each query depends on the result of the previous query or on the parameters of the incoming request body. Queries are done asynchronously using Future. The code works but is not readable. How can I restructure it to make it concise? I have thought of using for instead of map and flatMap but due to dependencies on previous queries, I am not able to figure out how to use result of previous Future in new ones and how to handle error paths.
I am using play 2.6 and scala 2.12.2
def oldSignupUser:Action[AnyContent] = silhouette.UserAwareAction.async {
implicit request => {
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
jsonBody match {
case Some(json) => {
val userProfile: Option[UserProfile] = json.asOpt[UserProfile] userProfile match {
case Some(profile) => {
val loginInfo = LoginInfo(CredentialsProvider.ID, profile.externalProfileDetails.email)
val userKeys = UserKeys(utilities.bucketIDFromEmail(profile.externalProfileDetails.email),profile.externalProfileDetails.email,loginInfo,profile.externalProfileDetails.firstName,profile.externalProfileDetails.lastName)
val findUserFuture: Future[Option[User]] = userRepo.findOne(userKeys) // userFuture will eventually contain the result of database query i.e Some(user) or None
findUserFuture.flatMap { (userOption: Option[User]) =>
userOption match {
case Some(user) => {
if(user.profile.internalProfileDetails.get.confirmed) {
Future {
Ok(Json.toJson(JsonResultError(messagesApi("error.duplicateUser")(langs.availables(0)))))
}
} else {
val userKeys = UserKeys(utilities.bucketIDFromEmail(user.profile.externalProfileDetails.email),user.profile.externalProfileDetails.email,loginInfo,user.profile.externalProfileDetails.firstName,user.profile.externalProfileDetails.lastName)
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
logger.trace(s"user token save future ${userTokenSaveFuture}")
userTokenSaveFuture.map( (userTokenOption:Option[UserToken])=>{
userTokenOption match {
case Some(userToken) => {
val signupEmailOption:Option[SignupEmail] = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail:SignupEmail) =>{
val _:String = mailerService.sendEmail(signupEmail.subject, signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
Ok(Json.toJson(JsonResultSuccess(messagesApi("success.userSignupConfirmationEmailSent")(langs.availables(0)))))
}
case None =>{
InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
case None => {
Ok(Json.toJson(JsonResultError("user not added"))) //Todom - this is misleading as user is added but token isn't
}
}
})
}
}
case None => {
val passwordInfo:PasswordInfo = userRepo.hashPassword(profile.externalProfileDetails.password.get)
val bucketId = utilities.bucketIDFromEmail(profile.externalProfileDetails.email)
val newUser:User = User(
utilities.getUniqueID(),//UUID.randomUUID(),
UserProfile(Some(InternalUserProfile(loginInfo,bucketId,false,Some(passwordInfo))),
profile.externalProfileDetails))
val saveUserFuture:Future[Option[User]] = userRepo.save(newUser)
saveUserFuture.flatMap { (userOption:Option[User]) =>{ userOption match {
case Some(user) => {
val initialPortfolio = user.profile.externalProfileDetails.portfolio
val profileAndPortfolio = profile.externalProfileDetails.copy(portfolio = initialPortfolio)
logger.trace(s"saving external profile and portfolio ${profileAndPortfolio}")
val savedProfileAndPortfolioOptionFuture = userProfileAndPortfolioRepo.save(profileAndPortfolio)
savedProfileAndPortfolioOptionFuture.flatMap(profileAndPortfolioOption =>{
profileAndPortfolioOption match {
case Some(profileAndPortfolio) => {
val userKeys = UserKeys(utilities.bucketIDFromEmail(user.profile.externalProfileDetails.email),user.profile.externalProfileDetails.email,loginInfo,user.profile.externalProfileDetails.firstName,user.profile.externalProfileDetails.lastName)
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
userTokenSaveFuture.flatMap( (userTokenOption:Option[UserToken])=>{
userTokenOption match {
case Some(userToken) => {
val signupEmailOption = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail) =>{
val _:String = mailerService.sendEmail(signupEmail.subject,signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
Future{Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))}
}
case None =>{
Future{InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))}
}
}
}
case None => {
Future{Ok(Json.toJson(JsonResultError("user not added"))) }
}
}
})
}
case None =>{
Future{InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))}
}
}
})
}
case None => {
Future{Ok(Json.toJson(JsonResultError("unable to add user")))}
}
}
}
}
.recover { case x => {
x match {
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case _ => InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
}
}
}
.recover { case x => {
logger.trace("Future failed in signupUser. In recover. Returning Internal Server Error"+x)
x match {
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case _ => InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
}
case None => Future {
logger.trace("invalid profile structure")
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyStructure")(langs.availables(0))))) }
}
}
case None => Future {
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyType")(langs.availables(0))))) }
}
}
}
Update - the question was closed before I could solve the problem. Many thanks to mfirry and tim for their answers. this is the version I could come up with which I believe is modular.
def getUserProfileFromBody(json:JsValue): Option[UserProfile] ={
val userProfile = json.asOpt[UserProfile] //check if json conforms with UserProfile structure
userProfile
}
def getJsonBody(body:AnyContent) = {
val jsonBody: Option[JsValue] = body.asJson
jsonBody
}
def generateUserKeysFromUserProfile(profile:UserProfile):UserKeys = {
val loginInfo = LoginInfo(/*provider id eg "credentials"*/CredentialsProvider.ID, /*provider Key eg email*/profile.externalProfileDetails.email)
val userKeys = UserKeys(utilities.bucketIDFromEmail(profile.externalProfileDetails.email),profile.externalProfileDetails.email,loginInfo,profile.externalProfileDetails.firstName,profile.externalProfileDetails.lastName)
logger.trace(s"generated userkey ${userKeys}")
userKeys
}
def findIfUserIsNewOrExisting(userKey:UserKeys): Future[Boolean] ={
logger.trace(s"looking for user with keys ${userKey}")
val findUserFuture = userRepo.findOne(userKey) // userFuture will eventually contain the result of database query i.e Some(user) or None
logger.trace(s"user future is ${findUserFuture}")
for(userOption <- findUserFuture) yield {
logger.trace(s"user option ${userOption}")
userOption match {
case Some(user) => {
val userConfirmed = isUserConfirmed(user)
if(userConfirmed) {
throw new DuplicateUserException(user,"duplicateuser", new Throwable("duplicateuser"))
} else throw new UnconfirmedUserException(user,"unconfirmeduser", new Throwable("unconfirmeduser"))
}
case None => {
false
}
}
}
}
def returnError(error:String) = {
println(s"returning error ${error}")
Future {
/*
note the distintion between langs and messages. Lang means languages this application supports
eg English, French
Messages are the messages defined per language. Eg app.title is a message defined inn English. It might not be defined in French
*/
/*logger.trace("langs array"+langs.availables) //languages available
logger.trace("app.title: "+messagesApi.isDefinedAt("app.title")(langs.availables(0)))//pick the first language and see taht app.title is defined in it
logger.trace("error: "+messagesApi.isDefinedAt("error.incorrectBodyType")(langs.availables(0)))//see taht error.incorrectBodyType is defined in the 1st language
*/
Ok(Json.toJson(JsonResultError(error)))
}/*TODOM - Standardise error messages. Use as constants*/
}
def isUserConfirmed(user:User):Boolean = user.profile.internalProfileDetails.get.confirmed
def sendConfirmationTokenForUser(user:User) = {
val userKeys = generateUserKeysFromUserProfile(user.profile)
//for this user, create a token which could be sent in the email for verification
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
logger.trace(s"saving token ${userToken}")
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
logger.trace(s"user token save future ${userTokenSaveFuture}")
for(userTokenOption <- userTokenSaveFuture) yield {
logger.trace(s"user token ${userTokenOption}")
userTokenOption match {
case Some(userToken) => {
val signupEmailOption:Option[SignupEmail] = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail:SignupEmail) =>{
val email = mailerService.sendEmail(signupEmail.subject, signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
println(s"sent email message ${email}")
email
//Ok(Json.toJson(JsonResultSuccess(messagesApi("success.userSignupConfirmationEmailSent")(langs.availables(0)))))
}
case None =>{
logger.trace("unable to create html response for email confirmation")
//InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
throw EmailMessageCreationException("errorInCreatingHTML", new Throwable("errorInCreatingHTML"))
}
}
}
case None => {
logger.trace("error in adding token")
throw EmailTokenSaveException("emailTokenSaveException", new Throwable("emailTokenSaveException"))
//Ok(Json.toJson(JsonResultError("user not added"))) //Todom - this is misleading as user is added but token isn't
}
}
}
}
def addUserToDatabase(user:User) = {
println(s"saving user ${user}")
val saveUserFuture:Future[Option[User]] = userRepo.save(user)
for(userOption <- saveUserFuture) yield {
userOption match {
case Some(user) => {
logger.trace("user added successfully "+user)
user
}
case None => throw new NewUserAdditionException("newUserAdditionError", new Throwable("newUserAdditionError"))
}
}
}
def addNewUser(profile:UserProfile) = {
logger.trace(s"new user sign up request with profile ${profile}")
////NOTE - //salt is empty for BCryptSha256PasswordHasher. The 'hash' method of BCryptSha256PasswordHasher does not return the salt separately because it is embedded in the hashed password.
//should creation of passwordInfo be moved to UserRepo? Not sure.
//A profile associated with the credentials provider stores a Silhouette PasswordInfo object holding the hashed password
//val passwordInfo:PasswordInfo = userRepo.passwordHasher.hash(profile.externalProfileDetails.password.get)
val passwordInfo: PasswordInfo = userRepo.hashPassword(profile.externalProfileDetails.password.get)
//logger.trace("password info is ",passwordInfo)
val bucketId = utilities.bucketIDFromEmail(profile.externalProfileDetails.email)
val loginInfo = LoginInfo(/*provider id eg "credentials"*/ CredentialsProvider.ID, /*provider Key eg email*/ profile.externalProfileDetails.email)
val newUser: User = User(
utilities.getUniqueID(), //UUID.randomUUID(),
UserProfile(Some(InternalUserProfile(loginInfo, bucketId, false, Some(passwordInfo))),
profile.externalProfileDetails))
logger.trace("adding new user" + newUser)
addUserToDatabase(newUser)
}
def saveProfileAndPortfolio(profileAndPortfolio:ExternalUserProfile) = {
logger.trace(s"saving external profile and portfolio ${profileAndPortfolio}")
val savedProfileAndPortfolioOptionFuture = userProfileAndPortfolioRepo.save(profileAndPortfolio)
for(savedProfileAndPortfolio <- savedProfileAndPortfolioOptionFuture) yield {
savedProfileAndPortfolio match {
case Some(profileAndPortfolio) => profileAndPortfolio
case None => throw ProfileAndPortfolioAdditionException("profileAndPortfolioAdditionException",new Throwable("profileAndPortfolioAdditionException"))
}
}
}
def createUserProfileAndPortfolioInformation(user:User) = {
val profile = user.profile
val initialPortfolio = user.profile.externalProfileDetails.portfolio //Some(TagsOfInterestToAUserAPI(Set(),Set(),Set()))
val profileAndPortfolio = profile.externalProfileDetails.copy(portfolio = initialPortfolio)
saveProfileAndPortfolio(profileAndPortfolio)
}
def signupUser = silhouette.UserAwareAction.async {
implicit request => {
logger.trace(s"received request ${request}")
val jsonBody = getJsonBody(request.body)
/*
TODOM - testcase - check what happens if some other body type is sent.
*/
jsonBody match {
case Some(json) => { //got json in message body.
//TODOM - convert to pretty print only if logger level is trace
val readableString: String = Json.prettyPrint(json)
logger.trace(s"received Json ${readableString}")
val userProfile = getUserProfileFromBody(json)
userProfile match {
case Some(profile) => { //json conforms to UserProfile.
logger.trace(s"received correct profile structure ${profile}")
val userKeys = generateUserKeysFromUserProfile(profile)
val res = for{isNewUser <- findIfUserIsNewOrExisting(userKeys) //this will throw error if user is duplicate
newUserDetails <- addNewUser(profile)
profileAndPortfolioInfo <- createUserProfileAndPortfolioInformation(newUserDetails)
confirmationEmail <- sendConfirmationTokenForUser(newUserDetails)} yield {
Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))
}
res.recover {
case exception: DuplicateUserException => Ok(Json.toJson(JsonResultError(messagesApi("error.duplicateUser")(langs.availables(0)))))
case exception: UnconfirmedUserException => {
sendConfirmationTokenForUser(exception.user)
Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))
}
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case exception:ProfileAndPortfolioAdditionException => Ok(Json.toJson(JsonResultError("user not added")))
case _:EmailMessageCreationException => Ok(Json.toJson(JsonResultError("user not added")))
case _: EmailTokenSaveException=> Ok(Json.toJson(JsonResultError("user not added")))
case x => {
logger.error(s"unknown exception ${x}")
InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
res
}
//Json doesn't conform to UserProfile
case None => Future {
logger.trace("invalid profile structure")
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyStructure")(langs.availables(0))))) } /*TODOM - Standardise error messages. Use as constants*/
}
}
//message body is not json. Error.
//TODOM - langs contains array of lang. picking the 1st one but would need too pick based on locale.
//langs.availables(0) maps to array defined in application.conf eg. - play.i18n.langs = [ "en", "en-US", "fr" ]
case None =>{
logger.trace("incorrect body type")
returnError(messagesApi("error.incorrectBodyType")(langs.availables(0)))
}
}
}
}
The most obvious change is to put code in functions. By breaking up the logic into functions with meaningful names you make the code much clearer to read. If you define them as local functions within the method they can access context from the method without it having to be passed in as parameters.
Put all the error returns in static vals outside the method rather than creating them each time.
Consider using Option.fold rather than match:
option.fold(error){ result =>
// Further code
}
Remove the {} after case x =>, they are not required.
The first case in case x => { x match { is redundant.
And don't worry about performance until you can prove that the performance of this section of code has a material effect on the behaviour of the application.
One problem is that you are not using the Failure case of Future to indicate failure so you can't use map/flatMap to chain operations that might fail.
Create your own subclass of Exception and have the methods return Future.failed(MyException(error)) on failure rather than Future(error)
Then do this
val res =
for {
res1 <- futureAction1
res2 <- futureAction2(res1)
res3 <- futureAction3(res2)
} yield {
res3
}
and finally
res.recover{ case MyException(err) => err }
The for will stop at the first Failure return value, and the recover will turn this into the appropriate Success value.

Play framework ignore following map/flatmap

Is there a way to ignore the following map/flatmap's without failed?
This is what I have:
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Future.failed(new DeleteFailedException)
// Can I return a `NotFound("...")` here instead of failed?
} else {
Future.successful(s.get)
}
}
.map{s =>
serverService.delete(s)
}.map{_ =>
Ok(Json.toJson(Map("success" -> "true")))
}
}
When I would return a NotFound("...") in the flatMap the following map would still be executed. Is there a way to ignore the following map/flatmap's?
Think so should be fine (I assumed that findByIdAndUserId returns Future[Option[_]], not an Option[_] as you answered in comment). In my approach I also removed usage of get and unnecessary map
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
request.user.flatMap(_.id).fold {
Future.successfull(NotFound("no user"))
} {userId =>
serverService.findByIdAndUserId(serverId, userId).map {
case None =>
NotFound("no server")
case Some(s) =>
serverService.delete(s)
Ok(Json.toJson(Map("success" -> "true")))
}
}
}
Instead of doing a Future.failed with an exception. you can return an Either. The good thing about either is that you can pattern match on it and then construct the appropriate http response.
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Left(new DeleteFailedException)
} else {
Right(s.get)
}
}
.map{s => s match {
case Left(notFound) =>
// construct the Json for failure
case Right(s) =>
serverService.delete(s)
// construct json for success.
}}
}

Combining/chaining futures in scala play framework async action

I'm a scala newbie trying to write a Rest Api using play framework. I have the following 3 data access methods
getDataDict: (dsType:String, name:String) => Future[Option[DatasetDictionary]]
getDatasetData: (DatasetDictionary) => Future[List[DatasetData]]
getMetadata: (DatasetDictionary) => Future[List[Metadata]]
I need to use these 3 methods to get the result of my async action method.
def index(dstype:String, name:String, metadata:Option[Boolean]) = Action.async{
/*
1. val result = getDataDict(type, name)
2. If result is Some(d) call getDatasetData
3.1 if metadata = Some(true)
call getMetadata function
return Ok((dict, result, metadata))
3.2 if metadata is None or Some(false)
return Ok(result)
4. If result is None
return BadRequest("Dataset not found")
*/
}
I got the steps 1 and 2 working as follows
def index1(dsType:String, dsName: String, metadata:Option[Boolean]) = Action.async {
getDataDict(dsType, dsName) flatMap {
case Some(x) => getDatasetData(x) map (x => Ok(Json.toJson(x)))
case None => Future.successful(BadRequest("Dataset not found"))
}
}
I'm stuck at how to get the metadata part working.
First of all, it is not very clear (d, result, x) what you really want to return. Hopefully I guessed it correctly:
def index(dstype:String, name:String, metadata:Option[Boolean]) = Action.async {
getDataDict(dstype, name) flatMap {
case Some(datasetDictionary) =>
getDatasetData(datasetDictionary) flatMap { datasetDataList =>
if (metadata == Some(true)) {
getMetadata(datasetDictionary) map { metadataList =>
Ok(Json.toJson((datasetDictionary, datasetDataList, metadataList)))
}
} else {
Future.successful(Ok(Json.toJson(datasetDataList)))
}
}
case None => Future.successful(BadRequest("Dataset not found"))
}
}

In Scala Play and Slick, how to get request to finish before sending response

In Scala Play and Slick, I wish to send a OK response only after a record has been created in the database, so far I've got:
def createItem = Action(BodyParsers.parse.json) {
request => {
val result = request.body.validate[Item]
result.fold(
invalid => {
val problem = new Problem(BAD_REQUEST, "Invalid Item JSON", invalid.toString)
returnProblemResult(BadRequest, problem)
},
item => {
service.create(item)
// TODO check for success before sending ok
Ok.as(ContentTypes("DEFAULT"))
}
)
}
}
And inside the service.create method:
def create(item: Item): Future[Unit] = {
exists(item.id).map {
case true =>
case _ => db.run(Item.table += cc)
}
}
Currently, the OK response get sent even if no new item is created. I would like it to only return OK if an item is created. If the item already exists, or if there're other errors (e.g. database errors), the createItem method should know what kind of problem occurred and return a Problem result with error message.
Try this:-
For service, change the method to
def create(item: Item): Future[Int] = {
exists(item.id).map {
case true => 0
case _ => db.run(Item.table += cc)
}
}
In controller, do
service.create(item).map {
case i:Int=> Ok.as(ContentTypes("DEFAULT"))
}.recoverWith {
// To handle the error in the processing
case ex: Throwable => InternalServerError(ex)
}

Handle services returning Try values in Spray

I am working on a codebase where calling my Spray API need to synchronously call a service that returns a Try which Spray need to format and return over HTTP.
My initial attempt looked like this :
// Assume myService has a run method that returns a Try[Unit]
lazy val myService = new Service()
val routes =
path("api") {
get {
tryToComplete {
myService.run()
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
tryToComplete {
myService.run(num)
}
}
}
def tryToComplete(result: => Try[Unit]): routing.Route = result match {
case Failure(t) => complete(StatusCodes.BadRequest, t.getMessage)
case Success(_) => complete("success")
}
However this caused myService.run() to be called when the application started. I am not sure why this method was called as there was no HTTP call made.
So I have two questions :
Why is my service being called as part of initialising the routes?
What is the cleanest way to handle this case? Imagine that there are a few other end points following a similar pattern. So I need to be able to handle this consistently.
Even though you have result parameter as call-by-name, it'll immediately get evaluated as you're doing
result match {
For it not to get evaluated, it has to be within the complete, ie your code should look something like (haven't tried to compile this):
def tryToComplete(result: => Try[Unit]): routing.Route = complete {
result match {
case Failure(t) => StatusCodes.BadRequest, t.getMessage
case Success(_) => "success"
}
}
The way I solved this was do do the following :
lazy val myService = new Service()
val routes =
path("api") {
get {
complete {
handleTry {
myService.run()
}
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
complete {
handleTry {
myService.run(num)
}
}
}
}
private def handleTry(operation: Try[_]):HttpResponse = operation match {
case Failure(t) =>
HttpResponse(status = StatusCodes.BadRequest, entity = t.getMessage)
case Success(_) =>
HttpResponse(status = StatusCodes.OK, entity = successMessage)
}