Introduction
Apache Cassandra is a massively scalable open source NoSQL database.
Cassandra can be used for managing large amounts of data across multiple data centers and the cloud.
The goal of this grails-data-mapping subproject is to provide a as-complete-as-possible GORM implemenatition that maps domain classes and instances to Cassandra 2.0 tables.
This project aims to provide an object-mapping layer on top of Cassandra to ease common activities such as:
-
Marshalling from Cassandra to Groovy/Java types and back again
-
Support for GORM dynamic finders, criteria and named queries
-
Session-managed transactions
-
Validating domain instances backed by the Cassandra datastore
-
Cassandra schema creation
Compatibility with GORM for Hibernate
This implementation tries to be as compatible as possible with GORM for Hibernate. In general you can refer to the GORM documentation and the "Domain Classes" section of the reference guide (see the right nav) for usage information.
The following key features are supported by GORM for Cassandra:
-
Simple persistence methods
-
Dynamic finders
-
Criteria queries
-
Named queries
-
Inheritance
-
Embedded types
-
Query by example
However, some features are not supported:
-
HQL queries
-
Associations (these should be supported in the future)
-
Any direct interaction with the Hibernate API
-
Custom Hibernate user types
-
Queries using OR, LIKE, NOT EQUAL, IS NULL, negation, property comparison, offset in paging, most projections (mainly because Cassandra itself doesn’t support these)
-
Table-per-hierarchy mapping
There may be other limitations not mentioned here so in general it shouldn’t be expected that an application based on GORM for Hibernate will "just work" without some tweaking involved. Having said that, the large majority of common GORM functionality is supported.
Getting Started
To get started with GORM for Cassandra you need configure it as a dependency in build.gradle
:
dependencies {
compile 'org.grails.plugins:cassandra:VERSION'
}
The next step is to setup a running Cassandra server. Refer to the Apache Cassandra Getting Started Guide for an explanation on how to setup and startup a Cassandra instance. Once installed, starting Cassandra is typically a matter of executing the following command:
CASSANDRA_HOME/bin/cassandra -f
With the above command executed in a terminal window you should see output like the following appear:
INFO 00:11:16,935 Starting listening for CQL clients on localhost/127.0.0.1:9042...
INFO 00:11:17,013 Using TFramedTransport with a max frame size of 15728640 bytes.
INFO 00:11:17,014 Binding thrift service to localhost/127.0.0.1:9160
INFO 00:11:17,042 Using synchronous/threadpool thrift server on localhost : 9160
INFO 00:11:17,042 Listening for thrift clients...
As you can see the server listens for CQL clients on port 9042, but don’t worry the Cassandra plugin for Grails will automatically configure itself to look for Cassandra on that port by default.
If you want to configure how Grails connects to Cassandra then you can do so using the following settings in grails-app/conf/application.yml
:
grails:
cassandra:
contactPoints: localhost
port: 9042
keyspace:
name: foo
}
}
Spring Boot 1.4.x and above includes Cassandra Auto Configuration and by default in Grails 3.2.x above you will need to configure Cassandra using the appropriate Spring Boot configuration unless you exclude the auto configuration in your Application class. For example:
|
import org.springframework.boot.autoconfigure.cassandra.*
import org.springframework.boot.autoconfigure.data.cassandra.*
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
@EnableAutoConfiguration(exclude=[CassandraAutoConfiguration, CassandraDataAutoConfiguration])
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application, args)
}
}
Using GORM for Cassandra Standalone
If you plan to use Cassandra as your primary datastore then you need to remove the Hibernate plugin from your build.gradle
file.
compile 'org.grails.plugins:hibernate'
With this done all domain classes in grails-app/domain will be persisted via Cassandra and not Hibernate. You can create a domain class by running the regular create-domain-class
command:
grails create-domain-class Person
The Person
domain class will automatically be a persistent entity that can be stored in Cassandra. Example:
class Person {
String firstName
String lastName
}
def person = new Person(firstName: "Fred", lastName: "Flintstone")
person.save()
...
def person2 = Person.get(uuid)
Combining Cassandra and Hibernate
If you have both the Hibernate and Cassandra plugins installed then by default all classes in the grails-app/domain
directory will be persisted by Hibernate and not Cassandra.
If you want to persist a particular domain class with Cassandra then you must use the mapWith
property in the domain class:
static mapWith = "cassandra"
By default the Hibernate plugin transparently adds an implicit id
property of type Long
to your domain class. The Cassandra plugin adds an id
property of type UUID
.
If you install both plugins, the id
property will be of type Long
.
So if you have a domain class with an auto-generated id
(the default behaviour) and you want to save it to both datastores, you should define a UUID id
property as a Long
won’t really work for Cassandra. You also need to set the id’s generator
attribute so that the Hibernate plugin can auto-generate a UUID
.
Advanced Configuration
Cassandra Database Connection Configuration
As mentioned above, the GORM for Cassandra plugin will configure itself with default settings, but if you wish to customize those defaults you can do so in the grails-app/conf/application.groovy
file:
grails {
cassandra {
contactPoints = "localhost"
port = 9042
schemaAction = "recreate-drop-unused"
keyspace {
name = "foo"
action = "create"
}
}
}
The keyspace name
property configures the default keyspace to use. If it’s not specified the keyspace
used will default to the name of your application.
In production scenarios you will typically use more than one Cassandra node:
grails {
cassandra {
contactPoints = "192.168.0.1, 192.168.0.2" //comma-separated list of hostnames or IP addresses of nodes to connect to
}
}
Keyspace creation
If you want the plugin to automatically create the application’s keyspace you can specify a value for the keyspace action
property:
grails {
cassandra {
keyspace {
name = "foo"
action = "create"
}
}
}
Valid options for action
are:
-
create - Create the keyspace if it doesn’t exist
-
create-drop - Drop the keyspace and create it if doesn’t exist
Schema creation
The plugin can automatically create the Cassandra tables and indexes required for your domain model. You have some control over when and how it does this through the dbCreate
property, which can take these values:
-
none - The default. Do not create any schema objects.
-
create - Create a table and indexes for each domain class on startup. Fail if a table already exists.
-
recreate - Create a table and indexes for each domain class on startup, dropping the table first if it exists.
-
recreate-drop-unused - Drop all tables in the keyspace, then create a table and indexes for each domain class on startup.
-
update - TO IMPLEMENT
Example:
grails {
cassandra {
dbCreate = "recreate-drop-unused"
}
}
You can remove the dbCreate property completely, which is recommended once your schema is relatively stable and definitely when your application and database are deployed in production.
Configuration Options Guide
Below is a complete example showing all configuration options, including keyspace options:
grails {
cassandra {
contactPoints = "localhost" //comma-separated list of hostnames or IP addresses of nodes to connect to
port = 9042 //the port to connect to
dbCreate = "recreate" //the strategy to create cassandra tables and indexes for domain classes, default: "none"
stateless = false // whether to use stateless sessions by default
keyspace {
name = "foo" //the name of the keyspace to use, default: the name of the application
action = "create" //whether to create a keyspace, default: no keyspace created
//keyspace properties to set only if the plugin is to create the keyspace
durableWrites = false //default: false
replicationStrategy = "SIMPLE_STRATEGY" OR "NETWORK_TOPOLOGY_STRATEGY" //default: "SIMPLE_STRATEGY"
replicationFactor = 1 //default: 1
dataCenter = [1,[us-west] 2][eu-west] //if replicationStrategy is "NetworkTopologyStrategy",
//a map of data centre names and replication factors
}
}
}
Global Mapping Configuration
Using the grails.cassandra.default.mapping
setting in application.groovy
you can configure global mapping options across your domain classes.
The following configuration will disable optimistic locking globally:
grails.cassandra.default.mapping = {
version false
}
Using GORM in Spring Boot
To use GORM for Hibernate in Spring Boot add the necessary dependencies to your Boot application:
compile("org.grails:gorm-cassandra-spring-boot:VERSION")
Note that if you get exceptions during startup or execution for newer versions of Spring Boot (1.3 and above) you may need to enforce the dependencies:
dependencies {
compile("org.grails:gorm-cassandra-spring-boot:VERSION")
compile("com.datastax.cassandra:cassandra-driver-core:2.0.4")
compile "org.springframework.data:spring-data-commons:1.8.4.RELEASE"
compile("org.springframework.data:spring-data-cassandra:1.0.4.RELEASE") {
exclude group:'org.apache.cassandra',module:'cassandra-all'
}
}
Ensure your Boot Application
class is annotated with ComponentScan
, example:
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.*
@Configuration
@EnableAutoConfiguration
@ComponentScan
class Application {
static void main(String[] args) {
SpringApplication.run Application, args
}
}
Using ComponentScan without a value results in Boot scanning for classes in the same package or any package nested within the Application class package.
If your GORM entities are in a different package specify the package name as the value of the ComponentScan annotation.
|
Finally create your GORM entities and ensure they are annotated with grails.persistence.Entity
:
import grails.persistence.*
@Entity
class Person {
String firstName
String lastName
}
GORM for Cassandra outside Grails
If you wish to use GORM for Cassandra outside of a Grails application you should declare the necessary dependencies, for example in Gradle:
compile "org.grails:grails-datastore-gorm-cassandra:VERSION"
Then annotate your entities with the grails.gorm.annotation.Entity
annotation:
@Entity
class Person {
String name
}
Then you need to place the bootstrap logic somewhere in your application which uses CassandraDatastoreSpringInitializer
:
def initializer = new CassandraDatastoreSpringInitializer(Person)
def applicationContext = initializer.configure()
println Person.count()
For configuration you can either pass a map or an instance of the org.springframework.core.env.PropertyResolver
interface:
def initializer = new CassandraDatastoreSpringInitializer(['grails.cassandra.port':7891], Person)
def applicationContext = initializer.configure()
println Person.count()
If you are using Spring with an existing ApplicationContext
you can instead call configureForBeanDefinitionRegistry
prior to refreshing the context. You can pass the Spring Environment
object to the constructor for configuration:
ApplicationContext myApplicationContext = ...
def initializer = new CassandraDatastoreSpringInitializer(myApplicationContext.getEnvironment(), Person)
initializer.configureForBeanDefinitionRegistry(myApplicationContext)
println Person.count()
Mapping to Cassandra Tables
Basic Mapping
The way the GORM for Cassandra plugin works is to map each domain class to a Cassandra table. For example given a domain class such as:
class Person {
String firstName
String lastName
}
This will map onto a Cassandra table called "person" and generate the following table if schema creation is on:
CREATE TABLE person (id uuid, firstname text, lastname text, version bigint, PRIMARY KEY (id));
The plugin transparently adds an implicit id property of type UUID which is auto-generated when an entity is saved.
|
Data types
In general a property’s Java type maps onto a CQL3 data type as listed here
Some Java types can map onto more than one CQL3 data type, the default mappings are shown in bold:
* java.util.UUID
- CQL uuid or timeuuid
* java.lang.String
- CQL text or ascii
or varchar
* long
/ java.lang.Long
- CQL bigint or counter
Java byte
and short
map onto CQL int
.
To map onto a different CQL type specify the type
attribute in the mapping. Example:
class Person {
String firstName
String lastName
UUID timeuuid
String ascii
String varchar
long counter
static mapping = {
timeuuid type:"timeuuid"
ascii type:'ascii'
varchar type:'varchar'
counter type:'counter'
}
}
Embedded Collections and Maps
You can map embedded lists, sets and maps of standard CQL data types simply by defining the appropriate collection type:
class Person {
String firstName
String lastName
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
...
new Person(friends:['Fred', 'Bob'], pets:[chuck:"Dog", eddie:'Parrot']).save()
There are certain limitations on collections and only the standard CQL data types can be stored inside embedded collections and maps.
When persisting a domain class containing embedded collections or maps using the save
method, the entire collection or map is saved or updated to Cassandra.
This may not be appropriate if you only want to persist the non-collection properties, in which case you can use the updateSimpleTypes
instance method.
Example:
def person = Person.get(uuid)
person.age = 31
person.updateSimpleTypes(flush:true)
If you want to add or remove an item from a collection or map and only have that change updated to Cassandra you can use the various dynamic methods listed in the "Domain Classes" section of the right nav. Example:
person.prependToScores(5)
Person.appendToFriends(person.id, 'Barney')
Person.deleteFromPets(person.id, 'eddie', [flush:true])
The last flush:true
argument causes the session to flush the pending collection updates to the datastore.
Customized Database Mapping
You may wish to customize how a domain class maps onto a Cassandra table. This is possible using the mapping
block as follows:
class Person {
..
static mapping = {
table "the_person"
}
}
In this example we see that the Person
entity has been mapped to a table called "the_person".
You can also control how an individual property maps onto a table column (the default is to use the property name itself):
class Person {
..
static mapping = {
firstName column:"first_name"
}
}
Identity Generation
By default in Cassandra GORM domain classes are supplied with a UUID-based identifier. So for example the following entity:
class Person {}
Has a property called id
of type java.util.UUID
. In this case GORM for Cassandra will generate a UUID identifier using java.util.UUID.randomUUID().
For a timeuuid
it will generate one using the Java driver
You can name the id
property something else, in which case you have to set the name
attribute in the mapping:
class Person {
UUID primary
static mapping = {
id name:"primary"
}
}
Assigned Identifiers
If you want to manually assign an identifier, the following mapping should be used:
class Person {
String firstName
String lastName
static mapping = {
id name:"lastName", generator:"assigned"
}
}
Note that it is important to specify generator:"assigned" so GORM can work out whether you are trying to achieve an insert or an update. Example:
def p = new Person(lastName:"Wilson")
// to insert
p.save()
// to update
p.save()
An existing manually-assigned entity will only be updated with the save
method if it is in the current persistence session.
Otherwise GORM will try to insert the entity again, which will result in an upsert to Cassandra (with no version checking if versioning is on).
So if the entity is not in the session and you want to explicitly direct an update to Cassandra then use the update
method instead of the save
method.
Example:
def p = new Person(lastName:"Wilson")
// to insert
p.save()
session.clear() or p.discard()
// to update
p.update()
Compount Primary Keys
In Cassandra, a compound primary key consists of more than one column and treats only one column as the partition key. The other columns are treated as clustering columns.
To define a compound primary key on a domain class, each property that is part of the key has to be defined in the mapping block with a primaryKey
map. Example:
class Person {
String lastName
String firstName
Integer age = 0
String location
static mapping = {
id name:"lastName", primaryKey:[ordinal:0, type:"partitioned"], generator:"assigned"
firstName index:true, primaryKey:[ordinal:1, type: "clustered"]
age primaryKey:[ordinal:2, type: "clustered"]
version false
}
}
The above mapping will generate the following Cassandra table if schema creation is on:
CREATE TABLE person (lastname text, firstname text, age int, location text, PRIMARY KEY (lastname, firstname, age))
Composite Partition Key
A composite partition key consists of multiple columns and treats more than one column as the partition key. The other columns are treated as clustering columns.
To define a composite partition key on a domain class, each property that is a part of the key has to have its primaryKey type
attribute set to "partitioned". Example:
class Person {
String lastName
String firstName
Integer age = 0
String location
static mapping = {
id name:"lastName", primaryKey:[ordinal:0, type:"partitioned"], generator:"assigned"
firstName index:true, primaryKey:[ordinal:1, type: "partitioned"]
age index:true, primaryKey:[ordinal:0, type: "clustered"]
version false
}
}
...
CREATE TABLE person (lastname text, firstname text, age int, location text, PRIMARY KEY ((lastname, firstname), age))
The mapping block
The first column of the partition key is always mapped using id
, and then the name of the actual property.
You should then add the primaryKey
map to all columns of the compound/composite primary key. The two attributes are:
* ordinal
- specifies the order of the column in the compound/composite primary key.
* type
- "partitioned" or "clustered". For a compound primary key only one property is type "partitioned" and the rest are type "clustered". For a composite partition key more than one property is type "partitioned".
Persistence and Querying for Compound/Composite Primary Key domain classes
Where you need to pass in an id to a persistence or query method, use a map containing the components of the compound/composite primary key instead.
Example:
def person = Person.get([firstName:"Bob", lastName: "Wilson", age: 25])
Person.updateProperties([firstName:"Bob", lastName: "Wilson", age: 25], [location: "London"], [flush:true])
Query Indexes
Basics
Cassandra doesn’t require that you specify indices to query. Cassandra supports creating an index on most columns, including a clustering column of a compound primary key or on the partition (primary) key itself. Indexing can impact performance greatly. Before creating an index, be aware of when and when not to create an index.
With that in mind it is important to specify the properties you plan to query using the mapping block:
class Person {
String name
static mapping = {
name index:true
}
}
The above mapping will generate the following Cassandra index if schema creation is on:
CREATE INDEX IF NOT EXISTS ON person (name)
Table Properties
You can configure clustering order, caching, compaction, and a number of other operations that Cassandra performs on a new table.
Clustering order
An explanation is provided in the Cassandra docs. Clustering order can only be used on a clustered primary key, to use it set its order attribute. Example:
class Person {
..
static mapping = {
id name:"lastName", primaryKey:[ordinal:0, type:"partitioned"], generator:"assigned"
firstName index:true, primaryKey:[ordinal:1, type: "clustered"], order: "desc" //or order: "asc"
}
}
Setting a table property
Available properties and their descriptions are defined in the Cassandra docs.
If you want to set a table property, define a static tableProperties
block.
Below is an example of the properties you can set with Cassandra GORM:
class Person {
..
static mapping = {
id name:"lastName", primaryKey:[ordinal:0, type:"partitioned"]
firstName primaryKey:[ordinal:1, type: "clustered"], order:"desc"
}
static tableProperties = {
comment "table comment"
"COMPACT STORAGE" true //OR "compact_storage" true
replicate_on_write false
caching "all"
bloom_filter_fp_chance 0.2
read_repair_chance 0.1
dclocal_read_repair_chance 0.2
gc_grace_seconds 900000
compaction class: "SizeTieredCompactionStrategy", bucket_high: 2.5, bucket_low: 0.6, max_threshold: 40, min_threshold: 5, min_sstable_size: 60
compression sstable_compression: "LZ4Compressor", chunk_length_kb: 128, crc_check_chance: 0.85
}
}
The above mapping will generate the following Cassandra table if schema creation is on:
CREATE TABLE person (lastname text, firstname text, version bigint, PRIMARY KEY (lastname, firstname))
WITH CLUSTERING ORDER BY (firstname DESC) AND comment = 'table comment' AND COMPACT STORAGE AND replicate_on_write = 'false'
AND caching = 'all' AND bloom_filter_fp_chance = 0.2 AND read_repair_chance = 0.1 AND dclocal_read_repair_chance = 0.2
AND gc_grace_seconds = 900000
AND compaction = { 'class' : 'SizeTieredCompactionStrategy', 'bucket_high' : 2.5, 'bucket_low' : 0.6, 'max_threshold' : 40, 'min_threshold' : 5,
'min_sstable_size' : 60 }
AND compression = { 'sstable_compression' : 'LZ4Compressor', 'chunk_length_kb' : 128, 'crc_check_chance' : 0.85 };
Table property options
-
comment : String
-
"COMPACT STORAGE" OR "compact_storage" : boolean
-
replicate_on_write : boolean
-
caching : "all", "keys_only", "rows_only", "none"
-
bloom_filter_fp_chance : double
-
read_repair_chance : double
-
dclocal_read_repair_chance : double
-
gc_grace_seconds : long
-
compaction : [class, tombstone_threshold, tombstone_compaction_interval, min_sstable_size, min_threshold, max_threshold, bucket_low, bucket_high, sstable_size_in_mb]
-
compression : [sstable_compression, chunk_length_kb, crc_check_chance]
Stateless Mode
GORM for Cassandra supports both stateless and stateful modes for mapping domain classes to Cassandra. In general stateful mapping is superior for write heavy applications and stateless mode better for read heavy applications (particularily when large amounts of data is involved).
Stateful mode
Domain classes are by default stateful, which means when they are read from Cassandra their state is stored in the user session (which is typically bound to the request in Grails). This has several advantages for write heavy applications:
-
GORM can automatically detect whether a call to save() is a an update or an insert and act appropriately
-
GORM can store the current version and therefore implement optimistic locking
-
Repeated reads of the same entity can be retrieved from the cache, thus optimizing reads as well
An example of when a stateful domain class is better is batching (TO BE IMPLEMENTED)
Stateless Domain classes
However, stateful domain classes can cause problems for read-heavy applications. Take for example the following code:
def books = Book.list() // read 100,000 books
for(b in books) {
println b.title
}
The above example will read 100,000 books and print the title of each. In stateful mode this will almost certainly run out of memory as each Cassandra row is stored in user memory as is each book. Rewriting the code as follows will solve the problem:
Book.withStatelessSession {
def books = Book.list() // read 100,000 books
for(b in books) {
println b.title
}
}
Alternatively you can map the domain class as stateless, in which case its state will never be stored in the session:
class Book {
...
static mapping = {
stateless true
}
}
Disadvantages of Stateless Mode
There are several disadvantages to using stateless domain classes as the default. One disadvantage is that if you are using assigned identifiers GORM cannot detect whether you want to do an insert or an update so you have to be explicit about which one you want:
def b = new Book(id:"The Book")
b.insert()
b.revenue = 100
b.update()
In the above case we use the explicit insert or 'update method to tell Cassandra GORM what to do.
Querying and Persistence Options
The GORM for Cassandra plugin supports passing various options to Cassandra when querying or persisting.
Querying Options
Limiting results
Using the max
parameter, you can specify that the query return a limited number of rows. Example:
def people = Person.list(max:2)
Note that Cassandra does not support the offset
parameter, so you cannot page results this way.
Fetch Size
Setting the fetch size, or number of rows returned simultaneously by a select query, is typically needed when queries return extremely large numbers of rows.
To do this you can use the fetchSize
argument:
def people = Person.list([fetchSize: 200])
people = Person.findAllByFirstName("Barney", [fetchSize: 200])
Setting the fetch size to small values is discouraged as it will yield very poor performance.
In some cases you may want or have to disable paging entirely, for example when using order by and IN, in which case set the fetchSize
to Integer.MAX_VALUE. Example:
People.createCriteria().list {
'in' "lastName", ["Flintstone", "Rubble"]
order "name"
fetchSize Integer.MAX_VALUE
}
Allow filtering
When you attempt a potentially expensive query Cassandra may throw an exception mentioning ALLOW FILTERING.
To run the query, you can use the allowFiltering
argument which is passed onto Cassandra. Imposing a limit using the max
parameter is recommended to reduce memory used.
Example:
def people = Person.findAllByFirstNameAndAgeLessThanEquals('Barney', 35, [allowFiltering:true, max:5])
def person = Person.findOrSaveWhere(firstName: 'Rubble', age: 35, [allowFiltering:true, flush:true])
def criteria = Person.createCriteria()
people = criteria.list (allowFiltering:true, max:5) {
and {
eq('firstName', 'Fred')
eq('age', 40)
}
}
people = criteria.list {
projections {
eq('firstName', 'Bob')
between("age", 40, 43)
}
allowFiltering true
max 5
}
Cassandra Low-level API
A lower level API is provided by the plugin that is based on the Spring Data Cassandra project.
Spring Data Cassandra provides a CassandraTemplate with methods to execute statements using the regular Cassandra Java Driver
To get hold of the cassandraTemplate
instance inside a controller or service simply define a cassandraTemplate
property. An example can be seen below:
def cassandraTemplate
def myAction = {
def people = []
people << new Person(firstName: "Fred", lastName: "Flintstone")
people << new Person(firstName: "Barney", lastName: "Rubble")
cassandraTemplate.insert(people)
}
You can also create a CQL or Java Driver statement and execute it using cassandraTemplate
Select personSelect = QueryBuilder.select().all().from("person")
List personList = cassandraTemplate.select(personSelect, Person.class)
String personCQL = "SELECT firstName FROM person"
List personList = cassandraTemplate.select(personCQL, Person.class)
Reference
Beans
cassandraTemplate
CassandraTemplate
Purpose
A Spring bean that provides access to the lower level CassandraTemplate API
Examples
def cassandraTemplate
def myAction = {
def person = cassandraTemplate.selectOneById(Person.class, uuid)
person.age = 30
cassandraTemplate.update(person)
}
Description
See the CassandraTemplate docs, the Cassandra Java Driver docs, and the Cassandra Java Driver API for API usage info.
Domain Classes
appendTo
appendTo*
Purpose
Adds an element to an existing instance’s embedded set, list or map, where the actual collection or map added to is indicated by the property used as the suffix to the method.
Examples
class Person {
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.appendToScores(5)
Person.appendToFriends(person.id, 'Barney', [flush:true])
Description
The appendTo* method is a dynamic method that adds an element to an embedded set, list or map. In the case of a list it adds the element to the end of the list. The method exists as a static and instance method. The static method simply adds the element to the datastore, the instance method adds the element to the in-memory instance and the datastore.
Argument: * id - the id or primary key map of the instance. Only required if using the static method. * element - the element to append * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
deleteAtFrom
deleteAtFrom*
Purpose
Removes an element at a specified index from an existing instance’s embedded list, where the actual list removed from is indicated by the property used as the suffix to the method.
Examples
class Person {
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.deleteAtFromScores(1)
Person.deleteAtFromScores([id: person.id], 2, [flush:true])
Description
The deleteAtFrom* method is a dynamic method that removes an element at the specified position in an embedded list. The method exists as a static and instance method. The static method simply removes the element in the datastore, the instance method removes the element in the in-memory instance and the datastore.
Argument: * id - the id or primary key map of the instance. Only required if using the static method. * index - the index of the element to remove * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
deleteFrom
deleteFrom*
Purpose
Removes an element from an existing instance’s embedded set, list or map, where the actual collection or map removed from is indicated by the property used as the suffix to the method.
Examples
class Person {
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.deleteFromScores(5)
Person.deleteFromFriends(person.id, 'Barney', [flush:true])
Person.deleteFromPets(person.id, 'eddie', [flush:true])
Description
The deleteFrom* method is a dynamic method that removes an element from an embedded set, list or map. In the case of a map it removes the entry keyed by the specified element. The method exists as a static and instance method. The static method simply removes the element from the datastore, the instance method removes the element from the in-memory instance and the datastore.
Argument: * id - the id or primary key map of the instance. Only required if using the static method. * element - the element to remove, or the key of the entry to remove from a map * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
prependTo
prependTo*
Purpose
Prepends an element to an existing instance’s embedded list, where the actual list prepended to is indicated by the property used as the suffix to the method.
Examples
class Person {
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.prependToScores(5)
Person.prependToScores(5(person.id, 6, [flush:true])
Description
The prependTo* method is a dynamic method that adds an element to the start of an embedded list. The method exists as a static and instance method. The static method simply adds the element to the datastore, the instance method adds the element to the in-memory instance and the datastore.
Argument: * id - the id or primary key map of the instance. Only required if using the static method. * element - the element to prepend * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
replaceAtIn
replaceAtIn*
Purpose
Replaces an element at a specified index in an existing instance’s embedded list, where the actual list used is indicated by the property used as the suffix to the method.
Examples
class Person {
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.replaceAtInScores(1, 9)
Person.replaceAtInScores([id: person.id], 2, 10, [flush:true])
Description
The replaceAtIn* method is a dynamic method that replaces an element at the specified position in an embedded list. The method exists as a static and instance method. The static method simply replaces the element in the datastore, the instance method replaces the element in the in-memory instance and the datastore.
Argument: * id - the id or primary key map of the instance. Only required if using the static method. * index - the index of the element to replace * element - element to be stored at the specified position * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
updateProperties
updateProperties
Purpose
Static method that updates multiple properties on an existing instance in the datastore.
Examples
Book.updateProperties(uuid, [revenue : 300, sold : 10])
Person.updateProperties([firstName:"Fred", lastName: "Flintstone"], [age : 30, email: "[email protected]"], [flush:true])
Description
Allows an update to multiple properties of an existing instance without having to get it from the datastore first. Will not validate the properties nor update the version column for optimistic locking.
Argument: * id - the id or primary key map of the instance to update * properties - a map of property name/value pairs to update * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
updateProperty
updateProperty
Purpose
Static method that updates a single property on an existing instance in the datastore.
Examples
Book.updateProperty(uuid, "revenue", 300)
Person.updateProperty([firstName:"Fred", lastName: "Flintstone"], "friends", ["Barney"], [flush:true])
Description
Allows an update to a property of an existing instance without having to get it from the datastore first. Will not validate the property nor update the version column for optimistic locking.
Argument: * id - the id or primary key map of the instance to update * propertyName - the name of the property to update * value - the new value of the property * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately
updateSingleTypes
updateProperty
Purpose
Updates non collection, non map types only on an existing instance in the datastore. Useful when you don’t want to update the entire embedded collection or map on an instance when updating.
Examples
class Person {
..
int age
List<Integer> scores
Set<String> friends
Map<String, String> pets
}
def person = Person.get(uuid)
person.age = 31
person.updateSimpleTypes(flush:true)
Description
The regular save() method would update all the elements in an embedded collection or map even if not required. This method allows an update of only the single types on an existing instance.
Argument: * flush (optional) - When set to true flushes the persistence context, updating the datastore immediately