Custom Encoders/Decoders
In case you need to apply a transformation during the extraction process, don’t have a 1-to-1 mapping of case class
names to cassandra table names, or are trying to use a type not included in the ScalaCass library, you can just define a
custom encoder and decoder for any type. We will define a UniqueId
class as an example for how you might customize
it. Let’s say this class will only accept ids less than 15 characters long.
abstract case class UniqueId(id: String)
object UniqueId {
def apply(untestedId: String): UniqueId =
if (untestedId.length > 15) throw new IllegalArgumentException("id must be less than 15 characters long")
else new UniqueId(untestedId) {}
}
You can provide a custom type in 2 ways:
Using forProduct$arity
for case classes
When parsing the entire row into a case class, sometimes it may not be possible to encapsulate specialized logic
using the basic encoders. In these cases, forProduct$arity
can be used as a way to have complete control over
how values are extracted out of/inserted into a row. They can also be used when names in a Cassandra row do not match
the names in the case class. Since it only applies on operations to an entire row, the functions are available
on CCCassFormatEncoder
and CCCassFormatDecoder
. The functions take a format of
forProduct1
/forProduct2
/…forProduct22
, and you choose the one that matches the number of arguments you wish
to extract/insert into a row.
scala> object Wrapper {
| case class SpecialInsert(s: String, i: Int, specialLong: Long)
| object SpecialInsert {
| implicit val ccDecoder: CCCassFormatDecoder[SpecialInsert] =
| CCCassFormatDecoder.forProduct3("s", "i", "special_long")((s: String, i: Int, specialLong: Long) => SpecialInsert(s, i, specialLong))
| implicit val ccEncoder: CCCassFormatEncoder[SpecialInsert] =
| CCCassFormatEncoder.forProduct3("s", "i", "special_long")((sc: SpecialInsert) => (sc.s, sc.i, sc.specialLong))
| }
| }
defined object Wrapper
scala> import Wrapper._ // Wrapper is necessary for this interpreter, and should be excluded in your code
import Wrapper._
And now the SpecialInsert is ready to be used:
scala> val specialInsertStatement = sSession.insert("specialtable", SpecialInsert("asdf", 1234, 5678L))
specialInsertStatement: com.weather.scalacass.scsession.SCInsertStatement = SCInsertStatement(INSERT INTO mykeyspace.specialtable (s, i, special_long) VALUES (asdf, 1234, 5678))
scala> specialInsertStatement.execute()
res4: com.weather.scalacass.Result[com.datastax.driver.core.ResultSet] = Left(com.datastax.driver.core.exceptions.InvalidQueryException: unconfigured columnfamily specialtable)
Renaming is not the only usage of forProduct$arity
, nor is it strictly required to create one for a case class.
Map over an existing type
This is the easier way to create a custom type, since it is based on an existing decoder/encoder. You first retrieve an
existing encoder/decoder via the CassFormatEncoder
/CassFormatDecoder
’s apply
method.
scala> import com.weather.scalacass.{CassFormatDecoder, CassFormatEncoder}
import com.weather.scalacass.{CassFormatDecoder, CassFormatEncoder}
scala> implicit val uniqueIdDecoder: CassFormatDecoder[UniqueId] = CassFormatDecoder[String].map[UniqueId](UniqueId.apply)
uniqueIdDecoder: com.weather.scalacass.CassFormatDecoder[UniqueId] = com.weather.scalacass.CassFormatDecoder$$anon$11@32e90a98
scala> implicit val uniqueIdEncoder: CassFormatEncoder[UniqueId] = CassFormatEncoder[String].map[UniqueId](uniqueId => uniqueId.id)
uniqueIdEncoder: com.weather.scalacass.CassFormatEncoder[UniqueId] = com.weather.scalacass.CassFormatEncoder$$anon$1@37e474b0
With these implicits in scope, you can now use the UniqueId
type directly when encoding a Row.
First, insert a row:
scala> case class Insertable(s: UniqueId, i: Int, l: Long)
defined class Insertable
scala> val insertStatement = sSession.insert("mytable", Insertable(UniqueId("a_unique_id"), 1234, 5678L))
insertStatement: com.weather.scalacass.scsession.SCInsertStatement = SCInsertStatement(INSERT INTO mykeyspace.mytable (s, i, l) VALUES (a_unique_id, 1234, 5678))
scala> insertStatement.execute()
res5: com.weather.scalacass.Result[com.datastax.driver.core.ResultSet] = Right(ResultSet[ exhausted: true, Columns[]])
Then, select that row:
scala> case class Query(s: UniqueId)
defined class Query
scala> val selectStatement = sSession.selectOneStar("mytable", Query(UniqueId("a_unique_id")))
selectStatement: com.weather.scalacass.scsession.SCSelectOneStatement = SCSelectOneStatement(SELECT * FROM mykeyspace.mytable WHERE s=a_unique_id LIMIT 1)
scala> val res = selectStatement.execute()
res: com.weather.scalacass.Result[Option[com.datastax.driver.core.Row]] = Right(Some(Row[a_unique_id, 1234, 5678]))
Then, extract using UniqueId
:
scala> res.map(_.map(_.as[UniqueId]("s")))
res6: scala.util.Either[Throwable,Option[UniqueId]] = Right(Some(UniqueId(a_unique_id)))
Of course, UniqueId might throw an exception, which may not be the behavior you want, so you can optionally use
flatMap
for operations that might fail, which uses a Result[T]
type, which is just an alias to Either[Throwable, T]
:
abstract case class SafeUniqueId(id: String)
object SafeUniqueId {
def apply(untestedId: String): Result[SafeUniqueId] =
if (untestedId.length > 15) Left(new IllegalArgumentException("id must be less than 15 characters long"))
else Right(new SafeUniqueId(untestedId) {})
}
And with this definition, let’s redefine the encoder/decoder:
scala> implicit val safeUniqueIdDecoder: CassFormatDecoder[SafeUniqueId] = CassFormatDecoder[String].flatMap[SafeUniqueId](SafeUniqueId.apply)
safeUniqueIdDecoder: com.weather.scalacass.CassFormatDecoder[SafeUniqueId] = com.weather.scalacass.CassFormatDecoder$$anon$12@3a645b94
scala> implicit val safeUniqueIdEncoder: CassFormatEncoder[SafeUniqueId] = CassFormatEncoder[String].map[SafeUniqueId](safeId => safeId.id)
safeUniqueIdEncoder: com.weather.scalacass.CassFormatEncoder[SafeUniqueId] = com.weather.scalacass.CassFormatEncoder$$anon$1@45e2f95e
So, let’s go through the same steps this time, except inject an id that is too long for extraction
scala> case class UnsafeInsertable(s: String, i: Int, l: Long)
defined class UnsafeInsertable
scala> val unsafeInsertStatement = sSession.insert("mytable", UnsafeInsertable("this_id_is_definitely_too_long_to_be_safe", 1234, 5678L))
unsafeInsertStatement: com.weather.scalacass.scsession.SCInsertStatement = SCInsertStatement(INSERT INTO mykeyspace.mytable (s, i, l) VALUES (this_id_is_definitely_too_long_to_be_safe, 1234, 5678))
scala> unsafeInsertStatement.execute()
res7: com.weather.scalacass.Result[com.datastax.driver.core.ResultSet] = Right(ResultSet[ exhausted: true, Columns[]])
And then select that row:
scala> case class UnsafeQuery(s: String)
defined class UnsafeQuery
scala> val unsafeSelectStatement = sSession.selectOneStar("mytable", UnsafeQuery("this_id_is_definitely_too_long_to_be_safe"))
unsafeSelectStatement: com.weather.scalacass.scsession.SCSelectOneStatement = SCSelectOneStatement(SELECT * FROM mykeyspace.mytable WHERE s=this_id_is_definitely_too_long_to_be_safe LIMIT 1)
scala> val unsafeRes = unsafeSelectStatement.execute()
unsafeRes: com.weather.scalacass.Result[Option[com.datastax.driver.core.Row]] = Right(Some(Row[this_id_is_definitely_too_long_to_be_safe, 1234, 5678]))
And finally, try to extract it:
scala> unsafeRes.map(_.map(_.attemptAs[SafeUniqueId]("s")))
res8: scala.util.Either[Throwable,Option[com.weather.scalacass.Result[SafeUniqueId]]] = Right(Some(Left(java.lang.IllegalArgumentException: id must be less than 15 characters long)))
Create a new encoder/decoder from scratch
You might use a new encoder/decoder from scratch if you’ve added a user type to Cassandra itself, and want to use the
library to read from it. However, let’s continue with the UniqueId
example, as above.
For decoder
type From
is the Java type that is extracted from Cassandra directly, from which you will convert to a Scala typeval typeToken
is the special class instance for that typeTypeToken
is used overclassOf
because it can correctly encode type parameters toMap
s,List
s, andSet
sCassFormatDecoder
provides 3 helper functions for these types:CassFormatDecoder.mapOf
,.listOf
, and.setOf
def f2t
defines the transformation from the Java type to the Scala typedef extract
defines the way to extract the Java type from the CassandraRow
def tupleExtract
is the same asextract
, but for tuples
scala> import com.google.common.reflect.TypeToken, com.datastax.driver.core.{Row, TupleValue}
import com.google.common.reflect.TypeToken
import com.datastax.driver.core.{Row, TupleValue}
scala> implicit val safeUniqueIdDecoder: CassFormatDecoder[SafeUniqueId] = new CassFormatDecoder[SafeUniqueId] {
| type From = String
| val typeToken = TypeToken.of(classOf[String])
| def f2t(f: String): Result[SafeUniqueId] = SafeUniqueId(f)
| def extract(r: Row, name: String): From = r.getString(name)
| def tupleExtract(tup: TupleValue, pos: Int): From = tup.getString(pos)
| }
safeUniqueIdDecoder: com.weather.scalacass.CassFormatDecoder[SafeUniqueId] = $anon$1@3d671b76
For encoder
type From
is the Scala type which you are encoding fromval cassDataType
is the Cassandra type which you are converting todef encode
is the way that you encode that conversion, meaning the Scala -> Java conversion
scala> import com.datastax.driver.core.DataType
import com.datastax.driver.core.DataType
scala> implicit val safeUniqueIdEncoder: CassFormatEncoder[SafeUniqueId] = new CassFormatEncoder[SafeUniqueId] {
| type From = String
| val cassDataType: DataType = DataType.varchar()
| def encode(f: SafeUniqueId): Result[String] = Right(f.id)
| }
safeUniqueIdEncoder: com.weather.scalacass.CassFormatEncoder[SafeUniqueId] = $anon$1@67e5e4e5
And as before,
scala> case class UnsafeQuery(s: String)
defined class UnsafeQuery
scala> val unsafeSelectStatement = sSession.selectOneStar("mytable", UnsafeQuery("this_id_is_definitely_too_long_to_be_safe"))
unsafeSelectStatement: com.weather.scalacass.scsession.SCSelectOneStatement = SCSelectOneStatement(SELECT * FROM mykeyspace.mytable WHERE s=this_id_is_definitely_too_long_to_be_safe LIMIT 1)
scala> val unsafeRes = unsafeSelectStatement.execute()
unsafeRes: com.weather.scalacass.Result[Option[com.datastax.driver.core.Row]] = Right(Some(Row[this_id_is_definitely_too_long_to_be_safe, 1234, 5678]))
scala> unsafeRes.map(_.map(_.attemptAs[SafeUniqueId]("s")))
res11: scala.util.Either[Throwable,Option[com.weather.scalacass.Result[SafeUniqueId]]] = Right(Some(Left(java.lang.IllegalArgumentException: id must be less than 15 characters long)))