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, thecase classSimpleClassNamewill be used (considering the column mapper)frozen: Where the CQL type is frozen or not. This istrueby 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 aCqlSessionto find how fields should be mapped.udtFromFields: This method allows us to define the order of the CQL type without requiring aCqlSession.
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, thecase classSimpleClassNamewill 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]]]
}
- Create a class for the collection you want a
TypeCodecfor. This class must take as parameter:- The
TypeCodecof its elements - Whether this codec is used on a Frozen Collection
- The
- Extend one of the Collection
TypeCodec. In this case we're usingAbstractSeqCodec. Notice we have useT(ie. the collection element type), andmutablecoll.Bufferas type parameters. This will let the parent class know what's the expected collection type. - Override
getJavaType, which should return aGenericTypeof the expected collection type, in this caseBuffer[T]. AGenericTypecan be created as follows- Use the
GenericType.ofmethod, while supplying aTypeTokenof the expected collection type. - Use the
wheremethod to specify any type parameters the collection will use.Bufferin this case only uses one type parameter, whereasMapwould callwheretwice, one for the key type parameter and another for the value.
- Use the