At first, when I got introduced to case classes in Scala, I thought, why then hell does Scala have another way of creating classes? My initial google search taught me that case classes are classes specifically meant for holding data with minimum ceremony.
The java way of doing this would be to (Usual way of creating model classes);
Define a class with bunch of private fields.
Write constructor with all these fields as the parameters.
Define getters and setters for each of the fields.
Make the class extend the Serializable interface.
Scala does this all underneath the hood for you if you create a case class. That is right! case class is just Java classes on steroids. Well, since we are talking about Scala, how is it different from normal classes in Scala?
The fun thing here is case classes can also be considered as Scala classes on steroids, how you might ask. Case classes, would also do the following things besides just creating a class;
Creates the class along with a companion object.
A default toString method that includes all the fields and not the annoying @ and hex strings.
A copy method that allows to create a new copy of the object with the exact same fields.
equals and hashCode methods would be overridden will be dependent on the fields instead of the meaningless object reference. This helps a lot with the objects being able to be added into List and Maps.
Finally, if your case class does not require any fields but just methods, we can go ahead and just create a case object directly.
So, I hope its now a bit more clear as to why the hell is there another way to create classes and objects in Scala. Use them exactly by knowing where and when to use them.
I guess my recent love for scala is quite evident lately. I began to fall in love with the conciseness and expressive nature of scala. But more than that, there is one thing that I am currently infatuated with; implicits – The magic keyword in scala.
Scala uses implicit's heavily to improve the conciseness of many design patterns that are native to functional programming and hence plays a key role in the core design of the language and many libraries in the community.
Let’s get started on how to put implicits into action and create an extension method to a class using implicit‘s.
So, what is this extension method in first place?
Extension methods are like extending a feature to a class that is otherwise in-extensible. Example of an extension method would be able to extend the functionality of Int class by creating a method that takes in a single digit as a number and returns the digit as a String. Something similar to the snippet shown below.
1.digitToWord() // This is an extension method on Int class
Clearly, this is not possible in statically typed language as the language would not allow you to add methods to the standard library and would fail to compile.
But scala has this magic language feature called implicit specially for performing a trick like this. Let us see how that is done.
implicit class ExtendedInt(val num: Int) {
def digitToWord: String = {
num match {
case 1 => "One"
case 2 => "Two"
case 3 => "Three"
case 4 => "Four"
case 5 => "Five"
case 6 => "Six"
case 7 => "Seven"
case 8 => "Eight"
case 9 => "Nine"
case 0 => "Zero"
case _ => "Not a valid single digit"
}
}
}
Scala allows you to create an implicit class which has a method that returns a String. When we define this in the scope, scala compiler keeps track of this transformation from Int to String in the context and tries to use it whenever appropriate.
Scala compiler will automatically check to see if its able to find one unique implicit that can take in an Int, construct an ExtendedInt and call digitToWord(). Since we already defined our implicit class Extendednt to take an Int to create an instance of ExtendedInt, compiler silently performs the following transformation for you.
new ExtendedInt(1).digitToWord() // After the implicit magic worn out
Cool isn’t it? Implicit is a complicated feature and would sound like something which makes the code very difficult to debug. Yes, that is true! Implicits are a very powerful feature indeed and often face a lot of criticisms. One has to use implicits with a word of caution and careful enough to stay responsible because, as Spiderman says,
This post is a quick tutorial on how to use Scala for interacting with Cassandra database. We will look into how to use phantom library for achieving this. phantom is a library that is written in Scala and acts as a wrapper on top of the cassandra-driver (Java driver for Cassandra).
The library phantom offers a very concise and typesafe way to write and design Cassandra interactions using Scala. In this post, I will be sharing my 2 cents for people getting started in using this library.
Prerequisites
Before getting started, I would like to make a few assumptions to keep this post concise and short as much as possible. My assumptions are that;
You know what Cassandra is.
You have Cassandra running locally or on a cluster for this tutorial.
You have a piece of basic knowledge of what is Scala and sbt.
In this post, let’s write a simple database client for accessing User information. For simplicity sake, our User model would just have id, firstName, lastName and emailId as its fields. Since cassandra is all about designing your data-model based on your query use-case, we shall have two use-cases for accessing data and hence shall define two tables in cassandra.
The tables would have the following schema.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Make sure your database has this schema configured before proceeding further.
Step 0: Add phantom library to the dependencies
Let us start by creating a simple sbtscala project and add the following dependency to build.sbt file to the libraryDependencies.
"com.outworkers" % "phantom-dsl_2.12" % "2.30.0"
Step 1: Define your model class
In cassandra, it is totally acceptable to denormalize the data and have multiple tables for the same model. phantom is designed to allow this. We define one case class as the data holder and several model classes that correspond to our different use cases. Let us go ahead and define the model and case classes for our User data-model.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Now, we define the tables that correspond to the tables in Cassandra. This allows phantom to construct the queries for us in a typesafe manner.
The following are the use cases for which we would need clients;
To access the data using the user id. (user_by_id table)
To access the data using the user’s first name. (user_by_first_name table)
We create appropriate models that reflect these tables in cassandra and define low-level queries using phantom-dsl to allow phantom to construct queries for us so we don’t have to write any cql statements ever in our application.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
You can access the state of the project until this step in GitHub here.
Now that we have our models and low level queries defined, we need to design an effective way to manage our session and database instances.
Step 2: Encapsulate database instances in a Provider
Since we have interactions with multiple tables for a single model User, phantom provides a way to encapsulate these database instances at one place and manage it for us. This way, our implementations won’t be scattered around and will be effectively managed with the appropriate session object.
So, in this step, we define an encapsulation for all the database instances by extending phantom‘s Database class. This is where we create instances for the models we defined in the above step.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Notice that we also defined a trait extending DatabaseProvider[UserDatabase] in the above snippet. We will in the next step discuss how this trait is useful.
You can access the state of the project until this step here.
Step 3: Orchestrate your queries using DatabaseProvider
Now, that you have your model and database instances in place, exposing these methods may not be a good design. What if you need some kind of data-validation/data-enrichment before accessing these methods. Well, to be very specific in our case, we would need to enter data into two tables when a User record is created. To serve such purposes, we need an extra layer of abstraction for accessing the queries we defined. This is the exact purpose of DatabaseProvider trait.
We define our database access layer (like a service) by extending the traitDatabaseProvider and orchestrate our low-level queries so that the application can access data with the right abstraction.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
You can see that we were able to combine both the inserts (to user_by_id & user_by_first_name) in one method call. We could have definitely, done some sort of validation or data-transformation if we wanted to here.
You can access the state of the project until this step here.
Step 4: Putting everything together
We are all set to the database service we created so far. Lets see how this is done.
We start out by creating our CassandraConnection instance. This is how we can inject out cassandra configuration into phantom and let it manage the database session for us.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Here we are using cassandra installed locally, hence we used ContactPoint.local. Ideally in real-world we would have to use ContactPoints(hosts).
Then we create our database encapsulation instance and the service object.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Now, it is as simple as just calling a bunch of methods to test out if our database interactions work.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
We might have had a lot of constructs like Database, DatabaseProvider, etc, bundled with phantom-dsl. But in my opinion, this is something that makes it more than just yet another scala dsl-library. It is because of these design constructs, phantom implicitly promotes good design for people using it.
Non-blocking external calls (web service) calls often would return a Future which will resolve to some value in the future. Testing such values in Java is pretty sad. Testing futures in java will usually involve performing a get()to resolve a future and then perform the assertions. I am pretty new to scala and was curious if I would have to do the same. Fortunately with Scala this is not the case. I will walk you thru my experience with an example and lets see how we do it in scala. In this post, we will be using scala-test library as our testing library and use FlatSpec type of testing.
Let’s start by defining our class to test.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The above code is a simple scala class that will simulate a dummy network call by just sleeping for an arbitrary amount time.
lengthOfTheResponse(url: String): Future[Int] is a function that waits for 5 seconds and returns the number 10.
Similarly, statusOfSomeOtherResponse(url: String): Future[Int]is another function that waits for 1 second and returns the number 200. Let’s see how we write tests for these function.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The above example looks great but will fail with the following error.
A timeout occurred waiting for a future to complete. Queried 10 times, sleeping 15 milliseconds between each query.
The default behavior of whenReady is to keep polling 10 times by waiting for 15 milliseconds in between to see if the future is resolved. If not, we get the above error. This clearly is not a consistent solution as we are depending on a finite time and polling to figure out the future result. Sure, we can configure the above whenReady method with a higher timeout and retry but there are better ways to test. Let’s see another approach.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The above example will first execute the lengthOfTheResponse("testurl") resolve the future and yield the result Intinto resvariable. We then assert the result. But there is a gotcha we need to keep in mind. It is important that we use the AsyncFlatSpec instead of FlatSpec.
We can also do more fluent version with multiple network calls.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This way, we can kind of chain the futures in the order that we want to test and perform assertions accordingly. This will ensure that the futures resolve appropriately.
Let me know what you think of this and would certainly like to learn other approach to test futures.