DynamoDB updating list from Java fails - java

I have next table:
#DynamoDBTable(tableName = "Table")
#Getter
#Setter
public class Table {
#DynamoDBHashKey
private String id;
#DynamoDBAttribute
private List<String> list;
}
I need to add values to list from Java. I tried next approach:
UpdateItemRequest request = new UpdateItemRequest()
.withTableName("Table")
.withKey(Collections.singletonMap("id", new AttributeValue().withS("123")))
.withUpdateExpression("SET #ri = list_append(:vals, #ri)")
.withExpressionAttributeNames(Collections.singletonMap("#ri","list"))
.withExpressionAttributeValues(Collections.singletonMap(":vals",
new AttributeValue().withSS("1","2","3")));
dynamoDB.updateItem(request);
But it fails without providing exact reason:
Unable to unmarshall exception response with the unmarshallers provided
I got two questions:
What is wrong with my request ?
Why I get unmarshall exception and how to fix it to get real reason ?
P.S. Other update requests work fine e.g. I can update id with same approach without problem.
Updated:
I've found a way to update values from Java
UpdateItemRequest request = new UpdateItemRequest()
.withTableName("Table")
.withKey(Collections.singletonMap("id", new AttributeValue().withS("123")))
.withUpdateExpression("SET #ri = list_append(:vals, #ri)")
.withExpressionAttributeNames(Collections.singletonMap("#ri","list"))
.withExpressionAttributeValues(Collections.singletonMap(":vals",
new AttributeValue().withL(new AttributeValue().withS("1"),AttributeValue().withS("2"),AttributeValue().withS("3"))));
dynamoDB.updateItem(request);
But it only allows to add one by one values in list, but I need to add several.

private void updateList(String id, String field, List<AttributeValue> values) {
UpdateItemRequest request = new UpdateItemRequest()
.withTableName("Table")
.withKey(Collections.singletonMap("id", new AttributeValue().withS(id)))
.withUpdateExpression("SET #field = list_append(:values, #field)")
.withExpressionAttributeNames(Collections.singletonMap("#field", field))
.withExpressionAttributeValues(Collections.singletonMap(":values",
new AttributeValue().withL(values)));
dynamoDB.updateItem(request);
}
And use next method to convert to list of AttributeValue
private List<AttributeValue> buildAttributeValuesList(List<String> values) {
return values
.stream()
.map(it -> new AttributeValue().withS(it))
.collect(Collectors.toList());
}

Related

DynamoDB - Query instead of Scan - how to achive with random generated key

Setup
I have the following table in java and DynamoDB (DynamoDB Mapper) annotations:
#DynamoDBTable(tableName="myentity")
public class MyEntity {
#DynamoDBHashKey
#DynamoDBAutoGeneratedKey
private String id;
#DynamoDBIndexHashKey()
//#DynamoDBIndexRangeKey
private String userId;
private Date eventDate;
The id is randomly generated when saved and the same userId can occur for multiple entities.
The tables where defined via Web GUI like this:
Primary partition key: id (String)
Primary sort key: userId (String)
Issue
I would like to have all the entities for one userId with a query instead of a scan.
Query - does not work like this:
public List<MyEntity> findByUserIdQuery(String userId) {
Map<String, AttributeValue> eav = new HashMap<String, AttributeValue>();
eav.put(":val1", new AttributeValue().withS(userId));
DynamoDBQueryExpression<MyEntity> queryExpression = new DynamoDBQueryExpression<MyEntity>()
.withKeyConditionExpression("userId = :val1").withExpressionAttributeValues(eav);
List<MyEntity> list = mapper.query(MyEntity.class, queryExpression);
of course I get:
com.amazonaws.AmazonServiceException: Query condition missed key schema element: id (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: 123) at com.amazonaws.http.AmazonHttpClient.handleErrorResponse(AmazonHttpClient.java:1305)
because I don't know the generated id when I want to read data.
Workaround
Instead I use:
public List<MyEntity> findByOwner(String userId){
Map<String, AttributeValue> eav = new HashMap<String, AttributeValue>();
eav.put(":val1", new AttributeValue().withS(userId));
DynamoDBScanExpression scanExpression = new DynamoDBScanExpression()
.withFilterExpression("userId = :val1").withExpressionAttributeValues(eav);
List<MyEntity> list = mapper.scan(MyEntity.class, scanExpression);
Possible solutions
The issue is that I never know the randomly generated key for the elements I have. When I want to read I only know the userId and I want to have all entities for them.
I watched the examples here:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.QueryScanExample.html
...but they know always the key for what they are searching. They are using a combinded key like this:
String partitionKey = forumName + "#" + threadSubject;*
eav.put(":val1", new AttributeValue().withS(partitionKey))
Map<String, AttributeValue> eav = new HashMap<String, AttributeValue>();
DynamoDBQueryExpression<Reply> queryExpression = new DynamoDBQueryExpression<Reply>()
.withKeyConditionExpression("Id = :val1 and ReplyDateTime > :val2").withExpressionAttributeValues(eav);
Question
Is there something like a wildcard for this combinded partition key that I could use? (Something like: "* # myGreatUser")
Other suggestions for db setup and other annotations as key element?
Maybe an Index where I could use query instead of scan?
Maybe somebody has a working example for this kind of use case?
You just need to switch your partition key and sort key around.
#DynamoDBTable(tableName="myentity")
public class MyEntity {
#DynamoDBRangeKey
#DynamoDBAutoGeneratedKey
private String id;
#DynamoDBHashKey
private String userId;
private Date eventDate;
And then query using
public List<MyEntity> findByUserIdQuery(String userId) {
Map<String, AttributeValue> eav = new HashMap<String, AttributeValue>();
eav.put(":val1", new AttributeValue().withS(userId));
DynamoDBQueryExpression<MyEntity> queryExpression = new DynamoDBQueryExpression<MyEntity>()
.withKeyConditionExpression("userId = :val1").withExpressionAttributeValues(eav);
List<MyEntity> list = mapper.query(MyEntity.class, queryExpression);
By the way, you mentioned that you thought your sort key was userId, but in the example supplied you have a primary hash key of id and a global secondary index hash key of userid. There is no sort key.

how to query dynamodb with LSI and mapper

I am currently trying to load info from a dynamo DB table using a LSI and dynamoDB mapper class. Assume i have the following code
Conisder this class
class Employee {
#DynamoDBHashKey
public String getID() {
return ID;
}
#DynamoDBRangeKey
public String getFirstName() {
return firstName;
}
#DynamoDBIndexRangeKey(localSecondaryIndexName = "my-lsi")
public String getLastName() {
return lastName;
}
public String getMyotherfield() {
return myotherfield;
}
}
Consider this piece of code to retrieve info using LSI and dynamoDB mapper
Employee obj = new Employee();
obj.setID("221");
obj.setLastName("SOMEONE");
DynamoDBQueryExpression<Employee> queryExpression = new DynamoDBQueryExpression<>();
queryExpression.setHashKeyValues(obj);
queryExpression.setIndexName("my-lsi");
queryExpression.setProjectionExpression("myotherfield");
PaginatedQueryList<Employee> paginatedQueryList = mapper.query(Employee.class, queryExpression);
Iterator<Employee> iterator = paginatedQueryList.iterator();
while (iterator.hasNext()) {
Employee pp = iterator.next();
// some code
}
[EDIT]
Is this the right way to query LSI with mapper?
If not what is a better way to fetch data from the table using LSI and dynamoDBMapper? (in terms of performance)
i also found this https://java.awsblog.com/post/Tx3GYZEVGO924K4/The-DynamoDBMapper-Local-Secondary-Indexes-and-You but its a 2013 link. I am not able to find latest documentation on querying with LSI and mapper.
Note that in the code you have given, you haven't set the range key at all. Use queryExpression.setRangeKeyConditions() to do so.
Here is a code sample:
Map<String, Condition> rangeKeyConditions = new HashMap<>();
rangeKeyConditions.put("lastName", new Condition()
.withComparisonOperator(ComparisonOperator.EQ)
.withAttributeValueList(new AttributeValue().withS("SOMEONE")));
queryExpression.setRangeKeyConditions(rangeKeyConditions);`
[Update]:
It is not sufficient (nor necessary) to set the range key value ("SOMEONE") in the object (obj), since that object is used only for setting the hashKey. Have a look at the examples here

Gson converter factory retrofit2. Different types in serialization/deserialization

I'm working with wordpress post metas and rest api, i've exposed to rest a meta field called "picture_collection" wich store data as an array of integers where every number represents the ID of an attachment.
I've then modified the response when interrogating the api to give me a list of links instead of the attachment ids, like this:
function get_pic_coll ($object, $field_name, $request) {
include_once dirname(__FILE__) . '/attach_coll.php';
$pic_coll = get_post_meta ($object['id'], $field_name, true);
$json_coll = array();
if($pic_coll != null || !empty($pic_coll)){
foreach ($pic_coll as $pic){
$media_id = $pic;
$link_med = wp_get_attachment_image_src($media_id, 'medium');
$link_full = wp_get_attachment_image_src($media_id, 'full');
$medium_size = $link_med[0];
$full_size = $link_full[0];
$obj = new attach_coll($media_id, $medium_size, $full_size);
$element = $obj->return_coll_object();
$json_coll[] = $element;
}
return $json_coll;
}
}
while the attach_coll object is:
class attach_coll{
public function __construct($media_id, $medium_url, $orig_url){
$this->attach_id = $media_id;
$this->medium_size_pic = $medium_url;
$this->full_size_pic = $orig_url;
}
private $attach_id;
private $medium_size_pic;
private $full_size_pic;
public function get_media_id(){
return $this->attach_id;
}
public function get_medium_pic(){
return $this->medium_size_pic;
}
public function get_orig_pic(){
return $this->full_size_pic;
}
public function return_coll_object(){
$ret_coll = array(
"ID" => $this->get_media_id(),
"medium" => $this->get_medium_pic(),
"full" => $this->get_orig_pic()
);
return $ret_coll;
}
}
Java side the things goes like this:
1)the user make a picture and upload her, he receive in exchange the ID of the attachment that is stored inside an Integers ArrayList.
2)when he has done the program update the post_meta passing to the api the entire list.
3)the program receive the response as a json containing the whole post with my custom field, it looks like this:
{...
"id":"someValue",
"title":"myTitle",
"pic_collection":[ {'ID':'picID','mediumSizePic':'someUrl', 'FullSizePic':'SomeOtherUrl},{...}],
The php code works well as i see from the ResponseBody the json i was expecting, the problem is that i'm getting an error 'gson expected a Integer and found an Object' that is logical because the pojo is defined like:
#SerializedName("pic_collection")
private List<Integer> idList = new ArrayList<Integer>();
public void setList(List<Integer> list){
this.idList=list;
}
I tried to change my list to:
List<PicCollection> picList = new ArrayList<PicCollection>();
public class PicCollection{
#SerializedName("ID")
private int picId;
#SerializedName("medium_size")
private String medSizeUrl;
#SerializedName("full_size")
private String fullSizeUrl;
Getters and Setters
}
But that just complicated everything up and didn't resolved the problem as i'm still having a the same gson error 'ID expecting an int but found an object' and no links returned at all.
A glimpse on the code to set the id's:
iterator=idList.iterator;
while(iterator.hasNext()){
FotoCollection fc = new FotoCollection();
fc.ID = iterator.next
What can i do to resove the problem? I need a custom converter?
I just created 2 objects:
One for the request and one for the response.

Hibernate custom constraint validator, how to set propertyPath explicitly?

my custom class is implemented via:
#RequiredIfSet.List({
#RequiredIfSet(field = "isFillingOutForSomeoneElse", dependentField = "reporterName", message = "may not be null"),
})
When I loop through my ConstraintViolations and whip it to the frontend via a DTO and ajax, I have getMessage() and getPropertyPath() in a little String[].
Works fine for everything, but propertyPath isn't set by this, as its not ON the field, its a list, how can i set it, or grab the field name to send back? Has been driving me crazy, have tried a few work around, also for note, heres where I loop:
public static <T> EntityValidationDTO GetEntityValidationDTO(Set<ConstraintViolation<T>> cv){
List<String[]> invalidFields = new ArrayList<String[]>();
Iterator<ConstraintViolation<T>> iterator = cv.iterator();
while(iterator.hasNext()) {
ConstraintViolation<T> i = iterator.next();
String message = i.getMessage();
String property = i.getPropertyPath().toString();
invalidFields.add(new String[] { property, message });
}
EntityValidationDTO EVDTO = new EntityValidationDTO();
EVDTO.setStatus("fail");
EVDTO.setInvalidFields(invalidFields);
return EVDTO;
}

REST - How to pass an array of long in parameter with Jersey?

I am trying to pass an array of long with Jersey :
In the client side i have trying something like that :
#GET
#Consume("text/plain")
#Produces("application/xml)
Response getAllAgentsById(#params("listOfId") List<Long> listOfId);
Is there a way to realize something like that?
Thanks in advance!
If you want to stick to "application/xml" format and avoid JSON format, you should wrap this data into a JAXB annotated object, so that Jersey can use the built-in MessageBodyWriter / MessageBodyReader.
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public ListOfIds{
private List<Long> ids;
public ListOfIds() {}
public ListOfIds(List<Long> ids) {
this.ids= ids;
}
public List<Long> getIds() {
return ids;
}
}
On the client side (using Jersey client)
// get your list of Long
List<Long> list = computeListOfIds();
// wrap it in your object
ListOfIds idList = new ListOfIds(list);
Builder builder = webResource.path("/agentsIds/").type("application/xml").accept("application/xml");
ClientResponse response = builder.post(ClientResponse.class, idList);
If you just need to pass array of long its possible without any problem. But I will probably pass the long as comma delimited string. (123,233,2344,232) and then split the string and convert in to long.
If not, I suggest you use Json Serialization. If you are using java client, then google gson is a good option. In client side, I will encode my list:
List<Long> test = new ArrayList<Long>();
for (long i = 0; i < 10; i++) {
test.add(i);
}
String s = new Gson().toJson(test);
And pass this string as post param. In the server side, I will decode like this.
Type collectionType = new TypeToken<List<Long>>() {
} // end new
.getType();
List<Long> longList = new Gson().fromJson(longString,
collectionType);

Categories

Resources