Skip to content

Manager API

DuyHai DOAN edited this page Jun 9, 2018 · 8 revisions

Below is a list of APIs supported by Achilles Manager:

CRUD

This API performs simple INSERT/DELETE operations on the entity. It exposes the following methods:

  • findById(...): find an entity providing the complete primary key
  • insert(ENTITY instance): insert an instance of the entity
  • insertStatic(ENTITY instance): insert only static columns from the entity
  • update(ENTITY instance): update Cassandra table with non null fields extracted from the entity
  • updateStatic(ENTITY instance): update Cassandra table with non null static fields extracted from the entity
  • delete(ENTITY instance): delete the entity
  • deleteById(...): delete the entity with the provided complete primary key
  • deleteByPartitionKeys(...): delete the entity with the provided complete partition key component(s)

For each of those operations, you can specify runtime values for

  1. Consistency level/Serial consistency level
  2. Retry policy
  3. Fetch size
  4. Outgoing payload
  5. Paging state (useful only for SELECT)
  6. ifExists() for INSERT, ifNotExists() for DELETE
  7. tracing ON/OFF
  8. ResultSet async listener
  9. Row async listener
  10. LightWeight Transaction result listener for INSERT/DELETE

The ResultSet async listener is just a function Function<ResultSet, ResultSet> to spy on the returned raw com.datastax.driver.core.ResultSet object. You are not allowed to call methods that will consume this resultset like one(), all(), iterator(), fetchMoreResults()

The Row async listener is a function Function<Row, Row> and lets you manipulate the raw com.datastax.driver.core.ResultSet object. This time, it is possible to read values from the row because it is already fetched

LightWeight Transaction result listener should implement the interface LWTResultListener

DSL

The DSL API lets you create type-safe SELECT, UPDATE and DELETE queries for your entity.

Select DSL

The Select DSL look like:

   
    User user = manager
        .dsl()
        .select()
        .id()
        .firstname()
        .lastname()
        .fromBaseTable()
        .where()
        .userId().Eq(id)
        .getOne();   

The DSL exposes the following final methods:

  • ENTITY getOne(): get the first found entity

  • CompletableFuture<ENTITY> getOneAsync(): get the first found entity asynchronously

  • Tuple2<ENTITY, ExecutionInfo> getOneWithStats(): get the first found entity with execution info

  • CompletableFuture<Tuple2<ENTITY, ExecutionInfo>> getOneAsyncWithStats(): get the first found entity with execution info asynchronously

  • List<ENTITY> getList(): get found entities

  • CompletableFuture<List<ENTITY>> getListAsync(): get found entities asynchronously

  • Tuple2<List<ENTITY>, ExecutionInfo> getListWithStats(): get found entities with execution info

  • CompletableFuture<Tuple2<List<ENTITY>, ExecutionInfo>> getListAsyncWithStats(): get found entities with execution info asynchronously

  • Iterator<ENTITY> iterator(): return an iterator for found entities

  • TypedMap getTypedMap(): get the first found CQL row as an instance of TypedMap

  • Tuple2<TypedMap, ExecutionInfo> getTypedMapWithStatus(): get the first found CQL row with execution info as an instance of TypedMap

  • CompletableFuture<TypedMap> getTypedMapAsync(): get the first found CQL row asynchronously as an instance of TypedMap

  • CompletableFuture<Tuple2<TypedMap, ExecutionInfo>> getTypedMapAsyncWithStats(): get the first found CQL row with execution info asynchronously an instance of TypedMap

  • List<TypedMap> getTypedMaps(): get the found CQL rows as a list of TypedMap

  • List<Tuple2<TypedMap, ExecutionInfo>> getTypedMapsWithStats(): get the found CQL rows with execution info as a list of TypedMap

  • CompletableFuture<List<TypedMap>> getTypedMapsAsync(): get the found CQL rows asynchronously as a list of TypedMap

  • CompletableFuture<Tuple2<List<TypedMap>, ExecutionInfo>> getTypedMapsAsyncWithStats(): get the found CQL rows with execution info asynchronously as a list of TypedMap

  • Iterator<TypedMap> typedMapIterator(): return an iterator for found CQL rows as TypedMap

It is not mandatory to provide values for the WHERE clause, you can call the method without_WHERE_Clause() to retrieve values

Please note that as soon as you invoke a function call (with .function(), Achilles can only return the results as TypedMap and not as entities) because it cannot map the result of the function call into a field of your entity.

If you need to map the result of a function call into a field of your entity, use the @Computed annotation instead.

Similar to the CRUD API, you can define runtime values for consistency levels, retry policy, ...

Update DSL

The Update DSL look like:

   
    manager
        .dsl()
        .update()
        .fromBaseTable()
        .firstname().Set("new firstname")
        .where()
        .userId().Eq(id)
        .execute();   

The DSL exposes the following final methods:

  • execute(): execute the update
  • CompletableFuture<Empty> executeAsync(): execute the update asynchronously
  • ExecutionInfo executeWithStats(): execute the update and return the execution info
  • CompletableFuture<ExecutionInfo>> executeAsyncWithStats(): execute the update and return the execution info asynchronously

Similar to the CRUD API, you can define runtime values for consistency levels, retry policy, ...

Delete DSL

The Delete DSL look like:

   
    manager
        .dsl()
        .delete()
        .firstname()
        .lastname()
        .fromBaseTable()
        .where()
        .userId().Eq(id)
        .execute();   

The DSL exposes the following final methods:

  • execute(): execute the delete
  • CompletableFuture<Empty> executeAsync(): execute the delete asynchronously
  • ExecutionInfo executeWithStats(): execute the delete and return the execution info
  • CompletableFuture<ExecutionInfo>> executeAsyncWithStats(): execute the delete and return the execution info asynchronously

Similar to the CRUD API, you can define runtime values for consistency levels, retry policy, ...

Indexed Queries

Achilles does support Cassandra native secondary index, SASI or DSE Search.

Please annotate your fields with @Index, @SASI or @DSE_Search

Native secondary index

For native secondary index, only equality ( = ) predicates are allowed. The API is quite simple:

@Table
    public class User {
   
        @PartitionKey
        private UUID user_id;
        
        ...
        
        @Index
        @Column
        private int age;
        
        ... 
    }
    
    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .indexed_age().Eq(32)
        ....

You can restrict the search further by providing partition keys or clustering columns.

    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .indexed_age().Eq(32)
        .user_id().Eq(...)
 

SASI

For SASI , equality and range queries (<, <=, >=, >) are possible for all data types other than text/ascci. For text/ascii types, you can also perform prefix/suffix/substring or full text search, provided you created the index with the appropriate options.

Example:

@Table
    public class User {
   
        @PartitionKey
        private UUID user_id;
        
        ...

        @SASI(indexMode = IndexMode.CONTAINS, analyzed = true, analyzerClass = Analyzer.NON_TOKENIZING_ANALYZER, normalization = Normalization.LOWERCASE)
        @Column
        private String name;

        @SASI(indexMode = IndexMode.PREFIX, analyzed = false)
        @Column        
        private String country:
                
        @SASI
        @Column
        private int age;
        
        ... 
    }
    
    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .indexed_name().Contains("John")
        .indexed_age().Gte_And_Lte(25, 35)
        .indexed_country().Eq("USA")
        ....
        

Again, similar to native secondary index, you can restrict the search by providing partition keys or/and clustering columns

The @SASI annotation exposes the following attributes:

Attribute Possible values Description Default value
indexMode IndexMode.PREFIX, IndexMode.CONTAINS or IndexMode.SPARSE
  • PREFIX (DEFAULT): allows search on prefix for text/ascii data types. Default and only valid index mode for non-text data types
  • CONTAINS: allows search on prefix, suffix and substring for text/ascii data types. Invalid for non-text data types
  • SPARSE: only valid for non-text data types. SPARSE mode is optimized for low-cardinality e.g. for indexed values having 5 or less corresponding rows. If there are more than 5 CQL rows having this index value, SASI will complain by throwing an exception
IndexMode.PREFIX
analyzed true or false Indicates whether the data should be analyzed or not. Setting 'analyzed' = true is only valid for text/ascii data types. Setting 'analyzed' = true is mandatory if 'analyzerClass' is set to:
  • NON_TOKENIZING_ANALYZER
  • STANDARD_ANALYZER
false
analyzerClass Analyzer.NO_OP_ANALYZER, Analyzer.NON_TOKENIZING_ANALYZER or Analyzer.STANDARD_ANALYZER Defines the analyzer class. Available values are:
  • NO_OP_ANALYSER (DEFAULT): do not analyze the input
  • NON_TOKENIZING_ANALYZER: only valid for text/ascii data types. Do not tokenize the input. Normalization by lowercase/uppercase is allowed
  • STANDARD_ANALYZER: only valid for text/ascii data types. Split the input text into tokens, using the locale defined by attribute 'locale' Normalization by lowercase/uppercase is allowed
Please note that setting 'analyzerClass' to NON_TOKENIZING_ANALYZER or STANDARD_ANALYZER also requires setting 'analyzed' to true
Analyzer.NO_OP_ANALYZER
maxCompactionFlushMemoryInMb any integer Maximum size of SASI data to keep in memory during compaction process. If there are more than 'maxCompactionFlushMemoryInMb' worth of index data, SASI will flush them on temporary files on disk before merging all the temp files into a single one. Of course it will add up to compaction duration. No free lunch, sorry 1024 (e.g. 1Gb)
normalization Normalization.NONE, Normalization.LOWERCASE or Normalization.UPPERCASE Defines the normalization to be applied to the input. Available values are:
  • NONE (DEFAULT): no normalization
  • LOWERCASE: normalize input text and search term to lower case
  • UPPERCASE: normalize input text and search term to upper case
Normalization.NONE
locale any valid locale string Defines the locale for tokenization. This attribute is only used when 'analyzerClass' == STANDARD_ANALYZER otherwise it is ignored "en"
enableStemming true or false Enable stemming of input text. This attribute is only used when 'analyzerClass' == STANDARD_ANALYZER false
skipStopWords true or false Enable stemming of input text. This attribute is only used when 'analyzerClass' == STANDARD_ANALYZER false

The following combinations are allowed for index options:

Data type Index Mode Analyzer Class Possible option values
Text or Ascii PREFIX or CONTAINS NoOpAnalyzer
  • analyzed = false (DEFAULT)
  • normalization = NONE (DEFAULT)
  • locale is ignored
  • maxCompactionFlushMemoryInMb (OPTIONAL)
  • enableStemming = false (DEFAULT)
  • skipStopWords = false (DEFAULT)
Text or Ascii PREFIX or CONTAINS NonTokenizingAnalyzer
  • analyzed = true (MANDATORY)
  • normalization (OPTIONAL)
  • locale (OPTIONAL)
  • maxCompactionFlushMemoryInMb (OPTIONAL)
  • enableStemming = false (DEFAULT)
  • skipStopWords = false (DEFAULT)
Text or Ascii PREFIX or CONTAINS StandardAnalyzer
  • analyzed = true (MANDATORY)
  • normalization (OPTIONAL)
  • locale (OPTIONAL)
  • maxCompactionFlushMemoryInMb (OPTIONAL)
  • enableStemming (OPTIONAL)
  • skipStopWords (OPTIONAL)
Non Text PREFIX OR SPARSE NoOpAnalyzer
  • analyzed = false (DEFAULT)
  • normalization = NONE (DEFAULT)
  • locale is ignored
  • maxCompactionFlushMemoryInMb (OPTIONAL)
  • enableStemming = false (DEFAULT)
  • skipStopWords = false (DEFAULT)

DSE Search

DSE Search is only available for users having Datastax Enterprise edition. Please use the @DSE_Search on the fields of your entity to let Achilles generate appropriate search methods.

Please note that this annotation is only valid for Cassandra versions:

  • DSE_4_8
  • DSE_5_0_X
  • DSE_5_1_X

Important: Achilles will NOT attempt to create the index for DSE Search even if doForceSchemaCreation() is set to true.

You should create the index in DSE yourself using dsetool create core .... (please refer to DSE documentation)

Nevertheless, Achilles will check the existence of DSE Search index at runtime and will complain if it cannot be found.

Also, please note that currently OR clause is not yet supported by Achilles. Please use ...where().rawSolrQuery(String rawSolrQuery) to search using OR clauses

Additionally, you need not map the solr_query in your Java bean. Just put the @DSE_Search annotation on the fields you want to search and Achilles will generate the appropriate DSL source code.

This annotation exposes the fullTextSearchEnabled attribute which is only useful on a text/ascii field/column.

If enabled, Achilles will generate:

  • StartWith(String prefix)
  • EndWith(String suffix)
  • Contains(String substring)

methods in addition of the standard Eq(String term) and RawPredicate(String rawSolrPredicate) methods.

Example:

@Table
    public class User {
   
        @PartitionKey
        private UUID user_id;
        
        ...

        @DSE_Search(fullTextSearchEnabled = true)
        @Column
        private String name;

        @DSE_Search
        @Column        
        private String country:
                
        @DSE_Search
        @Column
        private int age;
        
        ... 
    }
    
    //Standard usage
    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .search_on_name().Contains("John")
        .search_on_age().Gte_And_Lte(25, 35)
        .search_on_country().Eq("USA")
        ....
        
    
    //Raw Predicate    
    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .search_on_name().RawPredicate("*Jo??y*")
        ....

    //Raw Solr query with OR predicate
    manager.
        .indexed()
        .select()
        .allColumns_FromBaseTable()
        .where()
        .rawSolrQuery("(name:*John* OR login:jdoe*) AND age:[25 TO 35]")
        ....        
        

GROUP BY

This shiny new feature is only available from Cassandra 3.10 and DSE 5.1.x. To adhere to Cassandra philosophy you can only perform group by on the partition key columns and clustering colums, and only in their declaration order in the table schema.

For example, let's say we have the following table:

CREATE TABLE multi_partition(
  id bigint,
  uuid uuid, 
  clust1 int, 
  clust2 int, 
  clust3 int, 
  val int,
  PRIMARY KEY((id, uuid),clust1, clust2, clust3)
);

The only allowed queries using GROUP BY clause are:

  • SELECT ... FROM multi_partition GROUP BY id,uuid: please note that grouping by both id and uuid is mandatory because those columns define the partition key
  • SELECT ... FROM multi_partition GROUP BY id,uuid,clust1
  • SELECT ... FROM multi_partition GROUP BY id,uuid,clust1,clust2
  • SELECT ... FROM multi_partition GROUP BY id,uuid,clust1,clust2,clust3
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb GROUP BY clust1
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb GROUP BY clust1,clust2
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb GROUP BY clust1,clust2,clust3
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb AND clust1=ccc GROUP BY clust2
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb AND clust1=ccc GROUP BY clust2,clust3
  • SELECT ... FROM multi_partition WHERE id=aaa AND uuid=bbb AND clust1=ccc AND clust2=ddd GROUP BY clust3

Any other combination is forbidden for obvious performance reason.

How do those queries translate into Achilles DSL ?

  • manager.dsl().select()....groupBy().id_uuid()...
  • manager.dsl().select()....groupBy().id_uuid_clust1()
  • manager.dsl().select()... groupBy().id_uuid_clust1_clust2()
  • manager.dsl().select()... groupBy().id_uuid_clust1_clust2_clust3()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).groupBy().clust1()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).groupBy().clust1_clust2()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).groupBy().clust1_clust2_clust3()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).clust1().Eq(...).groupBy().clust2()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).clust1().Eq(...).groupBy().clust2_clust3()
  • manager.dsl().select()... where().id().Eq(...).uuid().Eq(...).clust1().Eq(...).clust2().Eq(...).groupBy().clust3()

And that's it! Simple isn't it ?

Raw Queries

The RAW queries API lets you inject any instance of:

  • com.datastax.driver.core.RegularStatement
  • com.datastax.driver.core.PreparedStatement
  • com.datastax.driver.core.BoundStatement

and execute the query for you.

Typed Query

The Typed Query API execute the statement and map the returned com.datastax.driver.core.Row object(s) back to entity instance. Thus this API can only be used for SELECT statements.

    Statement statement = ...;
    
    User instance = userManager
        .raw()
        .typedQueryForSelect(statement)
        .getOne();

This API exposes the following final methods:

  • ENTITY getOne(): get the first found entity

  • CompletableFuture<ENTITY> getOneAsync(): get the first found entity asynchronously

  • Tuple2<ENTITY, ExecutionInfo> getOneWithStats(): get the first found entity with execution info

  • CompletableFuture<Tuple2<ENTITY, ExecutionInfo>> getOneAsyncWithStats(): get the first found entity with execution info asynchronously

  • List<ENTITY> getList(): get found entities

  • CompletableFuture<List<ENTITY>> getListAsync(): get found entities asynchronously

  • Tuple2<List<ENTITY>, ExecutionInfo> getListWithStats(): get found entities with execution info

  • CompletableFuture<Tuple2<List<ENTITY>, ExecutionInfo>> getListAsyncWithStats(): get found entities with execution info asynchronously

  • Iterator<ENTITY> iterator(): return an iterator for found entities

Similar to the CRUD API, you can define runtime values for consistency levels, retry policy, ...

Native Query

The Native Query API execute the statement and map the returned com.datastax.driver.core.Row object(s) back to instance(s) of TypedMap.

    Statement statement = ...;
    final TypedMap found = userManager
        .raw()
        .nativeQuery(statement)
        .getOne();

This API exposes the following final methods:

  • TypedMap getOne(): get the first found row

  • CompletableFuture<TypedMap> getOneAsync(): get the first found row asynchronously

  • Tuple2<TypedMap, ExecutionInfo> getOneWithStats(): get the first found row with execution info

  • CompletableFuture<Tuple2<TypedMap, ExecutionInfo>> getOneAsyncWithStats(): get the first found row with execution info asynchronously

  • List<TypedMap> getList(): get found entities

  • CompletableFuture<List<TypedMap>> getListAsync(): get found rows asynchronously

  • Tuple2<List<TypedMap>, ExecutionInfo> getListWithStats(): get found rows with execution info

  • CompletableFuture<Tuple2<List<TypedMap>, ExecutionInfo>> getListAsyncWithStats(): get found rows with execution info asynchronously

  • Iterator<ENTITY> iterator(): return an iterator for found rows

  • execute(): execute the statement

  • CompletableFuture<Empty> executeAsync(): execute the statement asynchronously

  • ExecutionInfo executeWithStats(): execute the statement and return the execution info

  • CompletableFuture<ExecutionInfo>> executeAsyncWithStats(): execute the statement and return the execution info asynchronously

Similar to the CRUD API, you can define runtime values for consistency levels, retry policy, ...

Dynamic Schema Name

In some multi-tenant environment, the keyspace/table name cannot be known ahead of time but only during runtime. For this purpose, Achilles introduces a SchemaNameProvider interface to let people bind keyspace/table names dynamically at runtime.

This provider can be used with CRUD API and DSL API:

	
	final SchemaNameProvider dynamicProvider = ...;
 	
	userManager
		.crud()
		.withSchemaNameProvider(dynamicProvider)
		...
		.execute();
 		
	userManager
		.dsl()
		.select()
		...
		.from(dynamicProvider)
		.where()
		...
     		
	userManager
		.dsl()
		.update()
		.from(dynamicProvider)
		...
		.where()
		...

	userManager
		.dsl()
		.delete()
		...
		.from(dynamicProvider)
		...
		.where()
		... 
		

Raw Statement Generation

The Manager instance also exposes the following methods to generate raw com.datastax.driver.core.BoundStatement and bound values. They are available for all both CRUD API and DSL API

  • BoundStatement generateAndGetBoundStatement():
    • generate the prepared statement or get it from the statement cache if it already exists
    • extract values from entity or from the given API
    • bind values to the prepared statement
    • return the com.datastax.driver.core.BoundStatement instance
  • String getStatementAsString(): self-explanatory
  • List<Object> getBoundValues(): extract raw Java values from entity or from the given API
  • List<Object> getEncodedBoundValues(): similar as getBoundValues() but encode the values using the Codec System

Other Methods

Apart from the 3 API (CRUD, DSL and Query), the Manager class also exposes some utility methods:

  • public ENTITY mapFromRow(Row row): map a given instance of com.datastax.driver.core.Row to an instance of entity
  • public Session getNativeSession(): return the native com.datastax.driver.core.Session object used by this Manager
  • public Cluster getNativeCluster(): return the native com.datastax.driver.core.Cluster object used by this Manager

Home

Clone this wiki locally