Hi Greg,
Am Freitag, 5. Oktober 2012, 18:02 schrieb pelagic:
> Thanks for you input, I really appreciate you sharing your experience
> on working with RESTful services.
> Could you ellaborate more on what kind of problems you were having
Well, what you might be tempted to do if you want to map a model class
to a REST resource, is the following:
class Article extends LongKeyedMapper[Article] with IdPK {
lazy val data: NodeSeq = (fetch XML data from RESTful API using id)
def title = data \ "title" text
def body = data \ "body" text
def numberOfCcomments = asLong(data \ "comments" text) getOrElse 0L
// ...
}
And then in your source code, you just treat that class as a normal
mapper class, as in:
Article.find(By(Article.id, 7L)).get.title
and let's assume the ".get" is not a problem because you know there is
an entry with id 7, then this will work fine – if your backend is up and
running. If it takes a long time to respond, then the statement blocks,
and if it returns a response code that your code is not prepared to deal
with (for example, Http(myRequest as_str) in dispatch 0.8.x), then you
will get an exception. If the distributedness of your data store is
abstracted away and you treat Article as any other Mapper/Record class,
then you easily step into that trap.
In a second project where I was more aware of that issue, I defined my
REST-mapped model classes more like this:
class Product extends LongKeyedMapper[Product] with IdPK {
lazy val data: Box[NodeSeq] = tryo {
(try to fetch XML data using id)
}
def title = data.map(_ \ "name" text).getOrElse("missing title")
def price = asDouble(data.map(_ \ "list_price" text).getOrElse("0.0")
).getOrElse(0.0)
// ...
}
Here, I still have the problem of blocking, but I get around this by
LazyLoading snippets that show product data as much as possible. On the
other hand, if the service is down or returns an unexpected response
code, my page will still work fine, just display "missing title"
everywhere. Just need to make sure that nobody buys three items "missing
title" with a total price of 0.0 EUR ;-)
Now I'm also wondering what the best way to model this situation
actually is. Right after I sent my last email, I discovered that in
dispatch 0.9.x, network delays and failures are dealt with by wrapping
responses to HTTP requests into Promise objects. So maybe (!) one of the
following is the "right" way to do it?
class ProductOption extends LongKeyedMapper[ProductOption] with IdPK {
lazy val data: Promise[Option[xml.Elem]] =
Http(myRequest OK as.xml.Elem).option
def title: Promise[Option[String]] = data.map(_.map(_ \ "name" text))
def price: Promise[Option[Double]] = data.map(_.map(_ \ "price"
text).flatMap(asDouble))
}
OR:
class ProductEither extends LongKeyedMapper[ProductEither] with IdPK {
lazy val data: Promise[Either[String, xml.Elem]] = {
val res = Http(myRequest OK as.xml.Elem).either
for (exc <- res.left) yield "ERROR: " + exc.getMessage
}
def title: Promise[Either[String, String]] = {
def extractTitle(xml: scala.xml.Elem): Either[String, String] = {
val nodes = xml \ "title" map (_.text)
nodes.headOption.toRight("invalid data")
}
for {
xml <- data.right
t <- Promise(extractTitle(xml)).right
} yield t
}
def price: Promise[Either[String, Double]] = {
def extractPrice(xml: scala.xml.Elem): Either[String, Double] = {
val nodes = xml \ "price" map (_.text) flatMap (asDouble)
nodes.headOption.toRight("invalid data")
}
for {
xml <- data.right
t <- Promise(extractPrice(xml)).right
} yield t
}
// ...
}
Both of these methods neither block nor do they throw exceptions if
something goes wrong in the backend. In particular the latter method
always keeps the error message, in case you want to log or display it.
However, you haven't dealt with updating or saving your data yet... ;-)
Tobias