TypeCodecs - nMoncho/helenus GitHub Wiki

A TypeCodec tells Cassandra how to encode, or decode, a particular JVM type.

Helenus tries to make their use as transparent and seamless as possible. Most TypeCodecs are defined implicitly, so we don't have to worry about them in most cases.

Keep in mind that a TypeCodec is different from a RowMapper. The former tells Cassandra how to map a single column to a JVM type. Whereas the latter tells Helenus how to map a row into a Scala type, such as a tuple or a case class.

When do we have to define our own TypeCodec?

Helenus provides TypeCodecs for the most common JVM types, such as String, Int, Boolean and so on. It also provides instances for collections and tuples (we can treat Scala Tuple as Cassandra Tuples).

We have to define a TypeCodec when using:

  • An Enumeration
  • A Case Class
  • A Collection type that isn't supported out of the box

When a TypeCodec is required but no implicit instance is defined (or found), the compiler will complain with an implicit instance not found error.

Enumerations

Enumerations can be encoded either by name, or by order, mapping the enumeration to a column of type TEXT, or INT respectively.

Mark your enumeration either with the NominalEncoded or the OrdinalEncoded annotation:

import net.nmoncho.helenus.api.{ NominalEncoded, OrdinalEncoded }

@NominalEncoded
object Fingers extends Enumeration {
 type Finger = Value

 val Thumb, Index, Middle, Ring, Little = Value
}

@OrdinalEncoded
object Planets extends Enumeration {
 type Planet = Value

 val Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune = Value
}

val fingerCodec: TypeCodec[Fingers.Finger] = Codec[Fingers.Finger]
// fingerCodec: TypeCodec[Fingers.Finger] = EnumerationNominalCodec[Fingers]
val planetCodec: TypeCodec[Planets.Planet] = Codec[Planets.Planet]
// planetCodec: TypeCodec[Planets.Planet] = EnumerationOrdinalCodec[Planets]

Enumerations without extending Enumeration

We can use a sealed trait or a sealed abstract class as an idiom to define enumerations. We can define a TypeCodec for them with a MappingCodec:

sealed abstract class Room(val name: String)
object Room {
    case object LivingRoom extends Room("living-room")
    case object Bathroom extends Room("bathroom")
    case object Bedroom extends Room("bedroom")

    def apply(str: String): Room = str match {
        case "living-room" => LivingRoom
        case "bathroom" => Bathroom
        case "bedroom" => Bedroom
    }

    implicit val roomCodec: TypeCodec[Room] =
        Codec.mappingCodec[String, Room](Room.apply, _.name)
}

User Defined Types (UDTs)

Cassandra UDTs can be mapped to case classes. By using the identicalUdtOf we can derive a TypeCodec:

case class Address(street: String, number: Int, addition: String)

implicit val addressCodec: TypeCodec[Address] = Codec.identicalUdtOf[Address]()
// addressCodec: TypeCodec[Address] = UtdCodec[Address]

This method takes three optional parameters:

  • keyspace: In which keyspace is the CQL type defined. If left blank, the session's keyspace will be used.
  • name: CQL Type's Name. If left blank, the case class SimpleClassName will be used (considering the column mapper)
  • frozen: Where the CQL type is frozen or not. This is true by default.

UDT Field Order

When using identicalUdtOf it's important to respect the same component order as the one used in the CQL type. For the previous example, this would be a valid definition:

CREATE TYPE address(
    street      TEXT,
    number      INT,
    addition    TEXT
);

If we cannot respect this restriction, we must use:

  • nonIdenticalUdtCodecOf: This method requires a to a CqlSession to find how fields should be mapped.
  • udtFromFields: This method allows us to define the order of the CQL type without requiring a CqlSession.

nonIdenticalUdtCodecOf

case class Addr(street: String, addition: String, number: Int)

implicit val addrCodec: TypeCodec[Addr] = Codec.nonIdenticalUdtCodecOf[Addr](session = cqlSession, name = "address")
// addrCodec: TypeCodec[Addr] = UtdCodec[Addr]

This method takes two optional parameters:

  • keyspace: In which keyspace is the CQL type defined. If left blank, the session's keyspace will be used.
  • name: CQL Type's Name. If left blank, the case class SimpleClassName will be used (considering the column mapper)

Collections

We implement codecs for collection types to avoid conversions to and from Java Collections (e.g like the conversions available in scala.jdk.CollectionConverters)

Where is the TypeCodec for Collection X?

We don't provide TypeCodecs for all collections, just for some traits and some implementations. If you need support for another collection, you can extend one of the three abstract classes: AbstractSeqCodec, AbstractSetCodec, or AbstractMapCodec.

For example, say you'd like to add support for ArraySeq, you could implement it by doing:

class IndexedSeqCodec[T](inner: TypeCodec[T], frozen: Boolean)
    extends AbstractSeqCodec[T, IndexedSeq](inner, frozen) {

    override val getJavaType: GenericType[IndexedSeq[T]] =
        GenericType
        .of(new TypeToken[IndexedSeq[T]]() {}.getType)
        .where(new GenericTypeParameter[T] {}, inner.getJavaType.wrap())
        .asInstanceOf[GenericType[IndexedSeq[T]]]

    override def toString: String = s"IndexedSeqCodec[${inner.getCqlType.toString}]"
}

implicit def indexedSeqOf[T](implicit inner: TypeCodec[T]): TypeCodec[IndexedSeq[T]] =
    new IndexedSeqCodec(inner, frozen = true)

Please remember to create an implicit instance so Helenus can pick up on it.

And What About Mutable Collections?

Just like immutable collections, Helenus provides TypeCodecs for some implementations, like Buffer, or mutable.IndexedSeq. If you need support for another collection, you can implement one yourself, it's pretty easy and straightforward.

Let's take Buffer as an example:

import scala.collection.mutable

class BufferCodec[T](inner: TypeCodec[T], frozen: Boolean)
    extends AbstractSeqCodec[T, mutable.Buffer](inner, frozen) {

    override val getJavaType: GenericType[mutable.Buffer[T]] =
        GenericType
        .of(new TypeToken[mutable.Buffer[T]]() {}.getType)
        .where(new GenericTypeParameter[T] {}, inner.getJavaType.wrap())
        .asInstanceOf[GenericType[mutable.Buffer[T]]]

}
  1. Create a class for the collection you want a TypeCodec for. This class must take as parameter:
    1. The TypeCodec of its elements
    2. Whether this codec is used on a Frozen Collection
  2. Extend one of the Collection TypeCodec. In this case we're using AbstractSeqCodec. Notice we have use T (ie. the collection element type), and mutablecoll.Buffer as type parameters. This will let the parent class know what's the expected collection type.
  3. Override getJavaType, which should return a GenericType of the expected collection type, in this case Buffer[T]. A GenericType can be created as follows
    1. Use the GenericType.of method, while supplying a TypeToken of the expected collection type.
    2. Use the where method to specify any type parameters the collection will use. Buffer in this case only uses one type parameter, whereas Map would call where twice, one for the key type parameter and another for the value.