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"))
}
}
Related
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
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.
}}
}
I am struggling to use flatMap and map with Play framework + Scala. This method has a bunch of other issues, but I am trying to go through them one for each time. The first thing that I cannot figure out how to implement is how to fill a Seq inside nested flatMap and map and return a Json output. Here is my method:
def getRacks(at: String) = Action.async { implicit request: Request[AnyContent] =>
var rackSeq: Seq[Rack] = Seq.empty
var gpuSeq: Seq[Gpu] = Seq.empty
rackRepository.get(Util.toTime(at)).flatMap { resultRack: Seq[RackRow] =>
resultRack.map { r: RackRow =>
gpuRepository.getByRack(r.id).map { result: Seq[GpuRow] =>
result.map { gpuRow: GpuRow =>
gpuSeq = gpuSeq :+ Gpu(gpuRow.id, gpuRow.rackId, gpuRow.produced, Util.toDate(gpuRow.installedAt))
println(gpuRow)
}
}
val rack = Rack(r.id, r.produced, Util.toDate(r.currentHour), gpuSeq)
rackSeq = rackSeq :+ rack
}
println("rackSeq: " + rackSeq)
Future.successful(Ok(Json.toJson(rackSeq)).as(JSON))
}.recover {
case pe: ParseException => BadRequest(Json.toJson("Error on parse String to time."))
case e: Exception => BadRequest(Json.toJson("Error to get racks."))
case _ => BadRequest(Json.toJson("Unknow error to get racks."))
}
}
I was expecting that rackSeq will be filled with GpuRow. but my output is like this:
rackSeq: List(Rack(rack-1,0.2,2018-01-23T14:15:00.79Z,List()))
GpuRow(rack-1-gpu-0,rack-1,0.2,1515867048515)
How to evaluate both lists to the output?
Instead of mutating variables, stay within the context of a Future and perform transformations until you reach the desired result. Assuming the following types...
rackRepository.get(Util.toTime(at)) // Future[Seq[RackRow]]
gpuRepository.getByRack(r.id) // Future[Seq[GpuRow]]
...you could do this instead:
def gpuRowToGpu(gpuRow: GpuRow): Gpu = {
Gpu(gpuRow.id, gpuRow.rackId, gpuRow.produced, Util.toDate(gpuRow.installedAt))
}
def getRacks(at: String) = Action.async { implicit request: Request[AnyContent] =>
rackRepository.get(Util.toTime(at)).flatMap { resultRack: Seq[RackRow] =>
val seqFutRack: Seq[Future[Rack]] = resultRack.map { r: RackRow =>
gpuRepository.getByRack(r.id).map { result: Seq[GpuRow] =>
val gpus = result.map(gpuRowToGpu) // Seq[Gpu]
Rack(r.id, r.produced, Util.toDate(r.currentHour), gpus)
} // Future[Rack]
}
val futSeqRack: Future[Seq[Rack]] = Future.sequence(seqFutRack)
futSeqRack.map(racks => Ok(Json.toJson(racks)).as(JSON))
}.recover {
...
}
}
I got the error
found : scala.concurrent.Future[Option[models.ProcessTemplatesModel]]
required: Option[models.ProcessTemplatesModel]
My function is below
def createCopyOfProcessTemplate(processTemplateId: Int): Future[Option[ProcessTemplatesModel]] = {
val action = processTemplates.filter(_.id === processTemplateId).result.map(_.headOption)
val result: Future[Option[ProcessTemplatesModel]] = db.run(action)
result.map { case (result) =>
result match {
case Some(r) => {
var copy = (processTemplates returning processTemplates.map(_.id)) += ProcessTemplatesModel(None, "[Copy of] " + r.title, r.version, r.createdat, r.updatedat, r.deadline, r.status, r.comment, Some(false), r.checkedat, Some(false), r.approvedat, false, r.approveprocess, r.trainingsprocess)
val composedAction = copy.flatMap { id =>
processTemplates.filter(_.id === id).result.headOption
}
db.run(composedAction)
}
}
}
}
what is my problem in this case?
edit:
my controller function looks like this:
def createCopyOfProcessTemplate(processTemplateId: Int) = Action.async {
processTemplateDTO.createCopyOfProcessTemplate(processTemplateId).map { process =>
Ok(Json.toJson(process))
}
}
Is there my failure?
According to the your code - there are the following issues:
You use two db.run which return futures, but inner future will
not complete. For resolving it you should compose futures with
flatMap or for-comprehension.
You use only one partial-function case Some(_) => for pattern matching
and don't handle another value None.
You can use only one db.run and actions composition.
Your code can be like as:
def createCopyOfProcessTemplate(processTemplateId: Int): Future[Option[ProcessTemplatesModel]] = {
val action = processTemplates.filter(...).result.map(_.headOption)
val composedAction = action.flatMap {
case Some(r) =>
val copyAction = (processTemplates returning processTemplates...)
copyAction.flatMap { id =>
processTemplates.filter(_.id === id).result.headOption
}
case _ =>
DBIO.successful(None) // issue #2 has been resolved here
}
db.run(composedAction) // issue #3 has been resolved here
}
We get rid of issue #1 (because we use actions composition).
I'm trying to figure out how to link multiple asynchronous calls and return a result. I am currently trying to asynchronously user data first, and update user data asynchronously and return result, but it seems like it is not working :(
i used map { result => Ok(result)}, but play still thinks that I am returning an object. any help?
def updateUserData() = Action.async { implicit request =>
updateUserForm.bindFromRequest.fold(
errors => Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(errors, Option(""), Option("")))),
{
case (userData) =>
request.session.get("email") match {
case Some(email) =>
getUser(email, userData.curent_password) map { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
//val e = updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map {result => Ok("")}
// user is valid now update the user data
// call removeAuth to log out
// redirect to home
///Ok (updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result => result})
//Redirect(routes.settings.index()).addingToSession("email" -> email)
} else {
BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option("")))
}
}
}
})
}
The main part that i am having issue is this part. I think it is matter of some syntax. Could someone help?
Thanks
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
The issue is with your types and that they don't match up with the required ones.
.fold has to result in Future[Result] in both branches (the error and the success ones).
In the successful form bind branch you have this:
case (userData) => ... // The ... must evaluate to Future[Result]
Looking at your first operation we see:
request.session.get("email") match {
case Some(email) => ...
}
One big issue here is that the None case is not handled! (but this is does not cause the types not matching up). Having something like the following will solve this: case None => Future.successful(BadRequest(...))
So moving on: in the Some you have the following:
getUser(email, userData.curent_password) map { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
} else {
BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option("")))
}
}
This is where the issue is:
getUser will return with a Future[X] and when you map over it you will have Future[Y] where Y will be what userCheck => ... evaluates to.
In this case the types are totally mixed up, since when you do if(usercheck) on the true branch you have Future[Result] on the false branch you have Result. So the types don't align on both branches which is a big issue and the compiler will infer Any from this.
To fix this, in the false branch create a future: Future.successful(BadRequest(....))
Ok, now that we fixed the most inner type issues, let's start going backwards. Inside we have Future[Result], if we go back one level (before the getUser()) then we will have Future[Future[Result]]. Again this is not what we want, because we need Future[Result].
The solution to this is to flatMap instead of map, because with flatMap when you need to return with the same container type and it flattens it. A quick example to understand this:
Seq(1, 2, 3).flatMap(i => Seq(i, i))
// res0: Seq[Int] = List(1, 1, 2, 2, 3, 3)
In the case of Futures:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
Future(1).flatMap(i => Future(i*2))
// res1: scala.concurrent.Future[Int] = [....]
So we see that we don't have double nesting, but just a single Future.
Going back to your example this would be my updated code that would work better:
def updateUserData() = Action.async { implicit request =>
updateUserForm.bindFromRequest.fold(
errors => Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(errors, Option(""), Option("")))),
{
case (userData) =>
request.session.get("email") match {
case Some(email) =>
getUser(email, userData.curent_password).flatMap { userCheck =>
if (userCheck) {
updateUserOnService(email, userData.f_name, userData.l_name, userData.new_password) map { result =>
Ok("please")
}
} else {
Future.successful(BadRequest(views.html.authenticated.settings.settings_hero(updateUserForm.bindFromRequest.withGlobalError(Messages("error.login", email)), Option(""), Option(""))))
}
}
case None => Future.successful(BadRequest) // FIXME: Implement as you wish
}
})
}