Base table abstraction
stay 2. Design of AWS dynamodb actual combat table In this article, we introduced in detail how to design dto model and dynamodb table, and carefully observed our three DTOs, which have some common points:
- Both have getter s and setter s for pk and sk
- The unique identifier of item consists of pk and sk
Based on the above characteristics, we abstract a base class AbstractDto of dto mode:
- AbstractDto is an abstract class
- It contains four abstract methods: getter and setter of pk and sk
- It contains a method for obtaining the unique identifier of item. The format of the returned unique identifier is: pk#sk
package com.jessica.dynamodb.favorite.dto; public abstract class AbstractDto { public abstract String getPk(); public abstract void setPk(String hashKey); public abstract String getSk(); public abstract void setSk(String rangeKey); /** * return composite keys as unique key string * * @return */ public String getCompositeKey() { return String.join("#", getPk(), getSk()); } }
After AbstractDto is added, all other DTOs need to inherit this class:
package com.jessica.dynamodb.favorite.dto; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIgnore; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.utils.KeyGenerator; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @DynamoDBTable(tableName = "develop.Favorite") @EqualsAndHashCode(callSuper = false) public class TagDto extends AbstractDto { private static final String DTO_NAME = "Tag"; @DynamoDBIgnore private String userId; @DynamoDBIgnore private String tagId; @DynamoDBAttribute private String tagName; @DynamoDBAttribute private long createTime; @DynamoDBAttribute private long lastAccessTime; @DynamoDBHashKey @Override public String getPk() { return KeyGenerator.createHashKey(DTO_NAME, userId); } @Override public void setPk(String hashKey) { String[] keys = KeyGenerator.parseHashKey(DTO_NAME, hashKey); this.userId = keys[0]; } @DynamoDBRangeKey @Override public String getSk() { return KeyGenerator.createRangeKey(tagId); } @Override public void setSk(String rangeKey) { String[] keys = KeyGenerator.parseRangeKey(rangeKey); this.tagId = keys[0]; } @DynamoDBIndexRangeKey(localSecondaryIndexName = DynamoDBConstant.LSI_ONE_NAME) public String getLsiOneSk() { return KeyGenerator.createRangeKey(tagName); } public void setLsiOneSk(String lsiOneSk) { } @DynamoDBIndexRangeKey(localSecondaryIndexName = DynamoDBConstant.LSI_TWO_NAME) public String getLsiTwoSk() { return KeyGenerator.createRangeKey(String.valueOf(createTime), tagId); } public void setLsiTwoSk(String lsiTwoSk) { } @DynamoDBIndexRangeKey(localSecondaryIndexName = DynamoDBConstant.LSI_THREE_NAME) public String getLsiThreeSk() { return KeyGenerator.createRangeKey(String.valueOf(lastAccessTime), tagId); } public void setLsiThreeSk(String lsiThreeSk) { } }
package com.jessica.dynamodb.favorite.dto; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIgnore; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConvertedEnum; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.utils.KeyGenerator; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @DynamoDBTable(tableName = "develop.Favorite") @EqualsAndHashCode(callSuper = false) public class FavoriteDataDto extends AbstractDto { private static final String DTO_NAME = "FavoriteData"; @DynamoDBIgnore private String userId; @DynamoDBIgnore private String dataId; @DynamoDBAttribute private String creatorId; @DynamoDBAttribute private String title; @DynamoDBAttribute private String thumbnailUrl; @DynamoDBAttribute private String contentUrl; @DynamoDBTypeConvertedEnum @DynamoDBAttribute private FavoriteDataType dataType; @DynamoDBAttribute private String clipTime; @DynamoDBHashKey @Override public String getPk() { return KeyGenerator.createHashKey(DTO_NAME, userId); } @Override public void setPk(String hashKey) { String[] keys = KeyGenerator.parseHashKey(DTO_NAME, hashKey); this.userId = keys[0]; } @DynamoDBRangeKey @Override public String getSk() { return KeyGenerator.createRangeKey(dataId); } @Override public void setSk(String rangeKey) { String[] keys = KeyGenerator.parseRangeKey(rangeKey); this.dataId = keys[0]; } @DynamoDBIndexHashKey(globalSecondaryIndexName = DynamoDBConstant.GSI_ONE_NAME) public String getGsiOnePk() { return KeyGenerator.createHashKey(DTO_NAME, userId, dataType.getValue()); } public void setGsiOnePk(String hashKey) { } @DynamoDBIndexRangeKey(globalSecondaryIndexName = DynamoDBConstant.GSI_ONE_NAME) public String getGsiOneSk() { return KeyGenerator.createRangeKey(clipTime, dataId); } public void setGsiOneSk(String rangeKey) { } }
package com.jessica.dynamodb.favorite.dto; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIgnore; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConvertedEnum; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.utils.KeyGenerator; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor @DynamoDBTable(tableName = "develop.Favorite") @EqualsAndHashCode(callSuper = false) public class FavoriteDataTagDto extends AbstractDto { private static final String DTO_NAME = "FavoriteDataTag"; @DynamoDBIgnore private String userId; @DynamoDBIgnore private String dataId; @DynamoDBIgnore private String tagId; @DynamoDBAttribute private String clipTime; @DynamoDBTypeConvertedEnum @DynamoDBAttribute private FavoriteDataType dataType; @DynamoDBHashKey @Override public String getPk() { return KeyGenerator.createHashKey(DTO_NAME, userId, dataId); } @Override public void setPk(String hashKey) { String[] keys = KeyGenerator.parseHashKey(DTO_NAME, hashKey); this.userId = keys[0]; this.dataId = keys[1]; } @DynamoDBRangeKey @Override public String getSk() { return KeyGenerator.createRangeKey(tagId); } @Override public void setSk(String rangeKey) { String[] keys = KeyGenerator.parseRangeKey(rangeKey); this.tagId = keys[0]; } @DynamoDBIndexHashKey(globalSecondaryIndexName = DynamoDBConstant.GSI_ONE_NAME) public String getGsiOnePk() { return KeyGenerator.createHashKey(DTO_NAME, userId, tagId); } public void setGsiOnePk(String hashKey) { } @DynamoDBIndexRangeKey(globalSecondaryIndexName = DynamoDBConstant.GSI_ONE_NAME) public String getGsiOneSk() { return KeyGenerator.createRangeKey(clipTime, dataId); } public void setGsiOneSk(String rangeKey) { } @DynamoDBIndexHashKey(globalSecondaryIndexName = DynamoDBConstant.GSI_TWO_NAME) public String getGsiTwoPk() { return KeyGenerator.createHashKey(DTO_NAME, userId, tagId, dataType.getValue()); } public void setGsiTwoPk(String hashKey) { } @DynamoDBIndexRangeKey(globalSecondaryIndexName = DynamoDBConstant.GSI_TWO_NAME) public String getGsiTwoSk() { return KeyGenerator.createRangeKey(clipTime, dataId); } public void setGsiTwoSk(String rangeKey) { } }
Common interface abstraction
Generally speaking, each dto model corresponds to a dao for crud operation on the dto. In the actual implementation process, it is found that the codes of some basic CRUD operations are exactly the same, so we also implement a general interface to perform these basic CRUD operations
package com.jessica.dynamodb.favorite.dao; import java.util.List; import java.util.Map; import com.jessica.dynamodb.favorite.data.LazyLoadResult; public interface BasicDao<T> { /** * create or update dto * * @param dto */ void save(T dto); /** * load dto * * @param keyDto, pk and sk related fields must set * @return */ T load(T keyDto); /** * delete dto * * @param keyDto, pk and sk related fields must set */ void delete(T keyDto); /** * create or update dtos * * @param dtos */ void batchSave(List<T> dtos); /** * load dtos, result list sequence may not be same with keyDtos * * @param keyDtos, pk and sk related fields must set * @return */ List<T> batchLoad(List<T> keyDtos); /** * load dto map * * @param keyDtos, pk and sk related fields must set * @return key is pk#sk */ Map<String, T> batchLoadMap(List<T> keyDtos); /** * delete dtos * * @param keyDtos, pk and sk related fields must set * @return */ void batchDelete(List<T> keyDtos); /** * query with pk in given order * * @param clazz * @param hashKeyDto * @param asc * @param lastLoadSk * @param size * @return */ LazyLoadResult<T> query(Class<T> clazz, T hashKeyDto, boolean asc, String lastLoadSk, Integer size); /** * query index with pk in given order * * @param clazz * @param indexName * @param indexSkName * @param hashKeyDto * @param asc * @param lastLoadSk * @param size * @return */ LazyLoadResult<T> queryIndex(Class<T> clazz, String indexName, String indexSkName, T hashKeyDto, boolean asc, String lastLoadSk, Integer size); }
package com.jessica.dynamodb.favorite.dao.impl; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.favorite.dao.BasicDao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.AbstractDto; public class BasicDaoImpl<T extends AbstractDto> implements BasicDao<T> { protected DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(AmazonDynamoDBClientBuilder.standard().build()); @Override public void save(T dto) { this.dynamoDBMapper.save(dto); } @Override public T load(T keyDto) { return this.dynamoDBMapper.load(keyDto); } @Override public void delete(T keyDto) { this.dynamoDBMapper.delete(keyDto); } @Override public void batchSave(List<T> dtos) { this.dynamoDBMapper.batchSave(dtos); } @Override public List<T> batchLoad(List<T> keyDtos) { return this.dynamoDBMapper.batchLoad(keyDtos).values().stream().flatMap(Collection::stream) .map(object -> (T) object).collect(Collectors.toList()); } @Override public Map<String, T> batchLoadMap(List<T> keyDtos) { return this.batchLoad(keyDtos).stream() .collect(Collectors.toMap(t -> ((AbstractDto) t).getCompositeKey(), Function.identity())); } @Override public void batchDelete(List<T> keyDtos) { this.dynamoDBMapper.batchDelete(keyDtos); } @Override public LazyLoadResult<T> query(Class<T> clazz, T hashKeyDto, boolean asc, String lastLoadSk, Integer size) { DynamoDBQueryExpression<T> queryExpression = new DynamoDBQueryExpression<T>().withHashKeyValues(hashKeyDto) .withScanIndexForward(asc); if (lastLoadSk != null) { Condition rangeCondition = new Condition() .withComparisonOperator(asc ? ComparisonOperator.GT : ComparisonOperator.LT) .withAttributeValueList(new AttributeValue(lastLoadSk)); queryExpression.withRangeKeyCondition(DynamoDBConstant.RANGE_KEY, rangeCondition); } List<T> dtos; boolean hasLimit = size != null && size > 0; if (hasLimit) { queryExpression.setLimit(size); dtos = this.dynamoDBMapper.queryPage(clazz, queryExpression).getResults(); } else { dtos = this.dynamoDBMapper.query(clazz, queryExpression); } if (dtos.size() == 0) { return new LazyLoadResult<>(new ArrayList<>(), false, null); } return new LazyLoadResult<>(dtos, hasLimit ? dtos.size() == size : false, dtos.get(dtos.size() - 1).getSk()); } @Override public LazyLoadResult<T> queryIndex(Class<T> clazz, String indexName, String indexSkName, T hashKeyDto, boolean asc, String lastLoadSk, Integer size) { DynamoDBQueryExpression<T> queryExpression = new DynamoDBQueryExpression<T>().withIndexName(indexName) .withHashKeyValues(hashKeyDto).withScanIndexForward(asc); if (lastLoadSk != null) { Condition rangeCondition = new Condition() .withComparisonOperator(asc ? ComparisonOperator.GT : ComparisonOperator.LT) .withAttributeValueList(new AttributeValue(lastLoadSk)); queryExpression.withRangeKeyCondition(indexSkName, rangeCondition); } List<T> dtos; boolean hasLimit = size != null && size > 0; if (hasLimit) { queryExpression.setLimit(size); dtos = this.dynamoDBMapper.queryPage(clazz, queryExpression).getResults(); } else { dtos = this.dynamoDBMapper.query(clazz, queryExpression); } if (dtos.size() == 0) { return new LazyLoadResult<>(new ArrayList<>(), false, null); } T lastLoadDto = dtos.get(dtos.size() - 1); try { Method method = clazz.getMethod("get" + upperCaseFirstLatter(indexSkName)); return new LazyLoadResult<T>(dtos, hasLimit ? dtos.size() == size : false, (String) method.invoke(lastLoadDto)); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } private String upperCaseFirstLatter(String str) { char[] strChar = str.toCharArray(); strChar[0] -= 32; return String.valueOf(strChar); } }
After implementing BasicDao, we write JUnit test to test the implementation class:
Add dependency to pom file:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
Test class:
package com.jessica.dynamodb.favorite.dao; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.UUID; import org.junit.Test; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.favorite.dao.impl.BasicDaoImpl; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.TagDto; public class BasicDaoImplTest { BasicDao<TagDto> basicDao = new BasicDaoImpl<>(); @Test public void testSaveLoadDelete() { // prepare data String userId = "userId1"; String tagId = UUID.randomUUID().toString(); String tagName = "firstTag"; Date now = new Date(); TagDto newTagDto = TagDto.builder().userId(userId).tagId(tagId).tagName(tagName).createTime(now.getTime()) .lastAccessTime(now.getTime()).build(); // run test basicDao.save(newTagDto); TagDto loadedDto = basicDao.load(newTagDto); assertNotNull(loadedDto); // clean data basicDao.delete(newTagDto); loadedDto = basicDao.load(newTagDto); assertNull(loadedDto); } @Test public void testBatchSaveLoadDelete() { // prepare data String userId1 = "userId1"; String tagId1 = UUID.randomUUID().toString(); String tagName1 = "firstTag"; Date date1 = new Date(); String userId2 = "userId2"; String tagId2 = UUID.randomUUID().toString(); String tagName2 = "secondTag"; Date date2 = new Date(); TagDto newTagDto1 = TagDto.builder().userId(userId1).tagId(tagId1).tagName(tagName1).createTime(date1.getTime()) .lastAccessTime(date1.getTime()).build(); TagDto newTagDto2 = TagDto.builder().userId(userId2).tagId(tagId2).tagName(tagName2).createTime(date2.getTime()) .lastAccessTime(date2.getTime()).build(); List<TagDto> dtos = Arrays.asList(newTagDto1, newTagDto2); basicDao.batchSave(dtos); // run test List<TagDto> loadedDtos = basicDao.batchLoad(dtos); assertEquals(2, loadedDtos.size()); // clean data basicDao.batchDelete(dtos); // run test loadedDtos = basicDao.batchLoad(dtos); assertEquals(0, loadedDtos.size()); } @Test public void testQuery() { // prepare data String userId1 = "userId1"; String tagId1 = "firstTag"; String tagName1 = "firstTag"; Date date = new Date(); String tagId2 = "secondTag"; String tagName2 = "secondTag"; String tagId3 = "thirdTag"; String tagName3 = "thirdTag"; String tagId4 = "fouthTag"; String tagName4 = "fouthTag"; String userId2 = "userId2"; String tagId5 = "fifthTag"; String tagName5 = "fifthTag"; TagDto newTagDto1 = TagDto.builder().userId(userId1).tagId(tagId1).tagName(tagName1) .createTime(date.getTime() - 10000).lastAccessTime(date.getTime() - 10000).build(); TagDto newTagDto2 = TagDto.builder().userId(userId1).tagId(tagId2).tagName(tagName2) .createTime(date.getTime() - 5000).lastAccessTime(date.getTime() - 5000).build(); TagDto newTagDto3 = TagDto.builder().userId(userId1).tagId(tagId3).tagName(tagName3) .createTime(date.getTime() - 1000).lastAccessTime(date.getTime() - 1000).build(); TagDto newTagDto4 = TagDto.builder().userId(userId1).tagId(tagId4).tagName(tagName4).createTime(date.getTime()) .lastAccessTime(date.getTime()).build(); TagDto newTagDto5 = TagDto.builder().userId(userId2).tagId(tagId5).tagName(tagName5).createTime(date.getTime()) .lastAccessTime(date.getTime()).build(); List<TagDto> dtos = Arrays.asList(newTagDto1, newTagDto2, newTagDto3, newTagDto4, newTagDto5); basicDao.batchSave(dtos); try { // test sk desc order with size LazyLoadResult<TagDto> lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, false, null, 2); assertEquals(2, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, false, lazyLoadResult.getLoadedDtos().get(1).getSk(), 3); assertEquals(2, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(1).getTagId()); // test sk aes order with size lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, true, null, 2); assertEquals(2, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(1).getTagId()); lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, true, lazyLoadResult.getLoadedDtos().get(1).getSk(), 3); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(2, lazyLoadResult.getLoadedDtos().size()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(1).getTagId()); // test sk aes order without size lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, true, null, null); assertEquals(4, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(2).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(2).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(3).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(3).getTagId()); lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, true, newTagDto4.getSk(), null); assertEquals(2, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(1).getTagId()); // test sk desc order without size lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, false, null, null); assertEquals(4, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(2).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(2).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(3).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(3).getTagId()); lazyLoadResult = basicDao.query(TagDto.class, newTagDto1, false, newTagDto3.getSk(), null); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(1).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(2).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(2).getTagId()); } catch (Exception e) { throw e; } finally { // clean data basicDao.batchDelete(dtos); } } @Test public void testQueryIndex() { // prepare data String userId1 = "userId1"; String tagId1 = UUID.randomUUID().toString(); String tagName1 = "firstTag"; Date date = new Date(); String tagId2 = UUID.randomUUID().toString(); String tagName2 = "secondTag"; String tagId3 = UUID.randomUUID().toString(); String tagName3 = "thirdTag"; String tagId4 = UUID.randomUUID().toString(); String tagName4 = "fouthTag"; TagDto newTagDto1 = TagDto.builder().userId(userId1).tagId(tagId1).tagName(tagName1) .createTime(date.getTime() - 10000).lastAccessTime(date.getTime() - 10000).build(); basicDao.save(newTagDto1); TagDto newTagDto2 = TagDto.builder().userId(userId1).tagId(tagId2).tagName(tagName2) .createTime(date.getTime() - 5000).lastAccessTime(date.getTime() - 5000).build(); basicDao.save(newTagDto2); TagDto newTagDto3 = TagDto.builder().userId(userId1).tagId(tagId3).tagName(tagName3) .createTime(date.getTime() - 1000).lastAccessTime(date.getTime() - 1000).build(); basicDao.save(newTagDto3); TagDto newTagDto4 = TagDto.builder().userId(userId1).tagId(tagId4).tagName(tagName4).createTime(date.getTime()) .lastAccessTime(date.getTime()).build(); List<TagDto> dtos = Arrays.asList(newTagDto1, newTagDto2, newTagDto3, newTagDto4); basicDao.batchSave(dtos); try { // test tag name desc order LazyLoadResult<TagDto> lazyLoadResult = basicDao.queryIndex(TagDto.class, DynamoDBConstant.LSI_ONE_NAME, DynamoDBConstant.LSI_ONE_RANGE_KEY, TagDto.builder().userId(userId1).build(), false, null, 3); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = basicDao.queryIndex(TagDto.class, DynamoDBConstant.LSI_ONE_NAME, DynamoDBConstant.LSI_ONE_RANGE_KEY, TagDto.builder().userId(userId1).build(), false, lazyLoadResult.getLastLoadPos(), 3); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test tag name aes order lazyLoadResult = basicDao.queryIndex(TagDto.class, DynamoDBConstant.LSI_ONE_NAME, DynamoDBConstant.LSI_ONE_RANGE_KEY, TagDto.builder().userId(userId1).build(), true, null, 3); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = basicDao.queryIndex(TagDto.class, DynamoDBConstant.LSI_ONE_NAME, DynamoDBConstant.LSI_ONE_RANGE_KEY, TagDto.builder().userId(userId1).build(), true, lazyLoadResult.getLastLoadPos(), 3); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); } catch (Exception e) { throw e; } finally { // clean data basicDao.batchDelete(dtos); } } }
Implement TagDao
According to the previous analysis, in addition to the basic crud operation, TagDao also needs to support obtaining all tags created by a user. The returned results can be sorted in three ways:
- Sort by tag creation time
- Sort by tag name
- Sort by the last access time of the tag
Therefore, you only need to implement the above three sorting methods in TagDao
package com.jessica.dynamodb.favorite.dao; import com.jessica.dynamodb.constant.TagSortField; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.TagDto; public interface TagDao extends BasicDao<TagDto> { /** * * @param userId * @param tagSortField * @param lastLoadSk * @param size * @param asc * @return */ LazyLoadResult<TagDto> getTagsByUserId(String userId, TagSortField tagSortField, String lastLoadSk, Integer size, boolean asc); }
package com.jessica.dynamodb.favorite.dao.impl; import com.jessica.dynamodb.constant.TagSortField; import com.jessica.dynamodb.favorite.dao.TagDao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.TagDto; public class TagDaoImpl extends BasicDaoImpl<TagDto> implements TagDao { @Override public LazyLoadResult<TagDto> getTagsByUserId(String userId, TagSortField tagSortField, String lastLoadSk, Integer size, boolean asc) { return this.queryIndex(TagDto.class, tagSortField.getLsiName(), tagSortField.getLsiSkName(), TagDto.builder().userId(userId).build(), asc, lastLoadSk, size); } }
Similarly, we can write JUnit test to test the implementation class, or directly write a main method to test:
package com.jessica.dynamodb.favorite.dao; import static org.junit.Assert.assertEquals; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.UUID; import org.junit.Test; import com.jessica.dynamodb.constant.TagSortField; import com.jessica.dynamodb.favorite.dao.impl.TagDaoImpl; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.TagDto; public class TagDaoImplTest { TagDao tagDao = new TagDaoImpl(); @Test public void testGetTagsByUserId() { // prepare data String userId1 = "userId1"; String tagId1 = UUID.randomUUID().toString(); String tagName1 = "firstTag"; Date date = new Date(); String tagId2 = UUID.randomUUID().toString(); String tagName2 = "secondTag"; String tagId3 = UUID.randomUUID().toString(); String tagName3 = "thirdTag"; String tagId4 = UUID.randomUUID().toString(); String tagName4 = "fouthTag"; TagDto newTagDto1 = TagDto.builder().userId(userId1).tagId(tagId1).tagName(tagName1) .createTime(date.getTime() - 10000).lastAccessTime(date.getTime() - 10000).build(); tagDao.save(newTagDto1); TagDto newTagDto2 = TagDto.builder().userId(userId1).tagId(tagId2).tagName(tagName2) .createTime(date.getTime() - 5000).lastAccessTime(date.getTime() - 5000).build(); tagDao.save(newTagDto2); TagDto newTagDto3 = TagDto.builder().userId(userId1).tagId(tagId3).tagName(tagName3) .createTime(date.getTime() - 1000).lastAccessTime(date.getTime() - 1000).build(); tagDao.save(newTagDto3); TagDto newTagDto4 = TagDto.builder().userId(userId1).tagId(tagId4).tagName(tagName4).createTime(date.getTime()) .lastAccessTime(date.getTime()).build(); List<TagDto> dtos = Arrays.asList(newTagDto1, newTagDto2, newTagDto3, newTagDto4); tagDao.batchSave(dtos); try { // test tag name desc order LazyLoadResult<TagDto> lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.TAG_NAME, null, 3, false); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.TAG_NAME, lazyLoadResult.getLastLoadPos(), 3, false); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test tag name aes order lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.TAG_NAME, null, 3, true); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.TAG_NAME, lazyLoadResult.getLastLoadPos(), 3, true); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test create time desc order lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.CREATE_TIME, null, 3, false); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.CREATE_TIME, lazyLoadResult.getLastLoadPos(), 3, false); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test create time aes order lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.CREATE_TIME, null, 3, true); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.CREATE_TIME, lazyLoadResult.getLastLoadPos(), 3, true); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test last access time desc order lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.LAST_ACCESS_TIME, null, 3, false); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.LAST_ACCESS_TIME, lazyLoadResult.getLastLoadPos(), 3, false); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); // test last access time aes order lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.LAST_ACCESS_TIME, null, 3, true); assertEquals(3, lazyLoadResult.getLoadedDtos().size()); assertEquals(true, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId1, lazyLoadResult.getLoadedDtos().get(0).getTagId()); assertEquals(tagId2, lazyLoadResult.getLoadedDtos().get(1).getTagId()); assertEquals(tagId3, lazyLoadResult.getLoadedDtos().get(2).getTagId()); lazyLoadResult = tagDao.getTagsByUserId(userId1, TagSortField.LAST_ACCESS_TIME, lazyLoadResult.getLastLoadPos(), 3, true); assertEquals(1, lazyLoadResult.getLoadedDtos().size()); assertEquals(false, lazyLoadResult.isHasMore()); assertEquals(userId1, lazyLoadResult.getLoadedDtos().get(0).getUserId()); assertEquals(tagId4, lazyLoadResult.getLoadedDtos().get(0).getTagId()); } catch (Exception e) { throw e; } finally { // clean data tagDao.batchDelete(dtos); } } }
Implement FavoriteDataDao
According to the requirements, in addition to the basic crud operation, FavoriteDataDto has two methods to implement:
- Get all the data collected by the user and sort them in descending order according to the collection time. You only need to query with userId as hashKey, which has been implemented in BasicDao
- Get all the collection data of a specific type and sort them in descending order according to the collection time, so you only need to implement this method and query the Global Secondary Index with userid & type as hashKey
package com.jessica.dynamodb.favorite.dao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.FavoriteDataDto; import com.jessica.dynamodb.favorite.dto.FavoriteDataType; public interface FavoriteDataDao extends BasicDao<FavoriteDataDto> { /** * get given type of data for userId * * @param userId * @param dataType * @param lastLoadSk * @param size * @param asc * @return */ LazyLoadResult<FavoriteDataDto> getFavoriteData(String userId, FavoriteDataType dataType, String lastLoadSk, Integer size, boolean asc); }
package com.jessica.dynamodb.favorite.dao.impl; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.favorite.dao.FavoriteDataDao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.FavoriteDataDto; import com.jessica.dynamodb.favorite.dto.FavoriteDataType; public class FavoriteDataDaoImpl extends BasicDaoImpl<FavoriteDataDto> implements FavoriteDataDao { @Override public LazyLoadResult<FavoriteDataDto> getFavoriteData(String userId, FavoriteDataType dataType, String lastLoadSk, Integer size, boolean asc) { return this.queryIndex(FavoriteDataDto.class, DynamoDBConstant.GSI_ONE_NAME, DynamoDBConstant.GSI_ONE_RANGE_KEY, FavoriteDataDto.builder().userId(userId).dataType(dataType).build(), asc, lastLoadSk, size); } }
FavoriteDataDaoImpl finally calls the queryIndex method of BasicDaoImpl. The test case has been written in BasicDaoImplTest, so the test case can be omitted
Implement FavoriteDataTagDao
According to requirements, FavoriteDataTagDto has the following features:
- Get all the tag s added to the collected data and query with userId as hashKey, which has been implemented in BasicDao
- Get all the favorite data with a tag added, sort them in descending order according to the collection time, take userid & TagID as the hashKey, and query the first Global Secondary Index
- Get all the collection data of a specific type with a tag added, sort them in descending order according to the collection time, take userid & TagID & type as the hashKey, and query the second Global Secondary Index
package com.jessica.dynamodb.favorite.dao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.FavoriteDataTagDto; import com.jessica.dynamodb.favorite.dto.FavoriteDataType; public interface FavoriteDataTagDao extends BasicDao<FavoriteDataTagDto> { /** * * get data ids for userId with given tagId * * @param userId * @param tagId * @param lastLoadSk * @param size * @param asc * @return */ LazyLoadResult<String> getFavoriteDataIds(String userId, String tagId, String lastLoadSk, Integer size, boolean asc); /** * get data ids for userId with given tagId and type * * @param userId * @param tagId * @param dataType * @param lastLoadSk * @param size * @param asc * @return */ LazyLoadResult<String> getFavoriteDataIds(String userId, String tagId, FavoriteDataType dataType, String lastLoadSk, Integer size, boolean asc); }
package com.jessica.dynamodb.favorite.dao.impl; import java.util.stream.Collectors; import com.jessica.dynamodb.constant.DynamoDBConstant; import com.jessica.dynamodb.favorite.dao.FavoriteDataTagDao; import com.jessica.dynamodb.favorite.data.LazyLoadResult; import com.jessica.dynamodb.favorite.dto.FavoriteDataTagDto; import com.jessica.dynamodb.favorite.dto.FavoriteDataType; public class FavoriteDataTagDaoImpl extends BasicDaoImpl<FavoriteDataTagDto> implements FavoriteDataTagDao { @Override public LazyLoadResult<String> getFavoriteDataIds(String userId, String tagId, String lastLoadSk, Integer size, boolean asc) { LazyLoadResult<FavoriteDataTagDto> result = this.queryIndex(FavoriteDataTagDto.class, DynamoDBConstant.GSI_ONE_NAME, DynamoDBConstant.GSI_ONE_RANGE_KEY, FavoriteDataTagDto.builder().userId(userId).tagId(tagId).build(), asc, lastLoadSk, size); return new LazyLoadResult<>( result.getLoadedDtos().stream().map(FavoriteDataTagDto::getDataId).collect(Collectors.toList()), result.isHasMore(), result.getLastLoadPos()); } @Override public LazyLoadResult<String> getFavoriteDataIds(String userId, String tagId, FavoriteDataType dataType, String lastLoadSk, Integer size, boolean asc) { LazyLoadResult<FavoriteDataTagDto> result = this.queryIndex(FavoriteDataTagDto.class, DynamoDBConstant.GSI_TWO_NAME, DynamoDBConstant.GSI_TWO_RANGE_KEY, FavoriteDataTagDto.builder().userId(userId).tagId(tagId).dataType(dataType).build(), asc, lastLoadSk, size); return new LazyLoadResult<>( result.getLoadedDtos().stream().map(FavoriteDataTagDto::getDataId).collect(Collectors.toList()), result.isHasMore(), result.getLastLoadPos()); } }
FavoriteDataTagDao finally calls the queryIndex method of BasicDaoImpl. The test case has been written in BasicDaoImplTest, so the test case can be omitted