8

The database (Cassandra) does not allow specifying a default value when adding new columns. New columns are therefore null for every row.

Now, we added a flag to our entity (which might get more entries in the future):

enum UsageFlag {
    OTHER,
    SPECIAL_VALUE,
}

We also added a ReadingConverter:

@ReadingConverter
public class UsageFlagReadingConverter implements Converter<String, UsageFlag> {
  @Nonnull
  @Override
  public UsageFlag convert(String source) {
    return source == null
        ? UsageFlag.OTHER
        : UsageFlag.valueOf(source);
  }
}

It should basically treat null as UsageFlag.OTHER while parsing any other non-null value as an enum member.

However, this does not work since according to the Spring docs, the convert method of the Converter is only called if the value is not null. Because of this, the field of that entity remains null if it is null in the database. It works fine if (and only if) there is a value. Since the entire column is null when creating it, this completely defeats the purpose of the ReadingConverter.

Cassandra does not seem to support setting a value for every row (like UPDATE foo SET flag='OTHER' WHERE True), so this seemed like a good option. We wanted to avoid touching every row in the database manually.

Is there any way we can use Spring to help in this situation? Or is there a way to solve this using CQL or Cassandra?

Edit: Some more context on the setup:

public interface FancyEntityRepository extends MapIdCassandraRepository<FancyEntity> {
}

public class FancyEntity {
    @PrimaryKeyColumn(name = "id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
    private string id;
    private UsageFlag usage;
    // other columns omitted
    // getters and setter omitted for this example
}

// Somewhere in a service: 
FancyEntity fe = fancyEntityRepository.findOne("1");
System.out.println(fe.getUsage());

But I don't think that the behaviour of the converter is cassandra-specific.

The table was created and then altered this way:

CREATE TABLE FancyEntity (
    id TEXT,
    -- other columns omitted
    PRIMARY KEY (id)
);
ALTER TABLE FancyEntity ADD (
    usage TEXT
);

Cassandra Docs on ALTER TABLE and a related SO question about altering tables with a default value.

Erick Ramirez
  • 13,964
  • 1
  • 18
  • 23
nikeee
  • 10,248
  • 7
  • 40
  • 66
  • Well presented. Can you add the code where you are doing the request ? – Vinetos May 25 '20 at 14:41
  • Can you provide [MRE](https://stackoverflow.com/help/minimal-reproducible-example)? – 123 May 25 '20 at 14:44
  • I added some more context on the Repository/Entity used - does this suffice? @123 do you mean in a form of a GitHub repo or something? This could take some time to boil it down. I think the "real" solution is something that I'm missing about spring or cassandra. – nikeee May 25 '20 at 14:55
  • @nikeee A github repo would probably be the most helpful, but sort of goes against the format of StackExchange unfortunately. I meant pretty much what you've edited in now, cheers. – 123 May 25 '20 at 15:12
  • Just to clarify, this statement is incorrect: **New columns are therefore null for every row**. New columns simply do not have a value if one is not set. CQL and Spring Data _abstract_ that concept away from you by _showing_ null, which is different from actually containing a value of null. It's possible that Cassandra lightweight transactions could help you here: https://docs.datastax.com/en/cql-oss/3.3/cql/cql_using/useInsertLWT.html – Aaron May 26 '20 at 13:41
  • Why don't you handle this case in your model? When using constructor creation, you can default to a specific value. Alternatively, you can initialize your fields and use property accessors instead of field access. – mp911de May 26 '20 at 14:34
  • @mp911de For now, we use getters/setters that check for null and return/set the "real" default value. But this solution seems hacky. – nikeee May 26 '20 at 20:05
  • @Aaron thanks for the correction. Does this work without specifying a `WHERE` clause or is there a way to do this for every row? We want to avoid doing a query for every entry. – nikeee May 26 '20 at 20:06
  • Unfortunately, no. If you want to write to every valid partition, you need to specify every partition. – Aaron May 26 '20 at 20:38
  • Actually, that is the preferred approach. `Converter.convert(…)` accepts a non-nullable input type. During value retrieval from Cassandra, the column value is `null` and so the source type for the converter cannot be retrieved. – mp911de May 27 '20 at 08:18

1 Answers1

1

Cassandra has sparse storage meaning that it does not store columns (fields in RDBMS) if they are not set. It is therefore incorrect to say that "new columns are null".

If you are using prepared statements, you need to call BoundStatement.unset() with native protocol v3 so that tombstones are not generated for null values.

Otherwise, it is fine to leave variables unset with native protocol v4 (Cassandra 2.2 or later). Cheers!

Erick Ramirez
  • 13,964
  • 1
  • 18
  • 23