Toby Sullivan
Lead Engineer at Hootsuite
Initialize a skeleton service in one command
activator new favourites-service play-scala
You can run it immediately
activator run
DEMO: App skeleton
Find the code on Github
Our new service will support these operations
# Favourite a studio POST /users/:userId/favouriteStudios/:studioId # Unfavourite a studio DELETE /users/:userId/favouriteStudios/:studioId # Get a list (index) of all studios a user has favourited GET /users/:userId/favouriteStudios # Check if a studio has been favourited # Responds 200 if yes, otherwise 404 GET /users/:userId/favouriteStudios/:studioId
/* app/controllers/FavouriteStudios.scala */
package controllers
import play.api.mvc.{Controller, Action}
object FavouriteStudios extends Controller {
def add(userId: Int, studioId: Int) = Action {
// TODO: Add favourite
Ok(???)
}
}
# conf/routes POST /users/:userId/favouriteStudios/:studioId controllers.FavouriteStudios.add(userId: Int, studioId: Int)
/* app/models/FavouriteStudio.scala */
import play.api.libs.json._
import play.api.libs.functional.syntax._
object FavouriteStudio {
implicit val favouriteStudioWrites: Writes[FavouriteStudio] = (
(JsPath \ "userId").write[Int] and
(JsPath \ "studioId").write[Int]
)(unlift(FavouriteStudio.unapply))
}
DEMO: CuRL our new endpoint
Find the code on Github
# conf/application.conf
db.default.driver=com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://10.0.0.3:3306/favourites-svc"
db.default.user="favourites-svc"
db.default.password="s0meP@ssW0rd"
CREATE TABLE `favouriteStudio` (
`userId` INT(7) NOT NULL,
`studioId` INT(7) NOT NULL,
PRIMARY KEY `idx_user_studio` (`userId`, `studioId`),
INDEX `idx_user` (`userId`)
);
/* app/models/dao/FavouriteStudioDAO.scala */
object FavouriteStudioDAO {
def create(favourite: FavouriteStudio) =
DB.withConnection { implicit c =>
SQL(
"""
| INSERT IGNORE INTO `favouriteStudio` (`userId`, `studioId`)
| VALUES
| ({userId}, {studioId});
""".stripMargin).on(
"userId" -> favourite.userId,
"studioId" -> favourite.studioId
).executeInsert()
}
}
DEMO: CuRL data to the DB
Find the code on Github
DEMO: Repeat implementation for other CRUD endpoints
Find the code on Github
Improve response times in a single line
/* app/controllers/FavouriteStudios.scala */
def find(userId: Int, studioId: Int) =
Cached("find_" + userId + "_" + studioId) {
Action {
val oFavourite = FavouriteStudio.find(userId, studioId)
oFavourite match {
case None => NotFound(Json.obj("error" -> "NOT_FOUND"))
case Some(favourite) => Ok(Json.obj("result" -> favourite))
}
}
}
DEMO: See the performance difference
Find the code on Github
/* app/actors/NotificationActor.scala */
object NotificationActor {
def props: Props = Props(new NotificationActor)
}
class NotificationActor extends Actor {
def receive = {
case favourite: FavouriteStudio =>
notifyFriends(favourite)
}
private def notifyFriends(favourite: FavouriteStudio): Unit = {
// TODO: Lookup list of friends and send a push notification
}
}
/* app/models/FavouriteStudio.scala */
def addFavourite(userId: Int, studioId: Int): FavouriteStudio = {
val favourite = FavouriteStudio(userId, studioId)
FavouriteStudioDAO.create(favourite)
// Send a message to the notification actor describing the added favourite
notificationActor ! favourite
favourite
}
/* app/models/dao/FriendDAO.scala */
def index(userId: Int): Future[List[Friend]] = {
val holder: WSRequestHolder = WS.url(friendsServiceUrl)
.withQueryString("userId" -> userId.toString)
// Execute the web service request and get a future of a response
val fResponse = holder.get()
fResponse.map { response =>
// The response will be JSON so parse out the list of friends' IDs
val friendIds: List[Int] = (response.json \ "result").as[List[Int]]
// Map the list of IDs to a list of friends
friendIds.map(Friend.apply)
}
}
DEMO: Tying it all together.
Find the code on Github
Microservices by Martin Fowler
(martinfowler.com)
Building a REST API using Play by Saba El-Hilo
(code.hootsuite.com)
If you found this interesting, we're hiring.
tobyjsullivan.github.io/applicative-slides