Search code examples
javaspring-bootcassandrascylla

Can prepared statements be enabled out of the box?


I would like to make use of prepared statements when executing CQL in my application. This functionality looks to be provided by the ReactiveCqlTemplate class, which I have passed into the ReactiveCassandraTemplate in my Cassandra configuration here:

@Configuration
@EnableReactiveCassandraRepositories(
        basePackages = "com.my.app",
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {ScyllaPersonRepository.class})
        })
public class CassandraConfiguration extends AbstractReactiveCassandraConfiguration {

    @Value("${cassandra.host}")
    private String cassandraHost;

    @Value("${cassandra.connections}")
    private Integer cassandraConnections;

    @Override
    public CassandraClusterFactoryBean cluster() {
        PoolingOptions poolingOptions = new PoolingOptions()
                .setCoreConnectionsPerHost(HostDistance.LOCAL,  cassandraConnections)
                .setMaxConnectionsPerHost(HostDistance.LOCAL, cassandraConnections*2);

        CassandraClusterFactoryBean bean = super.cluster();
        bean.setJmxReportingEnabled(false);
        bean.setPoolingOptions(poolingOptions);
        bean.setLoadBalancingPolicy(new TokenAwarePolicy(new RoundRobinPolicy()));

        return bean;
    }

    @Override
    public ReactiveCassandraTemplate reactiveCassandraTemplate() {
        return new ReactiveCassandraTemplate(reactiveCqlTemplate(), cassandraConverter());
    }

    @Bean
    public CassandraEntityInformation getCassandraEntityInformation(CassandraOperations cassandraTemplate) {
        CassandraPersistentEntity<Person> entity =
                (CassandraPersistentEntity<Person>)
                        cassandraTemplate
                                .getConverter()
                                .getMappingContext()
                                .getRequiredPersistentEntity(Person.class);
        return new MappingCassandraEntityInformation<>(entity, cassandraTemplate.getConverter());
    }

    @Override
    public SchemaAction getSchemaAction() {
        return SchemaAction.CREATE_IF_NOT_EXISTS;
    }

    public String getContactPoints() {
        return cassandraHost;
    }

    public String getKeyspaceName() {
        return "mykeyspace";
    }
}

This is the ScyllaPersonRepository referenced in my Cassandra configuration filters.

public interface ScyllaPersonRepository extends ReactiveCassandraRepository<Person, PersonKey> {
    @Query("select id, name from persons where id = ?0")
    Flux<Object> findPersonById(@Param("id") String id);
}

After executing a few queries, the CQL Non-Prepared statements metric in my Scylla Monitoring Dashboard showed that I'm not using prepared statements at all.

I was able to use prepared statements after followed the documentation here which walked me through creating the CQL myself.

public class ScyllaPersonRepository extends SimpleReactiveCassandraRepository<Person, PersonKey> {
    private final Session session;
    private final CassandraEntityInformation<Person, PersonKey> entityInformation;
    private final ReactiveCassandraTemplate cassandraTemplate;
    private final PreparedStatementCache cache = PreparedStatementCache.create();

    public ScyllaPersonRepository(
            Session session,
            CassandraEntityInformation<Person, PersonKey> entityInformation,
            ReactiveCassandraTemplate cassandraTemplate
    ) {
        super(entityInformation, cassandraTemplate);
        this.session = session;
        this.entityInformation = entityInformation;
        this.cassandraTemplate = cassandraTemplate;
    }

    public Flux<ScyllaUser> findSegmentsById(String id) {
        return cassandraTemplate
                .getReactiveCqlOperations()
                .query(
                        findPersonByIdQuery(id),
                        (row, rowNum) -> convert(row)
                );
    }

    private BoundStatement findPersonByIdQuery(String id) {
        return CachedPreparedStatementCreator.of(
                cache,
                QueryBuilder.select()
                        .column("id")
                        .column("name")
                        .from("persons")
                        .where(QueryBuilder.eq("id", QueryBuilder.bindMarker("id"))))
                .createPreparedStatement(session)
                .bind()
                .setString("id", id);
    }

    private Person convert(Row row) {
        return new Person(
                row.getString("id"),
                row.getString("name"));
    }
}

But, I would really like the ORM to handle that all for me. Is it possible to configure this behaviour out of the box, so that I don't need to manually write the CQL myself but instead just enable it as an option in my Cassandra Configuration and get the ORM to orchestrate it all behind the scenes?


Solution

  • Frankly, I think this is a bug(request for enhancement) and it should be filed in Springs Jira. It seems the repository simply doesn't support this out of box(nor did I find any config option how to flip it, but I might have missed it).

    Actually, my theory was correct: https://jira.spring.io/projects/DATACASS/issues/DATACASS-578?filter=allopenissues so just add yourself and try to ask them for resolution.