6 min to complete
In a previous lesson, we explained how to create a sample Scala application that executes a few basic CQL operations with a ScyllaDB cluster using the Phantom Scala driver. We glossed over the details of how to define our tables and databases. This lesson will delve further into those definitions and explore how our applications should be structured using Phantom’s abstractions.
Modeling Tables with Phantom
The Phantom driver provides facilities for mapping ScyllaDB tables to actual data types we can work with. This approach’s immediate benefit is avoiding “stringly-typed” programming: that is, instead of using CQL strings embedded in our code to describe the operations we perform, we use rich types and functions to construct the operations.
This guarantees, for example, that if we’ve declared the firstName column on a table as a text column, we would only be able to compare it to String values in WHERE predicates.
Before we declare our table, it’s necessary to declare a data type that will hold a row from the table. In our case, we’re using the Mutant case class we’ve seen in the previous lesson. (if you haven’t downloaded the code yet, do so by following the instructions in the section “Set-up a ScyllaDB Cluster” there):
A table can be declared by extending the Table class, as seen in the Mutants.scala file:
We extend the Table class and fix its type parameters: the first type parameter is actually a reference to the table subclass itself (Mutants), and the second is the type of the row. The tableName field can be optionally overridden, like we’ve done, to specify the table’s name in ScyllaDB. By default, Phantom will use the subclass’s name in lowercase letters (mutants, in this case).
Within the class definition, we list several object definitions – each one corresponds to the table’s different columns. The objects extend classes that specify the type of the column: StringColumn, DoubleColumn, UUIDColumn, etc. The TableAliases type in Phantom contains all the possible column types. The objects can also, optionally, extend additional traits that specify properties of the columns. For example, the firstName column is part of the table’s partition key, and as such, it extends the PartitionKey trait.
The Mutants class is purposely defined as abstract, as we will not be instantiating it directly, but rather through a Database class.
Modeling Databases with Phantom
Databases in Phantom are several tables from the same keyspace bundled into one containing class. In our application, we’ve defined the MutantsDatabase class (found in the MutantsDatabase.scala file) as such:
First, note the connector constructor parameter: to construct the database, it must be provided with a connection to the cluster. The class extends the Database class and provides it with the connection.
In the definition of our database, we specify an object for every table we want to make available. The mutants object we define extends the Mutants class we defined earlier and mixes in the Connector trait. This trait injects the database’s connection into the table instance.
Exposing a Higher-level Interface
At this point, we can instantiate the database with a connection as follows:
We could use the db object directly throughout our application, but that’d be sub-optimal, as every module of our application would be able to access any of the tables defined on the database. Not a great way to architect our service! Instead, we want to define higher-level interfaces with focused responsibilities: the MutantsService class is an example of that.
Here’s a part of its definition:
The service uses the database as a constructor parameter, but only exposes specific methods for accessing (and mutating) the data. Funneling all Mutant-related data access through this service ensures we won’t have to refactor too much of our service should we want to change the data representation in ScyllaDB in the future.
In this lesson, we delved into the table and database abstractions provided by Phantom and described their role in structuring our application. We saw how using these abstractions, along with focused service interfaces and classes, leads to well-architected services. You can keep experimenting with these abstractions and continue to make your applications more modular.
*This lesson was written with the help of Itamar Ravid. Itamar is a Distributed systems engineer with a decade of experience in a wide range of technologies. He focuses on JVM-based microservices in Scala using functional programming. Thank you, Itamar!