blob: 7a9107af26fd8ec641e72ce0d5a2445171ea2487 [file] [log] [blame] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
## Custom codecs
Out of the box, the driver comes with [default CQL to Java mappings](../#cql-to-java-type-mapping).
For example, if you read a CQL `text` column, it is mapped to its natural counterpart
`java.lang.String`:
```java
// cqlsh:ks> desc table test;
// CREATE TABLE ks.test (k int PRIMARY KEY, v text)...
ResultSet rs = session.execute("SELECT * FROM ks.test WHERE k = 1");
String v = rs.one().getString("v");
```
Sometimes you might want to use different mappings, for example:
* read a text column as a Java enum;
* map an `address` UDT to a custom `Address` class in your application;
* manipulate CQL collections as arrays in performance-intensive applications.
Custom codecs allow you to define those dedicated mappings, and plug them into your session.
### Writing codecs
To write a custom codec, implement the [TypeCodec] interface. Here is an example that maps a CQL
`int` to a Java string containing its textual representation:
```java
public class CqlIntToStringCodec implements TypeCodec<String> {
@Override
public GenericType<String> getJavaType() {
return GenericType.STRING;
}
@Override
public DataType getCqlType() {
return DataTypes.INT;
}
@Override
public ByteBuffer encode(String value, ProtocolVersion protocolVersion) {
if (value == null) {
return null;
} else {
int intValue = Integer.parseInt(value);
return TypeCodecs.INT.encode(intValue, protocolVersion);
}
}
@Override
public String decode(ByteBuffer bytes, ProtocolVersion protocolVersion) {
Integer intValue = TypeCodecs.INT.decode(bytes, protocolVersion);
return intValue.toString();
}
@Override
public String format(String value) {
int intValue = Integer.parseInt(value);
return TypeCodecs.INT.format(intValue);
}
@Override
public String parse(String value) {
Integer intValue = TypeCodecs.INT.parse(value);
return intValue == null ? null : intValue.toString();
}
}
```
Admittedly, this is a trivial -- and maybe not very realistic -- example, but it illustrates a few
important points:
* which methods to override. Refer to the [TypeCodec] javadocs for additional information about each
of them;
* how to piggyback on a built-in codec, in this case `TypeCodecs.INT`. Very often, this is the best
approach to keep the code simple. If you want to handle the binary encoding yourself (maybe to
squeeze the last bit of performance), study the driver's
[built-in codec implementations](https://github.com/datastax/java-driver/tree/4.x/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec).
### Using codecs
Once you have your codec, register it when building your session:
```java
CqlSession session = CqlSession.builder()
.addTypeCodecs(new CqlIntToStringCodec())
.build();
```
You can now use the new mapping in your code:
```java
// cqlsh:ks> desc table test2;
// CREATE TABLE ks.test2 (k int PRIMARY KEY, v int)...
ResultSet rs = session.execute("SELECT * FROM ks.test2 WHERE k = 1");
String v = rs.one().getString("v"); // read a CQL int as a java.lang.String
PreparedStatement ps = session.prepare("INSERT INTO ks.test2 (k, v) VALUES (?, ?)");
session.execute(
ps.boundStatementBuilder()
.setInt("k", 2)
.setString("v", "12") // write a java.lang.String as a CQL int
.build());
```
Custom codecs are used not only for their base type, but also recursively in collections, tuples and
UDTs. For example, once your `int <-> String` codec is registered, you can also read a CQL
`list<int>` as a Java `List<String>`:
```java
// cqlsh:ks> desc table test3;
// CREATE TABLE ks.test2 (k int PRIMARY KEY, v list<int>)...
ResultSet rs = session.execute("SELECT * FROM ks.test3 WHERE k = 1");
List<String> v = rs.one().getList("v", String.class);
```
So far our examples have used a Java type with dedicated accessors in the driver: `getString` and
`setString`. But you can also map your own Java types. For example, let's assume you have a `Price`
class, and have registered a codec that maps it to a particular CQL type. When reading or writing
values, you need a way to tell the driver which Java type you want; this is done with the generic
`get` and `set` methods with an extra *type token* arguments:
```java
GenericType<Price> priceType = GenericType.of(Price.class);
// Reading
Price price = row.get("v", priceType);
// Writing
boundStatement.set("v", price, priceType);
```
Type tokens are instances of [GenericType]. They are immutable and thread-safe, you should store
them as reusable constants. Generic Java types are fully supported, using the following pattern:
```java
// Notice the '{}': this is an anonymous inner class
GenericType<Foo<Bar>> fooBarType = new GenericType<Foo<Bar>>(){};
Foo<Bar> v = row.get("v", fooBarType);
```
Whenever you read or write a value, the driver tries all the built-in mappings first, followed by
custom codecs. If two codecs can process the same mapping, the one that was registered first is
used. Note that this means that built-in mappings can't be overridden.
In rare cases, you might have a codec registered in your application, but have a legitimate reason
to use a different mapping in one particular place. In that case, you can pass a codec instance
to `get` / `set` instead of a type token:
```java
TypeCodec<String> defaultCodec = new CqlIntToStringCodec();
TypeCodec<String> specialCodec = ...; // a different implementation
CqlSession session =
CqlSession.builder().addTypeCodecs(defaultCodec).build();
String s1 = row.getString("anIntColumn"); // int -> String, will decode with defaultCodec
String s2 = row.get("anIntColumn", specialCodec); // int -> String, will decode with specialCodec
```
### Subtype polymorphism
Suppose the following class hierarchy:
```java
class Animal {}
class Cat extends Animal {}
```
By default, a codec will accept to serialize any object that extends or implements its declared Java
type: a codec such as `AnimalCodec extends TypeCodec<Animal>` will accept `Cat` instances as well.
This allows a codec to handle interfaces and superclasses in a generic way, regardless of the actual
implementation being used by client code; for example, the driver has a built-in codec that handles
`List` instances, and this codec is capable of serializing any concrete `List` implementation.
But this has one caveat: when setting or retrieving values with `get()` and `set()`, *you must pass
the exact Java type the codec handles*:
```java
BoundStatement bs = ...
bs.set(0, new Cat(), Animal.class); // works
bs.set(0, new Cat(), Cat.class); // throws CodecNotFoundException
Row row = ...
Animal animal = row.get(0, Animal.class); // works
Cat cat = row.get(0, Cat.class); // throws CodecNotFoundException
```
### The codec registry
The driver stores all codecs (built-in and custom) in an internal [CodecRegistry]:
```java
CodecRegistry getCodecRegistry = session.getContext().getCodecRegistry();
// Get the custom codec we registered earlier:
TypeCodec<String> cqlIntToString = codecRegistry.codecFor(DataTypes.INT, GenericType.STRING);
```
If all you're doing is executing requests and reading responses, you probably won't ever need to
access the registry directly. But it's useful if you do some kind of generic processing, for
example printing out an arbitrary row when the schema is not known at compile time:
```java
private static String formatRow(Row row) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < row.size(); i++) {
String name = row.getColumnDefinitions().get(i).getName().asCql(true);
Object value = row.getObject(i);
DataType cqlType = row.getType(i);
// Find the best codec to format this CQL type:
TypeCodec<Object> codec = row.codecRegistry().codecFor(cqlType);
if (i != 0) {
result.append(", ");
}
result.append(name).append(" = ").append(codec.format(value));
}
return result.toString();
}
```
[CodecRegistry]: https://docs.datastax.com/en/drivers/java/4.2/com/datastax/oss/driver/api/core/type/codec/registry/CodecRegistry.html
[GenericType]: https://docs.datastax.com/en/drivers/java/4.2/com/datastax/oss/driver/api/core/type/reflect/GenericType.html
[TypeCodec]: https://docs.datastax.com/en/drivers/java/4.2/com/datastax/oss/driver/api/core/type/codec/TypeCodec.html