Mapper in Cassandra Java API

Before writing some code in Apache Cassandra, we'll try to explore very interesting dependency - cassandra-driver-mapping.

The article presents basic CRUD operations which can be made through Cassandra mapping module. Unlike low level manipulation methods, the ones from mapping module are very intuitive and helps to easily translate rows to Java objects. The first part describes main objects defined in this module. The second part shows how to use them through test cases.

Mapping module for Cassandra

The module is located in com.datastax.driver.mapping package. The main class responsible for mapping, is MappingManager. Created for currently active session, it initializes mappers to specific Java objects. These mappers are the instances of Mapper class, typed to mapped Java class.

Mappers are created only once for mapped class. As already told, they can execute different CRUD operations, such as: delete, edit or entity reading through defined primary keys. Generally, it's easier to use these operations from Mapper rather than rewrite them manually and execute through Session's execute(String) method. By the way, under-the-hood Mapper does the same. For all operations it calls execute(String) method. A simple check on source code proves that (version 3.0.0):

public void save(T entity, Option... options) { 
  session().execute(saveQuery(entity, options));
}

public void delete(T entity) {
  session().execute(deleteQuery(entity));
}

Java classes are translated to/from Cassandra rows thanks to annotations defined inside com.datastax.driver.mapping.annotations package. We retrieve, among others, annotations to define a table (@Table), columns (...@Column), keys (@PartitionKey, @ClusteringColumn, both able to take in parameter index ordering the key) or user defined type (@UDT, @Field should be used to map its fields).

Example of Cassandra mapping module

Let's see how to use mapper through some tests cases around given a table representing football team. The mapping for Java object looks like:

@Table(name = "simple_team")
public class SimpleTeam {

  @PartitionKey
  @Column(name = "teamName")
  private String teamName;

  @ClusteringColumn(0)
  @Column(name = "foundationYear")
  private int foundationYear;

  @ClusteringColumn(1)
  @Column(name = "country")
  private String country;

  @Transient
  private boolean euCountry;
  // getters, setters and constructors ommitted
  public boolean isEuCountry() {
    return "France".equals(country);
  }
}

And the table is created with this query:

CREATE TABLE simple_team (
  teamName text,
  foundationYear int,
  country text,
  PRIMARY KEY (teamName, foundationYear, country)
)

Tests associated to this object, showing the use mapper on it, are:

@Before
public void deleteTable() {
  SESSION.execute("DROP TABLE IF EXISTS mapperTest.simple_team");
}

@Test(expected = InvalidQueryException.class)
public void should_fail_on_inserting_row_without_defined_table() {
  SESSION.execute("DROP TABLE IF EXISTS mapperTest.simple_team");
  // Please note that the table doesn't exist - was not created as previously in setup method
  // It makes this test fail because tables aren't created in the fly
  Mapper<SimpleTeam> mapperTeam = MAPPING_MANAGER.mapper(SimpleTeam.class);
  mapperTeam.save(new SimpleTeam("AC Ajaccio", 1910, "France", 3));
}

@Test
public void should_create_only_one_mapper_instance() throws IOException, URISyntaxException {
  createMappedTable();

  Mapper<SimpleTeam> mapperTeam1 = MAPPING_MANAGER.mapper(SimpleTeam.class);
  Mapper<SimpleTeam> mapperTeam2 = MAPPING_MANAGER.mapper(SimpleTeam.class);

  assertThat(mapperTeam1).isEqualTo(mapperTeam2);
}

@Test
public void should_correctly_manipulate_row_through_mapper() throws IOException, URISyntaxException {
  createMappedTable();

  Mapper<SimpleTeam> mapperTeam = MAPPING_MANAGER.mapper(SimpleTeam.class);
  mapperTeam.save(new SimpleTeam("AC Ajaccio", 1910, "France", 3));

  SimpleTeam acAjaccio = mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccio).isNotNull();
  assertThat(acAjaccio.getCountry()).isEqualTo("France");
  assertThat(acAjaccio.getFoundationYear()).isEqualTo(1910);
  assertThat(acAjaccio.getTeamName()).isEqualTo("AC Ajaccio");
  assertThat(acAjaccio.getDivision()).isEqualTo(3);
}

@Test
public void should_correctly_delete_created_team() throws IOException, URISyntaxException {
  createMappedTable();

  Mapper<SimpleTeam> mapperTeam = MAPPING_MANAGER.mapper(SimpleTeam.class);
  mapperTeam.save(new SimpleTeam("AC Ajaccio", 1910, "France", 2));

  SimpleTeam acAjaccio = mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccio).isNotNull();

  mapperTeam.delete(acAjaccio);
  acAjaccio = mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccio).isNull();
}

@Test
public void should_edit_team_in_asynchronous_way() throws IOException, URISyntaxException, InterruptedException, ExecutionException, TimeoutException {
  // Non-blocking operations are provided with mapper module
  // One of them is edition
  createMappedTable();

  Mapper<SimpleTeam> mapperTeam = MAPPING_MANAGER.mapper(SimpleTeam.class);
  mapperTeam.save(new SimpleTeam("AC Ajaccio", 1910, "France", 2));

  SimpleTeam acAjaccio = mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccio).isNotNull();

  acAjaccio.setDivision(1);
  ListenableFuture<Void> future = mapperTeam.saveAsync(acAjaccio);
  future.get(5, TimeUnit.SECONDS);

  SimpleTeam acAjaccioDiv1= mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccioDiv1).isNotNull();
  assertThat(acAjaccioDiv1.getDivision()).isEqualTo(1);
}

@Test
public void should_not_edit_row_when_one_primary_key_changes() throws IOException, URISyntaxException {
  // When one of primary keys changes, it means that new row is created
  // Cassandra doesn't allow to update PRIMARY KEYs
  createMappedTable();

  Mapper<SimpleTeam> mapperTeam = MAPPING_MANAGER.mapper(SimpleTeam.class);
  mapperTeam.save(new SimpleTeam("AC Ajaccio", 1910, "France", 2));

  SimpleTeam acAjaccio = mapperTeam.get("AC Ajaccio", 1910, "France");
  assertThat(acAjaccio).isNotNull();
  acAjaccio.setCountry("Corsica");
  mapperTeam.save(acAjaccio);

  SimpleTeam acAjaccioFrance= mapperTeam.get("AC Ajaccio", 1910, "France");
  SimpleTeam acAjaccioCorsica= mapperTeam.get("AC Ajaccio", 1910, "Corsica");
  assertThat(acAjaccioFrance).isNotNull();
  assertThat(acAjaccioFrance.getCountry()).isEqualTo("France");
  assertThat(acAjaccioCorsica).isNotNull();
  assertThat(acAjaccioCorsica.getCountry()).isEqualTo("Corsica");
}

private void createMappedTable() throws IOException, URISyntaxException {
  String tableCreateQuery = TestHelper.readFromFile("/queries/create_simple_team.cql");
  System.out.println("Executing query to create table: "+tableCreateQuery);
  SESSION.execute(tableCreateQuery);
}

To give you a general idea of manual mapping, please consider this example:

// Client part
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Jaap Stam', 'Ajax Amsterdam')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Javier Saviola', 'FC Barcelona')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Carles Puyol', 'FC Barcelona')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Andoni Zubizarreta', 'FC Barcelona')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Jaap Stam', 'Manchester United')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Laurent Blanc', 'Manchester United')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Fabien Barthez', 'Manchester United')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Patrick Kluivert', 'Ajax Amsterdam')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Kevin Kuranyi', 'Schalke 04')");
SESSION.execute("INSERT INTO players_descending (name, team) VALUES ('Edwin van der Sar', 'Ajax Amsterdam')");

ResultSet resultSet = SESSION.execute("SELECT * FROM players_descending WHERE team IN('FC Barcelona', " +
  "'Manchester United', 'Ajax Amsterdam')");

List<Player> players = resultSet.all().stream().map(row -> Player.fromRow(row)).collect(Collectors.toList());


// Conversion part
public static Player fromRow(Row row) {
  Player player = new Player();
  player.team = row.getString("team");
  player.name = row.getString("name");
  return player;
}

The difference is not immediately visible because Player object has only 2 small String fields. But if it had more complex structure, with for example frozen collections, some tuples and user defined types, the manual conversion would be really hard to achieve and maintain.

In this article we discovered module handling mapping between Apache Cassandra rows and corresponding to it Java entities. The first part shown that the mapping part is orchestrated by MappingManager, used further to create singletons for each mappers. Mapper can translate row from/to Java entities only when the last ones are annotated with special annotations. The second part implemented simple mapping case and compared it to manual conversion method.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!