We implement RESTful Web Service on Scala
Last week on Habré there were as many as two articles on the implementation of RESTful web-services in Java. Well, let's not lag behind and write our own version on Scala, with monads and applicative functors. Experienced Scala developers are unlikely to find anything new in this article, and Django fans will generally say that they have this functionality out of the box, but I hope that it will be interesting for Java developers and just curious to read.
We take the task from the previous article as the basis , but we will try to solve it so that the solution code fits on the screen. At least a 40-inch and fifth font. In the end, in the XXI century it should be possible to solve simple tasks without megabytes of xml-configs and dozens of abstract factories.
For those who do not want to follow the links I’ll clarify: we are going to implement the simplest RESTful service to access the customer database. Of the necessary functionality is the creation and deletion of objects in the database, as well as pagination of a list of all clients with the ability to sort by different fields.
As the bricks from which we will build a house, we take:
In the course of the article, I will try to give enough explanations so that the code can be understood by people who are not familiar with Scala, but I do not promise that I will succeed.
First we need to decide on a data model. Squeryl allows you to specify the model in the form of a regular class, and in order not to write too much, we will use the same class for subsequent serialization in JSON.
Fields of type
Annotation
Those who have worked with JDBC know that the first thing to do is to initialize the database driver class. Let's not deviate from this practice:
In the first line, we load the JDBC driver, and in the second we tell the Squeryl library which connection factory to use. As a database we use light and fast H2 .
Now the turn of the scheme has come:
First, we indicate that our database contains one table that corresponds to the class
To begin, let's initialize the JSON parser so that it can work with data types accepted in Scala:
Now we define two functions for turning JSON strings into objects:
The function
It is worth noting that it was not necessary to specify the type of the return value in both functions: the compiler had enough data to deduce the type without the help of a programmer, but the explicitly specified type sometimes facilitates human understanding of the code.
The reverse process - turning an object
In the future, we will simply return objects of the type
Another small touch is the generation of new customer identifiers. The easiest, though not always the most convenient way, is to use the UUID:
Now that most of the preparatory work has been done, we can proceed directly to the implementation of the web service. I will not go into the details of the Unfiltered library, I’ll just say that the simplest way to use it is:
Our service will have two entry points:
In the first line, we indicate that this code only wants to process the URL of the view
GET and DELETE requests are handled similarly.
In the second half of the handler serving requests to
These functions will be used to create
The part related to the POST request does not carry anything new, but then we have to process the request parameters and two strange symbols appear:
That's all, all that remains is to start the server
and you can pick up curl to check that everything works.
In my opinion, the resulting web service code is compact and fairly easy to read, and this is a very important property. Naturally, it is not ideal: for example, you probably should have used
The code, as expected, is posted on GitHub , and it differs from the one given in the article only by the presence of import-s and sbt-script for assembly.
I almost forgot - at the very beginning of the article I promised that there would be monads and other evil spirits in the web service. So,
And finally, if you are in Kharkov or Saratov and want to develop interesting things using Scala and Akka, write - we are looking for competent specialists.
Training
We take the task from the previous article as the basis , but we will try to solve it so that the solution code fits on the screen. At least a 40-inch and fifth font. In the end, in the XXI century it should be possible to solve simple tasks without megabytes of xml-configs and dozens of abstract factories.
For those who do not want to follow the links I’ll clarify: we are going to implement the simplest RESTful service to access the customer database. Of the necessary functionality is the creation and deletion of objects in the database, as well as pagination of a list of all clients with the ability to sort by different fields.
As the bricks from which we will build a house, we take:
- Scala is not even a brick, but rather a foundation,
- Unfiltered is a great library for handling HTTP requests,
- Squeryl - a library for database queries,
- Jackson is a library for working with JSON, originally written for Java, but with a bang it copes with Scala types,
- Scalaz is a library that allows you to write different funny characters in the code like ⊛, ↦, or ∃, and at the same time implements useful abstractions such as applicative functors, monoids, semigroups, and Kleisley arrows. True, I have not yet had to use the latter, but this is most likely due to the fact that I have not yet reached the required degree of functional enlightenment.
In the course of the article, I will try to give enough explanations so that the code can be understood by people who are not familiar with Scala, but I do not promise that I will succeed.
To battle
Data model
First we need to decide on a data model. Squeryl allows you to specify the model in the form of a regular class, and in order not to write too much, we will use the same class for subsequent serialization in JSON.
@JsonIgnoreProperties(Array("_isPersisted"))
case class Customer(id: String,
firstName: String,
lastName: String,
email: Option[String],
birthday: Option[Date]) extends KeyedEntity[String]
Fields of type
Option[_]correspond to nullable columns of the database. Such fields can take two kinds of values: Some(value)if there is a value, and Noneif it is not. Use Optionallows you to minimize the chances of appearing NullPointerExceptionand is common practice in functional programming languages (especially those in which nullthere is no concept at all). Annotation
@JsonIgnorePropertiesexcludes certain fields from JSON serialization. In this case, it was necessary to exclude the field _isPersistedthat Squeryl added.Initializing a Database Schema
Those who have worked with JDBC know that the first thing to do is to initialize the database driver class. Let's not deviate from this practice:
Class.forName("org.h2.Driver")
SessionFactory.concreteFactory =
Some(() => Session.create(DriverManager.getConnection("jdbc:h2:test", "sa", ""), new H2Adapter))
In the first line, we load the JDBC driver, and in the second we tell the Squeryl library which connection factory to use. As a database we use light and fast H2 .
Now the turn of the scheme has come:
object DB extends Schema {
val customer = table[Customer]
}
transaction { allCatch opt DB.create }
First, we indicate that our database contains one table that corresponds to the class
Customer, and then we execute DDL commands to create this table. In real life, using automatic table creation is usually problematic, but for a quick demonstration it is very convenient. If the tables already exist in the database, it will DB.createthrow an exception, which we, thanks to allCatch opt, will successfully ignore.JSON serialization and deserialization
To begin, let's initialize the JSON parser so that it can work with data types accepted in Scala:
val mapper = new ObjectMapper().withModule(DefaultScalaModule)
Now we define two functions for turning JSON strings into objects:
def parseCustomerJson(json: String): Option[Customer] =
allCatch opt mapper.readValue(json, classOf[Customer])
def readCustomer(req: HttpRequest[_], id: => String): Option[Customer] =
parseCustomerJson(Body.string(req)) map (_.copy(id = id))
The function
parseCustomerJsonactually parses JSON. Through the use of allCatch optexceptions that arose in the process of parsing, will be caught and as a result we get None. The second function,, readCustomeris directly related to the processing of the HTTP request - it reads the request body, turns it into an object of type Customerand sets the field idto the specified value. It is worth noting that it was not necessary to specify the type of the return value in both functions: the compiler had enough data to deduce the type without the help of a programmer, but the explicitly specified type sometimes facilitates human understanding of the code.
The reverse process - turning an object
Customer(or list List[Customer]) into the body of an HTTP response - is also not difficult:case class ResponseJson(o: Any) extends ComposeResponse(
ContentType("application/json") ~> ResponseString(mapper.writeValueAsString(o)))
In the future, we will simply return objects of the type
ResponseJson, and the Unfiltered framework will take care of turning it into the correct HTTP response. Another small touch is the generation of new customer identifiers. The easiest, though not always the most convenient way, is to use the UUID:
def nextId = UUID.randomUUID().toString
HTTP request processing
Now that most of the preparatory work has been done, we can proceed directly to the implementation of the web service. I will not go into the details of the Unfiltered library, I’ll just say that the simplest way to use it is:
val service = cycle.Planify {
case /* шаблон запроса */ => /* код, генерирующий ответ */
}
Our service will have two entry points:
/customerand /customer/[id]. Let's start with the second one:case req@Path(Seg("customer" :: id :: Nil)) => req match {
case GET(_) => transaction { DB.customer.lookup(id) cata(ResponseJson, NotFound) }
case PUT(_) => transaction { readCustomer(req, id) ∘ DB.customer.update cata(_ => Ok, BadRequest) }
case DELETE(_) => transaction { DB.customer.delete(id); NoContent }
case _ => Pass
}
In the first line, we indicate that this code only wants to process the URL of the view
/customer/[id]and binds the passed identifier to the id variable (if the immutable variable can be called that at all). In the following lines, we refine the behavior depending on the type of request. Let us examine, for example, the processing of the PUT method in steps:transaction { ... }: we indicate that for the duration of the body of the handler, a transaction should be opened,readCustomer(req, id): use a pre-written method that reads the request body and returnsOption[Customer]∘: this symbol deserves special attention, in fact it is a synonym for the map operation and allows you to apply some function to the Option content, if this content is,DB.customer.update: the very function we want to apply is updating the entity in the database,cata(_ => Ok, BadRequest): returnsOkifOptionthere is a value orBadRequestif the request could not be parsed and we haveNoneinstead of the client.
GET and DELETE requests are handled similarly.
In the second half of the handler serving requests to
/customer, we will need two auxiliary functions: val field: PartialFunction[String, Customer => TypedExpressionNode[_]] = {
case "id" => _.id
case "firstName" => _.firstName
case "lastName" => _.lastName
case "email" => _.email
case "birthday" => _.birthday
}
val ordering: PartialFunction[String, TypedExpressionNode[_] => OrderByExpression] = {
case "asc" => _.asc
case "desc" => _.desc
}
These functions will be used to create
order bypart of the request and, most likely, rummaging in the bowels of Squeryl, they could be written easier, but this option also suited me. The handler code itself:case req@Path(Seg("customer" :: Nil)) => req match {
case POST(_) =>
transaction {
readCustomer(req, nextId) ∘ DB.customer.insert ∘ ResponseJson cata(_ ~> Created, BadRequest)
}
case GET(_) & Params(params) =>
transaction {
import Params._
val orderBy = (params.get("orderby") ∗ first orElse Some("id")) ∗ field.lift
val order = (params.get("order") ∗ first orElse Some("asc")) ∗ ordering.lift
val pageNum = params.get("pagenum") ∗ (first ~> int)
val pageSize = params.get("pagesize") ∗ (first ~> int)
val offset = ^(pageNum, pageSize)(_ * _)
val query = from(DB.customer) {
q => select(q) orderBy ^(orderBy, order)(_ andThen _ apply q).toList
}
val pagedQuery = ^(offset, pageSize)(query.page) getOrElse query
ResponseJson(pagedQuery.toList)
}
case _ => Pass
}
The part related to the POST request does not carry anything new, but then we have to process the request parameters and two strange symbols appear:
∗and ^. The first (carefully, do not confuse it with a normal asterisk *) is a synonym for flatMapand differs from the mapfact that the function used should also return Option. Thus, we can sequentially perform several operations, each of which either successfully returns a value or returns Nonein case of an error. The second operator is a little more complicated and allows you to perform some operation only if all the variables used are not equalNone. This allows us to sort only if both the column and the direction are specified, and break the result into pages only if both the page number and its size are specified. That's all, all that remains is to start the server
Http(8080).plan(service).run()
and you can pick up curl to check that everything works.
Conclusion
In my opinion, the resulting web service code is compact and fairly easy to read, and this is a very important property. Naturally, it is not ideal: for example, you probably should have used
scala.Eitheror to handle errors scalaz.Validation, and some might not like the use of Unicode operators. In addition, behind the external simplicity, sometimes quite complex operations can be hidden, and to understand how everything works "under the hood" you will have to strain gyrus. Nevertheless, I hope that this article will encourage someone to take a closer look at Scala: even if you fail to apply this language in your work, you will surely learn something new. The code, as expected, is posted on GitHub , and it differs from the one given in the article only by the presence of import-s and sbt-script for assembly.
I almost forgot - at the very beginning of the article I promised that there would be monads and other evil spirits in the web service. So,
flatMap(aka ∗) this is a monadic bind, and the operator ^is directly related to applicative functors. And finally, if you are in Kharkov or Saratov and want to develop interesting things using Scala and Akka, write - we are looking for competent specialists.