DataStax Developer Blog

Introducing DataStax Java Driver 2.1

By Olivier Michallat -  August 26, 2014 | 5 Comments

We are pleased to announce the latest version of our Java driver, released in time for Cassandra 2.1.

This release brings support for Cassandra 2.1, while remaining compatible with 1.2 and 2.0. It also introduces a new object mapping API, which simplifies the conversion of query results to custom Java classes. Finally, it includes several improvements (for a full list, refer to the changelog).

Without further ado, let’s see these new features in action:

User Defined Types and tuples

As previously described on this blog, Cassandra 2.1 introduces User Defined Types, which are named groups of related properties:

CREATE TYPE address (
    street text,
    city text,
    zip int
);

CREATE TABLE user_profiles (
    email text PRIMARY KEY,
    address address
);

From the driver’s perspective, UDT values can be retrieved like any other type. They are implemented as a map-like object, with the usual getters and setters:

Row row = session.execute("SELECT * FROM user_profiles").one();
UDTValue address = row.getUDTValue("address");

String street = address.getString("street"); // by field name
int zip       = address.getInt(2);           // by index

You can also get hold of the data type representing a particular UDT; it is useful when you want to create new values:

// Get the type from an existing value:
UserType addressType = address.getType();
// Or from the cluster metadata:
UserType addressType = cluster.getMetadata().getKeyspace("ks").getUserType("address");

UDTValue address2 = addressType.newValue()
                               .setString("street", "1600 Pennsylvania Ave NW")
                               .setString("city", "Washington")
                               .setInt("zip", 20500);

session.execute("INSERT INTO user_profiles (email, address) VALUES (?, ?)",
                "xyz@example.com", address2);

One thing that immediately comes to mind is that we’d rather use our own Address class in our code. This is exactly what the object mapper is here for, as we’ll find out shortly.

Also new in Cassandra 2.1, tuples are essentially anonymous UDTs: collections of unnamed fields with predefined types.

CREATE TABLE points_of_interest (
    id int PRIMARY KEY,
    name text,
    coordinates tuple<float,float>
);

Querying works mostly like UDTs, except that fields can only be accessed by index:

Row row = session.execute("SELECT * FROM points_of_interest").one();
TupleValue coordinates = row.getTupleValue("coordinates");
float latitude  = coordinates.getFloat(0);
float longitude = coordinates.getFloat(1);

New tuple values can be created directly based on the types of the field:

TupleType coordinatesType = TupleType.of(DataType.cfloat(), DataType.cfloat());
TupleValue newCoordinates = coordinatesType.newValue(48.858222F, 2.2945F);

Simple object mapper

Most Java applications use custom Java classes to represent their data (for example UserProfile and Address in our first example). Converting back and forth between those classes and the driver’s own types (Row and TupleValue) involves some boilerplate code and can be automated.

The goal of the object mapper is to generate most of that boilerplate for you. To specify the target tables and UDTs in Cassandra, decorate your Java classes with annotations:

@UDT(keyspace = "ks", name = "address")
public class Address {
    private String street;
    private String city;
    private int zip;

    // getters and setters omitted...
}

@Table(keyspace = "ks", name = "user_profiles")
public class UserProfile {
    @PartitionKey
    private String email;
    private Address address;

    // getters and setters omitted...
}

You can then retrieve a mapper that handles basic CRUD operations (which come in both synchronous and asynchronous flavors):

MappingManager manager = new MappingManager(session);
Mapper mapper = manager.mapper(UserProfile.class);

UserProfile myProfile = mapper.get("xyz@example.com");
ListenableFuture saveFuture = mapper.saveAsync(anotherProfile);
mapper.delete("xyz@example.com");

For more complex queries, the mapper can also generate an “accessor” object from an interface annotated with the queries to perform:

@Accessor
interface ProfileAccessor {
    @Query("SELECT * FROM user_profiles LIMIT :max")
    Result firstN(@Param("max") int limit);
}

ProfileAccessor accessor = manager.createAccessor(ProfileAccessor.class);
Result profiles = accessor.firstN(10);

// Result is like ResultSet, but specialized for a mapped class:
for (UserProfile profile : profiles) {
    System.out.println(profile.getAddress().getZip());
}

The object mapper is deliberately simple: its primary goal is to replace boilerplate code, not to hide Cassandra from the developer. Therefore it avoids complex features like lazy-loading or entity proxies.

Upgrading to 2.1

This new version of the driver is available from the Maven repositories (note that the object mapper is published as a separate artifact), and as a packaged binary. Refer to the upgrade guide if you are upgrading from a previous version.

While we strive to preserve backwards-compatibility, version 2.1 introduces a few internal API changes that will be transparent to most users, and two user API changes (all documented here).

Thank you for using and supporting Apache Cassandra and DataStax.



Comments

  1. Ziju says:

    Is prepared statement used in the backgrond? What’s the performance overhead of mapper/accessor? How to specify composite partition keys and compound keys?

    1. Olivier Michallat says:

      Yes, prepared statements are used.

      We’ve not run benchmarks for the mapper yet, but it’s designed to have a very low profile. Most of the work is done when initializing the Mapper instances, and then cached at the MappingManager level.

      You specify composite partition keys and compound keys with the @PartitionKey and @ClusteringColumn annotations.

      1. Ziju says:

        Thanks for your quick reply.

        Since most of work is done during initialization, should we only initialize the mapper/accessor instances once and use them during application’s lifetime?

        Another question is how the conversion from Java POJOs to text is handled by the mapper? The example listed in http://www.datastax.com/documentation/developer/java-driver/2.1/java-driver/reference/crudOperations.html shows that the field “phones” in Address instances is stored as list in the UDT type. What protocol does the mapper use to convert Phone instances to text?

        Thanks

        1. Ziju says:

          I mean “phones” is stored as “list”.

        2. Ziju says:

          I wanted to post list《text》 but the “text” part seemed to be treated as a html tag and omitted.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>