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 3 ways:
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@4fc500e9
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@76f041d2
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()
res4: 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")))
res5: 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@13282fd1
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@7df6ca86
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()
res6: 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")))
res7: scala.util.Either[Throwable,Option[com.weather.scalacass.Result[SafeUniqueId]]] = Right(Some(Left(java.lang.IllegalArgumentException: id must be less than 15 characters long)))
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()
res8: com.weather.scalacass.Result[com.datastax.driver.core.ResultSet] = Left(com.datastax.driver.core.exceptions.InvalidQueryException: unconfigured table specialtable)
Renaming is not the only usage of forProduct$arity, nor is it strictly required to create one for a case class.
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 Fromis the Java type that is extracted from Cassandra directly, from which you will convert to a Scala typeval typeTokenis the special class instance for that typeTypeTokenis used overclassOfbecause it can correctly encode type parameters toMaps,Lists, andSetsCassFormatDecoderprovides 3 helper functions for these types:CassFormatDecoder.mapOf,.listOf, and.setOf
def f2tdefines the transformation from the Java type to the Scala typedef extractdefines the way to extract the Java type from the CassandraRowdef tupleExtractis 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@3170139c
For encoder
type Fromis the Scala type which you are encoding fromval cassDataTypeis the Cassandra type which you are converting todef encodeis 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@2fc318ea
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)))