· 10 years ago · Sep 06, 2016, 06:22 PM
1package agorapulse.core.dynamo
2
3import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
4import com.amazonaws.services.dynamodbv2.datamodeling.*
5import com.amazonaws.services.dynamodbv2.model.*
6import grails.plugin.awssdk.AmazonWebService
7import org.apache.log4j.Logger
8
9import java.lang.reflect.Field
10import java.lang.reflect.Method
11import java.text.ParseException
12import java.text.SimpleDateFormat
13
14abstract class DynamoDBService {
15
16 static transactional = false
17
18 static final int BATCH_DELETE_LIMIT = 100
19 static final int DEFAULT_QUERY_LIMIT = 20
20 static final int DEFAULT_COUNT_LIMIT = 100
21 static final String SERIALIZED_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
22 static final String SERIALIZED_DATE_TIMEZONE = 'GMT'
23 static final int WRITE_BATCH_SIZE = 100 // Max number of elements to write at once in DynamoDB (mixed tables)
24
25 static protected SimpleDateFormat dateFormatter = new SimpleDateFormat(SERIALIZED_DATE_FORMAT)
26
27 // Set as protected to be accessible from Closures
28 protected AmazonDynamoDBClient client
29 protected String hashKeyName
30 protected Class hashKeyClass
31 protected log = Logger.getLogger(DynamoDBService.class)
32 protected DynamoDBMapper mapper
33 protected Class mainClass
34 protected DynamoDBTable mainTable
35 protected String rangeKeyName
36 protected Class rangeKeyClass
37 protected List<String> secondaryIndexes = new ArrayList<String>()
38
39 /**
40 * Initialize service for a given mapper class
41 *
42 * @param mainClass
43 * @param amazonWebService
44 */
45 protected void init(Class mainClass,
46 AmazonWebService amazonWebService) {
47 if (amazonWebService) { // Ignore when null amazonWebService is passed during Spock tests
48 assert amazonWebService?.dynamoDBMapper
49 this.client = amazonWebService.dynamoDB
50 this.mapper = amazonWebService.dynamoDBMapper
51 this.mainClass = mainClass
52 this.mainTable = (DynamoDBTable) mainClass.getAnnotation(DynamoDBTable.class)
53 dateFormatter.timeZone = TimeZone.getTimeZone(SERIALIZED_DATE_TIMEZONE)
54
55 if (!mainTable) {
56 throw new RuntimeException("Missing @DynamoDBTable annotation on class: ${mainClass}")
57 }
58
59 mainClass.getDeclaredMethods().findAll { Method method ->
60 method.name.startsWith('get') || method.name.startsWith('is')
61 }.each { Method method ->
62 // Get hash key
63 if (method.getAnnotation(DynamoDBHashKey.class)) {
64 hashKeyName = ReflectionUtils.getFieldNameByGetter(method, true)
65 hashKeyClass = mainClass.getDeclaredField(hashKeyName).type
66 }
67 // Get range key
68 if (method.getAnnotation(DynamoDBRangeKey.class)) {
69 rangeKeyName = ReflectionUtils.getFieldNameByGetter(method, true)
70 rangeKeyClass = mainClass.getDeclaredField(rangeKeyName).type
71 }
72 // Get secondary indexes
73 DynamoDBIndexRangeKey indexRangeKeyAnnotation = method.getAnnotation(DynamoDBIndexRangeKey.class)
74 if (indexRangeKeyAnnotation) {
75 secondaryIndexes.add(indexRangeKeyAnnotation.localSecondaryIndexName())
76 }
77 }
78 }
79 }
80
81 /**
82 * Optional settings:
83 * - consistentRead (default to false)
84 * - limit (default to DEFAULT_COUNT_LIMIT)
85 *
86 * @param hashKey
87 * @param rangeKeyName
88 * @param rangeKeyValue
89 * @param operator
90 * @param settings
91 * @return
92 */
93 int count(hashKey,
94 String rangeKeyName,
95 rangeKeyValue,
96 ComparisonOperator operator = ComparisonOperator.EQ,
97 Map settings = [:]) {
98 Map conditions = [(rangeKeyName): buildCondition(rangeKeyName, rangeKeyValue, operator)]
99 countByConditions(hashKey, conditions, settings)
100 }
101
102 /**
103 * Optional settings:
104 * - consistentRead (default to false)
105 * - limit (default to DEFAULT_COUNT_LIMIT)
106 *
107 * @param hashKey
108 * @param rangeKeyName
109 * @param rangeKeyDates
110 * @param settings
111 * @return
112 */
113 int countByDates(hashKey,
114 String rangeKeyName,
115 Map rangeKeyDates,
116 Map settings = [:]) {
117 Map conditions = buildDateConditions(rangeKeyName, rangeKeyDates)
118 countByConditions(hashKey, conditions, settings)
119 }
120
121 /**
122 * Optional settings:
123 * - consistentRead (default to false)
124 * - limit (default to DEFAULT_COUNT_LIMIT)
125 *
126 * @param hashKey
127 * @param rangeKeyConditions
128 * @param settings
129 * @return
130 */
131 int countByConditions(hashKey,
132 Map<String, Condition> rangeKeyConditions,
133 Map settings = [:]) {
134 settings.batchGetDisabled = true
135 if (!settings.limit) {
136 settings.limit = DEFAULT_COUNT_LIMIT
137 }
138 QueryResultPage resultPage = queryByConditions(hashKey, rangeKeyConditions, settings)
139 resultPage?.results.size() ?: 0
140 }
141
142 /**
143 * Create the DynamoDB table for the given Class.
144 *
145 * @param classToCreate Class to create the table for
146 * @param readCapacityUnits default to 10
147 * @param writeCapacityUnits default to 5
148 */
149 def createTable(Class classToCreate = null,
150 Long readCapacityUnits = 10,
151 Long writeCapacityUnits = 5) {
152 if (!classToCreate) {
153 classToCreate = mainClass
154 }
155 DynamoDBTable table = classToCreate.getAnnotation(DynamoDBTable.class)
156
157 try {
158 // Check if the table exists
159 client.describeTable(table.tableName())
160 } catch (ResourceNotFoundException e) {
161 CreateTableRequest createTableRequest = mapper.generateCreateTableRequest(classToCreate) // new CreateTableRequest().withTableName(table.tableName())
162
163 // ProvisionedThroughput
164 ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput()
165 .withReadCapacityUnits(readCapacityUnits)
166 .withWriteCapacityUnits(writeCapacityUnits)
167 createTableRequest.setProvisionedThroughput(provisionedThroughput)
168
169 log.info("Creating DynamoDB table: ${createTableRequest}")
170
171 client.createTable(createTableRequest)
172 }
173 }
174
175 /**
176 * Decrement a count with an atomic operation
177 *
178 * @param hashKey
179 * @param rangeKey
180 * @param attributeName
181 * @param attributeIncrement
182 * @return
183 */
184 Integer decrement(hashKey,
185 rangeKey,
186 String attributeName,
187 int attributeIncrement = 1) {
188 increment(hashKey, rangeKey, attributeName, -attributeIncrement)
189 }
190
191 /**
192 * Delete item by IDs.
193 *
194 * @param hashKey hash key of the item to delete
195 * @param rangeKey range key of the item to delete
196 * @param settings settings
197 */
198 void delete(hashKey,
199 rangeKey,
200 Map settings = [:]) {
201 delete(mainClass.newInstance((hashKeyName): hashKey, (rangeKeyName): rangeKey), settings)
202 }
203
204 /**
205 * Delete item from Java object
206 *
207 * @param item
208 * @param settings
209 */
210 void delete(item,
211 Map settings = [:]) {
212 deleteAll([item], settings)
213 }
214
215 /**
216 * Delete a list of items from DynamoDB.
217 *
218 * @param itemsToDelete a list of objects to delete
219 * @param settings settings
220 */
221 def deleteAll(List itemsToDelete,
222 Map settings = [:]) {
223 if (!settings.containsKey('batchEnabled')) {
224 settings.batchEnabled = true
225 }
226
227 if (settings.batchEnabled && itemsToDelete.size() > 1) {
228 itemsToDelete.collate(WRITE_BATCH_SIZE).each { List batchItems ->
229 log.debug("Deleting items from DynamoDB ${batchItems}")
230 mapper.batchDelete(batchItems)
231 }
232 } else {
233 itemsToDelete.each {
234 log.debug("Deleting item from DynamoDB ${it}")
235 mapper.delete(it)
236 }
237 }
238 }
239
240 /**
241 * Delete all items for a given hashKey
242 *
243 * @param hashKey
244 * @param settings
245 * @return
246 */
247 int deleteAll(hashKey,
248 Map settings = [:]) {
249 deleteAllByConditions(
250 hashKey,
251 [:],
252 settings
253 )
254 }
255
256 /**
257 * Delete all items f
258 *
259 * @param hashKey
260 * @param rangeKeyName
261 * @param rangeKeyValue
262 * @param operator
263 * @param settings
264 * @return
265 */
266 int deleteAll(hashKey,
267 String rangeKeyName,
268 rangeKeyValue,
269 ComparisonOperator operator = ComparisonOperator.BEGINS_WITH,
270 Map settings = [:]) {
271 Map conditions = [(rangeKeyName): buildCondition(rangeKeyValue, operator)]
272 deleteAllByConditions(
273 hashKey,
274 conditions,
275 settings
276 )
277 }
278
279 /**
280 *
281 * @param hashKey
282 * @param rangeKeyConditions
283 * @param settings
284 * @param indexName
285 * @return
286 */
287 int deleteAllByConditions(hashKey,
288 Map<String, Condition> rangeKeyConditions,
289 Map settings = [:],
290 String indexName = '') {
291 if (!settings.containsKey('batchEnabled')) {
292 settings.batchEnabled = true
293 }
294 if (!settings.limit) {
295 settings.limit = BATCH_DELETE_LIMIT
296 }
297
298 DynamoDBQueryExpression query = buildQueryExpression(hashKeyName, hashKey, settings)
299 query.hashKeyValues = mainClass.newInstance((hashKeyName): hashKey)
300 if (rangeKeyConditions) {
301 query.rangeKeyConditions = rangeKeyConditions
302 }
303 if (indexName) {
304 query.indexName = indexName
305 }
306
307 QueryResultPage itemsPage = mapper.queryPage(mainClass, query)
308
309 int deletedItemsCount = -1
310 Map lastEvaluatedKey = itemsPage.lastEvaluatedKey
311 while (lastEvaluatedKey || deletedItemsCount == -1) {
312 if (deletedItemsCount == -1) {
313 deletedItemsCount = 0
314 } else {
315 query.exclusiveStartKey = lastEvaluatedKey
316 }
317 itemsPage = mapper.queryPage(mainClass, query)
318 if (itemsPage.results) {
319 log.debug "Deleting ${itemsPage.results.size()} items, class: ${mainClass}"
320 deletedItemsCount = deletedItemsCount + itemsPage.results.size()
321 // Delete all items
322 deleteAll(itemsPage.results, settings)
323 }
324 lastEvaluatedKey = itemsPage.lastEvaluatedKey
325 }
326 log.debug "Successfully deleted ${deletedItemsCount} items"
327 deletedItemsCount
328 }
329
330 /**
331 * Load an item
332 *
333 * @param hashKey
334 * @param rangeKey
335 * @return
336 */
337 def get(hashKey,
338 rangeKey) {
339 mapper.load(mainClass, hashKey, rangeKey)
340 }
341
342 /**
343 * Retrieve batched items corresponding to a list of item IDs, in the same order.
344 * Example: items = twitterItemDBService.getAll(1, [1, 2]).
345 *
346 * @param hashKey Hash Key of the items to retrieve
347 * @param rangeKey Range keys of the items to retrieve
348 * @param settings only used for setting throttle/readCapacityUnit when getting large sets
349 * @return a list of DynamoDBItem
350 */
351 List getAll(hashKey,
352 List rangeKeys,
353 Map settings = [:]) {
354 Map result = [:]
355 List objects = rangeKeys.unique().collect { it -> mainClass.newInstance((hashKeyName): hashKey, (rangeKeyName): it) }
356 if (settings.throttle) {
357 int resultCursor = 0
358 long readCapacityUnit = settings.readCapacityUnit
359 if (!readCapacityUnit) {
360 DescribeTableResult tableResult = client.describeTable(mainTable.tableName())
361 readCapacityUnit = tableResult?.getTable()?.provisionedThroughput?.readCapacityUnits ?: 10
362 }
363 objects.collate(20).each { List batchObjects ->
364 result += mapper.batchLoad(batchObjects)
365 resultCursor++
366 if (readCapacityUnit && resultCursor >= (readCapacityUnit * 0.8)) {
367 resultCursor = 0
368 sleep(1000)
369 }
370 }
371 } else {
372 result = mapper.batchLoad(objects)
373 }
374 if (result[mainTable.tableName()]) {
375 List unorderedItems = result[mainTable.tableName()]
376 List items = []
377
378 // Build an item list ordered in the same manner as the list of IDs we've been passed
379 rangeKeys.each { rangeKey ->
380 def matchingItem = unorderedItems.find { item ->
381 item[rangeKeyName] == rangeKey
382 }
383 if (matchingItem) {
384 items.add(matchingItem)
385 }
386 // Remove the matching item from the unordered list to reduce the number of loops in the find above
387 unorderedItems.remove(matchingItem)
388 }
389 items
390 } else {
391 []
392 }
393 }
394
395 /**
396 * Increment a count with an atomic operation
397 *
398 * @param hashKey
399 * @param rangeKey
400 * @param attributeName
401 * @param attributeIncrement
402 * @return
403 */
404 Integer increment(hashKey,
405 rangeKey,
406 String attributeName,
407 int attributeIncrement = 1) {
408 UpdateItemResult result = updateItemAttribute(hashKey, rangeKey, attributeName, attributeIncrement, AttributeAction.ADD)
409 result?.attributes[attributeName]?.getN()?.toInteger()
410 }
411
412 /**
413 * Optional settings:
414 * - batchGetDisabled (only when secondary indexes are used, useful for count when all item attributes are not required)
415 * - consistentRead (default to false)
416 * - exclusiveStartKey a map with the rangeKey (ex: [id: 2555]), with optional indexRangeKey when using LSI (ex.: [id: 2555, totalCount: 45])
417 * - limit
418 * - returnAll disable paging to return all items, WARNING: can be expensive in terms of throughput (default to false)
419 * - scanIndexForward (default to false)
420 *
421 * @param hashKey
422 * @param settings
423 * @return
424 */
425 QueryResultPage query(hashKey,
426 Map settings = [:]) {
427 queryByConditions(hashKey, [:], settings)
428 }
429
430 /**
431 * Optional settings:
432 * - batchGetDisabled (only when secondary indexes are used, useful for count when all item attributes are not required)
433 * - consistentRead (default to false)
434 * - exclusiveStartKey a map with the rangeKey (ex: [id: 2555]), with optional indexRangeKey when using LSI (ex.: [id: 2555, totalCount: 45])
435 * - limit
436 * - returnAll disable paging to return all items, WARNING: can be expensive in terms of throughput (default to false)
437 * - scanIndexForward (default to false)
438 *
439 * @param hashKey
440 * @param rangeKeyName
441 * @param rangeKeyValue
442 * @param operator
443 * @param settings
444 * @return
445 */
446 QueryResultPage query(hashKey,
447 String rangeKeyName,
448 rangeKeyValue,
449 ComparisonOperator operator = ComparisonOperator.EQ,
450 Map settings = [:]) {
451 Map conditions = [(rangeKeyName): buildCondition(rangeKeyValue, operator)]
452 queryByConditions(hashKey, conditions, settings)
453 }
454
455 /**
456 * Optional settings:
457 * - batchGetDisabled (only when secondary indexes are used, useful for count when all item attributes are not required)
458 * - consistentRead (default to false)
459 * - exclusiveStartKey a map with the rangeKey (ex: [id: 2555]), with optional indexRangeKey when using LSI (ex.: [id: 2555, totalCount: 45])
460 * - limit
461 * - returnAll disable paging to return all items, WARNING: can be expensive in terms of throughput (default to false)
462 * - scanIndexForward (default to false)
463 * - throttle insert sleeps during execution to avoid reaching provisioned read throughput (default to false)
464 *
465 * @param hashKey
466 * @param rangeKeyConditions
467 * @param settings
468 * @return
469 */
470 QueryResultPage queryByConditions(hashKey,
471 Map<String, Condition> rangeKeyConditions,
472 Map settings = [:],
473 String indexName = '') {
474 DynamoDBQueryExpression query = buildQueryExpression(hashKeyName, hashKey, settings)
475 query.hashKeyValues = mainClass.newInstance((hashKeyName): hashKey)
476 if (rangeKeyConditions) {
477 query.rangeKeyConditions = rangeKeyConditions
478 }
479 if (indexName) {
480 query.indexName = indexName
481 }
482
483 long readCapacityUnit = 0
484 int resultCursor = 0
485 QueryResultPage resultPage = new QueryResultPage()
486 if (settings.returnAll) {
487 // Get table read Throughput
488 if (settings.throttle) {
489 DescribeTableResult tableResult = client.describeTable(mainTable.tableName())
490 readCapacityUnit = tableResult?.getTable()?.provisionedThroughput?.readCapacityUnits ?: 0
491 }
492
493 // Query all
494 String lastEvaluatedKey = "0"
495 resultPage.results = []
496 while (lastEvaluatedKey) {
497 QueryResultPage currentPage = mapper.queryPage(mainClass, query)
498 resultPage.results.addAll(currentPage.results)
499 lastEvaluatedKey = currentPage.lastEvaluatedKey
500 if (settings.throttle) {
501 resultCursor++
502 if (readCapacityUnit && resultCursor >= (readCapacityUnit * 0.8)) {
503 resultCursor = 0
504 sleep(1000)
505 }
506 }
507 }
508 } else {
509 // Query page
510 resultPage = mapper.queryPage(mainClass, query)
511 }
512
513 if (resultPage && (rangeKeyConditions || indexName) && !settings.batchGetDisabled) {
514 // Indexes result only provides hash+range attributes, we need to batch get all items
515 List rangeKeys = resultPage.results.collect { it[rangeKeyName] }
516 if (rangeKeys) {
517 resultPage.results = getAll(hashKey, rangeKeys, settings + [readCapacityUnit: readCapacityUnit])
518 }
519 }
520 resultPage
521 }
522
523 /**
524 * Query by dates with 'after' and/or 'before' range value
525 * 1) After a certain date : [after: new Date()]
526 * 2) Before a certain date : [before: new Date()]
527 * 3) Between provided dates : [after: new Date() + 1, before: new Date()]
528 *
529 * Optional settings:
530 * - batchGetDisabled (only when secondary indexes are used, useful for count when all item attributes are not required)
531 * - consistentRead (default to false)
532 * - exclusiveStartKey a map with the rangeKey (ex: [id: 2555]), with optional indexRangeKey when using LSI (ex.: [id: 2555, totalCount: 45])
533 * - limit
534 * - scanIndexForward (default to false)
535 *
536 * @param hashKey
537 * @param rangeKeyName
538 * @param rangeKeyDates
539 * @param settings
540 * @return
541 */
542 QueryResultPage queryByDates(hashKey,
543 String rangeKeyName,
544 Map rangeKeyDates,
545 Map settings = [:]) {
546 Map conditions = buildDateConditions(rangeKeyName, rangeKeyDates)
547 queryByConditions(hashKey, conditions, settings)
548 }
549
550 /**
551 * Save an item.
552 *
553 * @param item the item to save
554 * @param settings settings
555 * @return the Item after it's been saved
556 */
557 def save(item,
558 Map settings = [:]) {
559 saveAll([item], settings).first()
560 }
561
562 /**
563 * Save a list of objects in DynamoDB.
564 *
565 * @param itemsToSave a list of objects to save
566 * @param settings settings
567 */
568 def saveAll(List itemsToSave,
569 Map settings = [:]) {
570 if (!settings.containsKey('batchEnabled')) {
571 settings.batchEnabled = true
572 }
573
574 // Nullify empty collection properties
575 itemsToSave.each { object ->
576 object.properties.each { String prop, val ->
577 if (object.hasProperty(prop)
578 && object[prop] instanceof HashSet
579 && object[prop]?.size() == 0) {
580 // log.debug("Nullifying collection ${prop} before sending to DynamoDB")
581 object[prop] = null
582 }
583 }
584 }
585
586 log.debug "Saving items in DynamoDB ${itemsToSave}"
587
588 if (settings.batchEnabled && itemsToSave.size() > 1) {
589 itemsToSave.collate(WRITE_BATCH_SIZE).each { List batchItems ->
590 log.debug "Saving batched items in DynamoDB ${batchItems}"
591 List failedBatchResult = settings.config ? mapper.batchSave(batchItems, settings.config) : mapper.batchSave(batchItems)
592 if (failedBatchResult) {
593 failedBatchResult.each { DynamoDBMapper.FailedBatch failedBatch ->
594 log.error "Failed batch with ${failedBatch.unprocessedItems.size()} unprocessed items"
595 log.error "Exception: ${failedBatch.exception}"
596 throw failedBatch.exception
597 }
598 }
599 }
600 } else {
601 itemsToSave.each {
602 log.debug "Saving item in DynamoDB ${it}"
603 settings.config ? mapper.save(it, settings.config as DynamoDBMapperConfig) : mapper.save(it)
604 }
605 }
606 }
607
608 /**
609 * Update a single item attribute
610 *
611 * @param hashKey
612 * @param rangeKey
613 * @param attributeName
614 * @param attributeValue
615 * @param action
616 * @return
617 */
618 UpdateItemResult updateItemAttribute(hashKey,
619 rangeKey,
620 String attributeName,
621 attributeValue,
622 AttributeAction action = AttributeAction.PUT) {
623 UpdateItemRequest request = new UpdateItemRequest(
624 tableName: mainTable.tableName(),
625 key: [
626 (hashKeyName): buildAttributeValue(hashKey),
627 (rangeKeyName): buildAttributeValue(rangeKey)
628 ],
629 returnValues: ReturnValue.UPDATED_NEW
630 ).addAttributeUpdatesEntry(
631 attributeName,
632 new AttributeValueUpdate(
633 action: action,
634 value: buildAttributeValue(attributeValue)
635 )
636 )
637 client.updateItem(request)
638 }
639
640 static Date deserializeDate(String date) throws ParseException {
641 dateFormatter.parse(date)
642 }
643
644 static String serializeDate(Date date) {
645 dateFormatter.format(date)
646 }
647
648 /**
649 *
650 * @param key
651 * @return
652 */
653 static protected AttributeValue buildAttributeValue(Object key) {
654 if (key.toString().isNumber()) {
655 new AttributeValue().withN(key.toString())
656 } else if (key instanceof Boolean) {
657 new AttributeValue().withN(key ? "1" : "0")
658 } else if (key instanceof Date) {
659 new AttributeValue().withS(dateFormatter.format(key))
660 } else {
661 new AttributeValue().withS(key.toString())
662 }
663 }
664
665 /**
666 *
667 * @param rangeKeyValue
668 * @param operator
669 * @return
670 */
671 static protected Condition buildCondition(rangeKeyValue,
672 ComparisonOperator operator = ComparisonOperator.EQ) {
673 new Condition()
674 .withComparisonOperator(operator)
675 .withAttributeValueList(buildAttributeValue(rangeKeyValue))
676 }
677
678 /**
679 *
680 * @param rangeKeyName
681 * @param rangeKeyDates
682 * @return
683 */
684 static protected Map buildDateConditions(String rangeKeyName,
685 Map rangeKeyDates) {
686 assert rangeKeyDates.keySet().any { it in ['after', 'before'] }
687 ComparisonOperator operator
688 List attributeValueList = []
689 if (rangeKeyDates.containsKey('after')) {
690 operator = ComparisonOperator.GE
691 attributeValueList << new AttributeValue().withS(serializeDate(rangeKeyDates['after']))
692 }
693 if (rangeKeyDates.containsKey('before')) {
694 operator = ComparisonOperator.LE
695 attributeValueList << new AttributeValue().withS(serializeDate(rangeKeyDates['before']))
696 }
697 if (rangeKeyDates.containsKey('after') && rangeKeyDates.containsKey('before')) {
698 operator = ComparisonOperator.BETWEEN
699 }
700 [
701 (rangeKeyName): new Condition()
702 .withComparisonOperator(operator)
703 .withAttributeValueList(attributeValueList)
704 ]
705 }
706
707 /**
708 *
709 * @param hashKeyName
710 * @param hashKey
711 * @param settings
712 * @return
713 */
714 static protected DynamoDBQueryExpression buildQueryExpression(hashKeyName,
715 hashKey,
716 Map settings = [:]) {
717 DynamoDBQueryExpression query = new DynamoDBQueryExpression()
718 if (settings.containsKey('consistentRead')) {
719 query.consistentRead = settings.consistentRead
720 }
721 if (settings.exclusiveStartKey) {
722 assert settings.exclusiveStartKey instanceof Map
723 query.exclusiveStartKey = buildStartKey(settings.exclusiveStartKey + [(hashKeyName): hashKey])
724 }
725 if (settings.limit) {
726 assert settings.limit.toString().isNumber()
727 query.limit = settings.limit
728 } else {
729 query.limit = DEFAULT_QUERY_LIMIT
730 }
731 if (settings.containsKey('scanIndexForward')) {
732 query.scanIndexForward = settings.scanIndexForward
733 } else {
734 query.scanIndexForward = false
735 }
736 query
737 }
738
739 static protected Map buildStartKey(Map map) {
740 map.inject([:]) { startKey, it ->
741 startKey[it.key] = buildAttributeValue(it.value)
742 startKey
743 }
744 }
745
746 /**
747 * Returns the DynamoDB type for a given Field.
748 *
749 * Currently handled types :
750 * 1) Primitive numbers : short, int, long, float, double
751 * 2) String
752 * Any other type will result in an exception.
753 *
754 * @param field Field to determine the type for
755 * @return the DynamoDB type associated to the given field
756 */
757 static protected String getDynamoType(Field field) {
758 if (field.type.name in ['short', 'int', 'long', 'float', 'double']) {
759 return 'N'
760 } else if (field.type.name == 'java.lang.String') {
761 return 'S'
762 } else {
763 throw new RuntimeException("DynamoDB Invalid property type: ${field.type.name}, property: ${field.name}")
764 }
765 }
766
767}