使用DynamoDB

第0部分–应用程序。

我们有一个名为Tasqui的应用程序,可以在存储库中找到它。 这是命令行的另一个待办事项列表。 我知道,很有创造力。

现在,该应用程序具有addtasksremove 3个主要操作。

 $ tasqui  Usage: tasqui [OPTIONS] COMMAND [ARGS]...  Options: 
   -h, --help Show this message and exit  Commands: 
   add    Add new task 
   tasks  Prints all tasks 
   delete Delete a task 

这是一个非常简单的应用程序,所有内容都保存到JSON文件中。 最近,我认为在我的个人和办公笔记本电脑之间进行所有同步都是一个好主意。

为此使用关系数据库会很烦人。 我现在不想处理架构,也不想拘泥于过去的决定。 由于该应用程序已经保存了JSON文件,因此DynamoDB是一个不错的选择(如果我选择了RDBMS,则无法编写DynamoDB)。

第1部分–访问DynamoDB和

我们需要访问我们的应用程序才能进行读写。 每个应用程序都有一个用户是一个好习惯,因此我们将创建一个应用程序并为其分配一个角色。

在为您的应用程序创建用户时,您必须知道要从Access Type开始向他授予哪种权限。 在这种情况下,我们正在为我们的应用程序创建一个用户,因此我们没有任何理由授予对AWS管理控制台的访问权限。

DynamoDB

展望未来,我们必须确定用户所需的访问级别,并选择适当的角色。 该应用程序正在从单个DynamoDB表读取和写入,您将向AmazonDynamoDBFullAccess授予对所有表和功能的访问权限。 如果您需要更严格的限制,则可以创建一个自定义策略,仅授予对所需资源的访问权限。

DynamoDB

创建用户后,我们将获得一个Access Key ID和一个Secret Access Key ,您需要将这两个密钥保存在安全的地方,因为您将需要使用它们来连接DynamoDB。 如果密钥对发生问题,则必须创建一个新的密钥对。

如果没有安装和配置aws cli,则可以执行以下步骤:– 安装AWS CLI配置AWS CLI

现在我们已经完成了所有设置,我们可以继续前进并在我们的应用程序上开始工作。

提醒:始终向用户提供尽可能少的特权 ,如果访问密钥泄漏,则恢复的麻烦将更少。

第2部分–进行变更

通过为所有需要的方法编写集成测试,可以以测试驱动的方式进行这些更改。 唯一的问题是:

 How we are going to test our changes? 

幸运的是,Amazon提供了可与docker一起使用的DynamoDB本地版本,因此我认为我们应该使用它。

2.0 –设置DynamoDB docker容器

我们可以开始创建docker-compose.yml并映射端口,不需要其他更改,因为默认配置就是我们要测试的配置。 您可以使用docker-compose up启动数据库。

 version: '3.1'  services:  dynamo: 
     image: amazon/dynamodb-local: 1.11 . 475 
     ports: 
     - "8000:8000" 

默认配置为:

 Port: 8000 # => Default port  InMemory: true # => The database will be saved in memory, everytime your container stops you will lose all the data  DbPath: null # => Path of the database file, can't be used with InMemory  SharedDb: false # => Use the same database independent of region and credentials  shouldDelayTransientStatuses: false # => It's a delay to simulate the database in a real situation  CorsParams: * # => CORS configuration to give access to foreign resources 

我们可以通过在命令行中执行来查看一切是否正常:

 $ aws dynamodb list-tables --endpoint-url http: //localhost:8000  { 
     "TableNames" : []  } 

--endpoint-url http://localhost:8000非常重要,没有此选项,请求将被重定向到默认端点。

随着容器的运行,我们可以开始考虑如何为该功能设置测试。 首先是将DynamoDB sdk引入我们的项目:

 implementation 'software.amazon.awssdk:dynamodb:2.4.0' 

2.1 –首次集成测试

现在我们终于可以开始编写一些代码了,我们已经有了一个存储库,并且希望能够在实现之间进行切换。 因此,让我们使用save方法从LocalFileTaskRepository提取一个接口。

首先,我们使用save方法从存储库中提取一个interface

 interface TaskRepository { 
     fun save(task: Task)  } 

让我们对我们的存储库进行测试。 开始测试之前,我们需要在进行任何测试之前连接到数据库并创建表。

 DynamoDBTaskRepositoryShould { class DynamoDBTaskRepositoryShould { 
     @Test 
     internal fun `add Task to table`() { 
         val endpoint = " http://localhost:8000 " 
         val dynamoDbClient = DynamoDbClient.builder() 
             .endpointOverride(URI.create(endpoint)) 
             .build() 
     }  } 

连接非常简单,因为我们不必进行身份验证即可连接到本地DynamoDB,我们要做的唯一一件事就是将端点设置为http://localhost:8000 。 现在,使用dynamoDbClient我们可以继续创建表。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         ... 
         dynamoDbClient.createTable { builder -> 
             builder.tableName( "tasqui" ) 
             builder.provisionedThroughput { provisionedThroughput -> 
                 provisionedThroughput.readCapacityUnits( 5 ) 
                 provisionedThroughput.writeCapacityUnits( 5 ) 
             } 
             builder.keySchema( 
                 KeySchemaElement.builder() 
                     .attributeName( "task_id" ) 
                     .keyType(KeyType.HASH) 
                     .build() 
             ) 
             builder.attributeDefinitions( 
                 AttributeDefinition.builder() 
                     .attributeName( "task_id" ) 
                     .attributeType(ScalarAttributeType.N) 
                     .build() 
             ) 
         } 
     }  } 

那么,此createTable方法中发生了什么? 让我们按命令分解命令并查看:

 builder.tableName( "tasqui" ) 

这是一个相当简单的部分,我们只需设置表的名称,然后我们可以:

 builder.provisionedThroughput { provisionedThroughput -> 
     provisionedThroughput.readCapacityUnits( 5 ) 
     provisionedThroughput.writeCapacityUnits( 5 )  } 

这部分是在查看表的吞吐量,即通过表读取和写入内容的能力。 我们将读写吞吐量设置为5,但是5到底是什么? 吞吐量如何计算?

吞吐量以units为单位进行units ,每个unit可能具有不同的值,具体取决于您执行的操作类型。 对于读取,每个unit为4Kb / s,以实现始终如一的强劲读取,为8Kb / s,以实现最终一致。 写入要容易一些, unit为1Kb / s,因此强一致性或最终一致性之间没有任何区别。

在这种情况下,选择5是因为这是Amazon在免费套餐中提供给您的默认值。

移至实际表,我们必须设置主键:

 builder.keySchema( 
     KeySchemaElement.builder() 
         .attributeName( "task_id" ) 
         .keyType(KeyType.HASH) 
         .build()  )  builder.attributeDefinitions( 
     AttributeDefinition.builder() 
         .attributeName( "task_id" ) 
         .attributeType(ScalarAttributeType.N) 
         .build()  ) 

这仅通过将keyType定义为HASH来将主键设置为task_id并具有Partition Key ,然后将键的类型设置为integer (在这种情况下为ScalarAttributeType.N 。 您还可以设置具有stringbinary

现在一切准备就绪,我们可以开始写断言了。 我们希望存储库将任务保存在数据库中,因此我们可以查询刚刚保存的对象以查看它是否确实存在。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         ... 
         val task = Task( 1 , "Task description" ) 
         val item = dynamoDbClient.getItem( 
                 GetItemRequest.builder() 
                     .tableName( "tasqui" ) 
                     .key(mapOf( "task_id" .key(mapOf( "task_id" to AttributeValue.builder().n( "1" ).build())) 
                     .build()).item() 
         val storedTask = Task(item[ "task_id" ]!!.n().toInt(), item[ "description" ]!!.s()) 
         Assertions.assertEquals(storedTask, task) 
     }  } 

sdk提供了getItem方法来查询数据库中的特定项目,我们必须构建一个传递tableNamekeyGetItemRequest

key是一个映射,其中包含主键的名称和要查询的值。 getItem的返回是一个GetItemResponse ,它只有两个方法itemconsumedCapacity 。 在这种情况下,我们得到的itemMap<String, Attribute> ,我们可以在其中映射到我们的Task对象。 构建AttributeValue并不是很复杂,但是方法背后的命名并不是最好的,因此您可以查看文档以了解它们的作用。 最后,我们将数据库中的任务与我们的任务进行比较。

唯一缺少的是我们的实际类以及在设置和断言之间对save方法的调用。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         ... 
         val task = Task( 1 , "Task description" ) 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDbClient) 
         dynamoDbTaskRepository.save(task) 
         val item = dynamoDbClient.getItem( 
         ... 
     }  } 
 DynamoDbTaskRepository( class DynamoDbTaskRepository( private val dynamoDbClient: DynamoDbClient) : TaskRepository { 
     override fun save(task: Task) { 
         TODO( "not implemented" ) //To change body of created functions use File | Settings | File Templates. //To change body of created functions use File | Settings | File Templates. 
     }  } 

运行测试,并查看它们因正确的原因而失败。

 kotlin.NotImplementedError: An operation is not implemented: not implemented 
     at com.github.andre2w.tasqui.DynamoDbTaskRepository.save(DynamoDBTaskRepository.kt: 8 ) 
     at com.github.andre2w.tasqui.DynamoDbTaskRepositoryShould.add Task to DynamoDB$com_github_andre2w_tasqui_main(DynamoDbTaskRepositoryShould.kt: 47 )  ... 

现在,我们准备实施生产代码。 我们将dynamoDBClient注入存储库中,因此下一步是:

  1. 创建要插入的item
  2. 使用putItem插入项目
 DynamoDbTaskRepository( class DynamoDbTaskRepository( private val dynamoDbClient: DynamoDbClient) : TaskRepository { 
     override fun save(task: Task) { 
         val item = mapOf( 
             "task_id" to AttributeValue.builder().n(task.id.toString()).build(), 
             "description" to AttributeValue.builder().s(task.description).build() 
         ) 
         dynamoDbClient.putItem( 
             PutItemRequest.builder() 
                 .tableName( "tasqui" ) 
                 .item(item) 
                 .conditionExpression( "attribute_not_exists(task_id)" ) 
                 .build()) 
     }  } 

我们改造Task进入Map<String, AttributeValue>和我们使用putItem方法与PutItemRequest我们建立在表中插入该项目。 在.conditionExpression("attribute_not_exists(task_id)")旁边,插入似乎非常简单。 此conditionExpression方法是在对项目进行更改之前过滤或创建检查的一种方法,如果该任务已经存在,我们不想覆盖该任务,您可以在此处查看有关conditionExpression的文档。

准备好一切之后,我们再次运行测试,而不是珠宝,然后发生这种情况:

 software.amazon.awssdk.services.dynamodb.model.ResourceInUseException: Cannot create preexisting table (Service: DynamoDb, Status Code: 400 , Request ID: d9056558-bb38- 4119 -a89d-d2323e859a68) 

等等,为什么? 这是一个教程,如果没有错误,应该可以顺利进行,如果我想要我可以去其他地方的错误。 发生此错误是因为我们在上一个测试中创建了表,并且每次运行测试时,我们都需要一个新表,该表非常新,可以移动到Bel-Air与他的叔叔同住。 因此,这次我们要进行docker-compose down擦除容器,然后再次使用docker-compose up -d 。 现在我们的测试应该通过了。

测试通过了,但是依赖于表不存在的事实。 这不是很好,因此必须在测试开始之前通过删除表来解决。 这段代码是在createTable call之前添加的,并在同一容器中多次运行测试(或者只是不断疯狂地运行测试以使其一次又一次通过)。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         ... 
         val tableExists = dynamoDbClient.listTables() 
             .tableNames() 
             .contains( "tasqui" ) 
         if (tableExists) { 
             dynamoDbClient.deleteTable( 
                 DeleteTableRequest.builder() 
                 .tableName( "tasqui" ) 
                 .build()) 
         } 
         dynamoDbClient.createTable { builder -> 
         ... 
         } 
     }  } 

2.2 –重构

随着第一个测试的通过,是时候进行下一步了,我们需要重构代码。 值得注意的第一件事是测试中的所有DynamoDB代码,创建连接,删除和创建表,检索Task,所有这些东西都不应放在测试中,而是可以创建一个新的帮助器类。

2.2.0介绍

具有所有将要使用的测试方法的封装的帮助器类,因此无需担心实现。 第一步是创建类,并使该类以DynamoDbClient作为属性生成DynamoDBHelper类。

添加具有该属性的DynamoDBHelper ,并创建一个连接数据库的静态函数,并创建一个DynamoDBHelper的新实例,然后回到测试类中,只需更改旧的dynamoDbClient变量即可使用该助手中的变量。

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     companion object { 
         fun connect(endpoint: String = " http://localhost:8000 " ): DynamoDbHelper { 
             val dynamoDbClient = DynamoDbClient.builder() 
                 .endpointOverride(URI.create(endpoint)) 
                 .build() ?: throw IllegalStateException() 
             return DynamoDbHelper(dynamoDbClient) 
         } 
     }  } 
 @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val dynamoDbClient = dynamoDbHelper.dynamoDbClient 
         ... 
     } 

如果所有测试都通过了并且应该通过(我认为),那么该是进行下一步的时候了。

2.2.1创建表

在这一步中,我们必须将代码从测试类移至帮助程序的初始化。 首先将表的所有代码(创建/删除)提取到方法中。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val dynamoDbClient = dynamoDbHelper.dynamoDbClient 
         setupTable(dynamoDbClient) 
         ... 
     } 
     private fun setupTable(dynamoDbClient: DynamoDbClient) { 
         //all that code to delete and create the table 
     }  } 

将该方法移至DynamoDBHelper类,进行更改,使其可以使用帮助器中的dynamoDbClient ,并通过测试在帮助器中调用该方法:

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     fun setupTable() { 
         val tableExists = dynamoDbClient.listTables() 
             .tableNames() 
             .contains( "tasqui" ) 
         if (tableExists) { 
             dynamoDbClient.deleteTable( 
                 DeleteTableRequest 
                     .builder() 
                     .tableName( "tasqui" ) 
                     .build() 
             ) 
         } 
         dynamoDbClient.createTable { builder -> 
             builder.tableName( "tasqui" ) 
             builder.provisionedThroughput { provisionedThroughput -> 
                 provisionedThroughput.readCapacityUnits( 5 ) 
                 provisionedThroughput.writeCapacityUnits( 5 ) 
             } 
             builder.keySchema( 
                 KeySchemaElement.builder() 
                     .attributeName( "task_id" ) 
                     .keyType(KeyType.HASH) 
                     .build() 
             ) 
             builder.attributeDefinitions( 
                 AttributeDefinition.builder() 
                     .attributeName( "task_id" ) 
                     .attributeType(ScalarAttributeType.N) 
                     .build() 
             ) 
         } 
     }  } 
 @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val dynamoDbClient = dynamoDbHelper.dynamoDbClient 
         dynamoDbHelper.setupTable() 
         val task = Task( 1 , "Task description" ) 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDbClient) 
         dynamoDbTaskRepository.save(task) 
         val item = dynamoDbClient.getItem( 
                 GetItemRequest.builder() 
                     .tableName( "tasqui" ) 
                     .key(mapOf( "task_id" .key(mapOf( "task_id" to AttributeValue.builder().n( "1" ).build())) 
                     .build()).item() 
         val storedTask = Task(item[ "task_id" ]!!.n().toInt(), item[ "description" ]!!.s()) 
         Assertions.assertEquals(storedTask, task) 
     } 

测试通过了,代码中的一切都很好,但是必须手动设置表并不是最佳选择,因此只需将setupTable移至setupTable的初始化并将其DynamoDBHelper私有即可。

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     init { 
         setupTable() 
     } 
     ...  } 
 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val dynamoDbClient = dynamoDbHelper.dynamoDbClient 
         val task = Task( 1 , "Task description" ) 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDbClient) 
         dynamoDbTaskRepository.save(task) 
         val item = dynamoDbClient.getItem( 
                 GetItemRequest.builder() 
                     .tableName( "tasqui" ) 
                     .key(mapOf( "task_id" .key(mapOf( "task_id" to AttributeValue.builder().n( "1" ).build())) 
                     .build()).item() 
         val storedTask = Task(item[ "task_id" ]!!.n().toInt(), item[ "description" ]!!.s()) 
         Assertions.assertEquals(storedTask, task) 
     }  } 

2.2.2从数据库获取任务

这部分与上一部分类似,该方法将被移至助手,而测试将使用新创建的方法。

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     init { 
         setupTable() 
     } 
     fun findById(taskId: String): Task { 
         val item = dynamoDbClient.getItem( 
             GetItemRequest.builder() 
                 .tableName( "tasqui" ) 
                 .key(mapOf( "task_id" .key(mapOf( "task_id" to AttributeValue.builder().n(taskId).build())) 
                 .build() 
         ).item() 
         return buildTask(item) 
     } 
     private fun buildTask(item: MutableMap<String, AttributeValue>) = 
         Task(item[ "task_id" ]!!.n().toInt(), item[ "description" ]!!.s()) 
     ...  } 
 @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val dynamoDbClient = dynamoDbHelper.dynamoDbClient 
         val task = Task( 1 , "Task description" ) 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDbClient) 
         dynamoDbTaskRepository.save(task) 
         val storedTask = dynamoDbHelper.findById(task.id.toString()) 
         Assertions.assertEquals(storedTask, task) 
     } 

Kotlin允许创建扩展功能,因此可以将buildTask方法更改为更惯用的方式,例如Task.from(item)同时使该方法仅在助手内部可见。

开始在Task类内添加companion object

 data class Task(val id: Int, val description: String) { 
     companion object  } 

将它们添加到帮助程序中,添加扩展方法:

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     ... 
     fun findById(taskId: String): Task { 
         val item = dynamoDbClient.getItem( 
             GetItemRequest.builder() 
                 .tableName( "tasqui" ) 
                 .key(mapOf( "task_id" .key(mapOf( "task_id" to AttributeValue.builder().n(taskId).build())) 
                 .build() 
         ).item() 
         return Task.from(item) 
     } 
     ... 
     private fun Task.Companion.from(item: MutableMap<String, AttributeValue>) = 
         Task(item[ "task_id" ]!!.n().toInt(), item[ "description" ]!!.s())  } 

2.2.3最终变更

现在测试并不会被所有数据库代码所dynamoDbClient ,唯一缺少的是删除dynamoDbClient并在帮助器中提取字符串。

 @Test 
     internal fun `add Task to DynamoDB`() { 
         val dynamoDbHelper = DynamoDBHelper.connect() 
         val task = Task( 1 , "Task description" ) 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDbHelper.dynamoDbClient) 
         dynamoDbTaskRepository.save(task) 
         val storedTask = dynamoDbHelper.findById(task.id.toString()) 
         assertEquals(storedTask, task) 
     } 

现在, task_idtasqui所有引用都使用变量而不是字符串。

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     init { 
         setupTable() 
     } 
     private val primaryKey = "task_id" 
     private val tableName = "tasqui" 
     ... 
     fun findById(taskId: String): Task { 
         val item = dynamoDbClient.getItem( 
             GetItemRequest.builder() 
                 .tableName(tableName) 
                 .key(mapOf(primaryKey to AttributeValue.builder().n(taskId).build())) 
                 .build() 
         ).item() 
         return Task.from(item) 
     } 
     ... 

3 –检索数据。

随着变化的进行,现在是时候实现从Dynamo检索数据的时候了。 在第一个测试中,实现了查询,但是要从表中获取所有数据,将需要进行scan操作。

3.0要查询还是要扫描?

  • 查询:查询基于主键搜索表,排序键可用于优化结果,并且结果始终按排序键排序。 所有查询最终都是一致的(除非另有说明),并且始终向前扫描。
  • 扫描:检查表中的每个项目并返回所有数据属性。 可以使用ProjectionExpression参数来优化扫描。 由于“扫描”将转储整个表,然后过滤出结果,因此,如果表增加,则操作将变慢。

3.1实施

扫描是all()方法的正确选项,可以通过以下方式进行测试:

 @Test 
     internal fun `retrieve all Tasks`() { 
         val task1 = Task( 1 , "Task description" ) 
         val task2 = Task( 2 , "Another task description" ) 
         val dynamoDBHelper = DynamoDBHelper.connect() 
         val dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDBHelper.dynamoDbClient) 
         dynamoDBHelper.save(task1, task2) 
         val tasks = dynamoDbTaskRepository.all() 
         assertEquals(listOf(task2, task1), tasks) 
     } 

设置基本上与上一个设置相同,但必须使用DynamoDBHelper 。 仓库中的代码可以在这里使用:

 fun save(vararg tasks: Task) { 
         tasks.forEach { 
             dynamoDbClient.putItem( 
                 PutItemRequest.builder() 
                     .tableName(tableName) 
                     .item(it.toAttributeMap()) 
                     .conditionExpression( "attribute_not_exists(task_id)" ) 
                     .build()) 
         } 
     } 

为了使插入多个任务更容易,可以使用vararg ,它可以像Task ...tasks一样转换为Java中的spread运算符。

运行测试,一切都因正确的原因而失败,需要时间来生产代码。

 DynamoDbTaskRepository( class DynamoDbTaskRepository( private val dynamoDbClient: DynamoDbClient) : TaskRepository { 
     override fun all(): List<Task> { 
         val scanResponse = dynamoDbClient.scan { scan -> 
             scan.tableName( "tasqui" ) 
             scan.limit( 1 ) 
         } 
         return scanResponse.items().map { it.toTask() } 
     } 
     ... 
     private fun MutableMap<String, AttributeValue>.toTask() = 
         Task( this [ "task_id" ]!!.n().toInt(), this [ "description" ]!!.s() )  } 

这应该使测试顺利通过。

3.1重构

这两个测试都在创建到数据库的新连接,我们必须修复该连接只能连接一次,并删除将在其他测试中使用的元素重复项。

将在每个测试中创建帮助程序,并使用该帮助程序创建一个新的连接,这是一件好事,只能在测试开始时创建一次,而且DynamoDBTaskRepository可以在每个新测试中实例化DynamoDBTaskRepository

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     private val dynamoDBHelper: DynamoDBHelper = DynamoDBHelper.connect() 
     private lateinit var dynamoDbTaskRepository: DynamoDbTaskRepository 
     @BeforeEach 
     internal fun setUp() { 
         dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDBHelper.dynamoDbClient) 
     } 
     @Test 
     internal fun `add Task to DynamoDB`() { 
         val task = Task( 1 , "Task description" ) 
         dynamoDbTaskRepository.save(task) 
         val storedTask = dynamoDBHelper.findById(task.id.toString()) 
         assertEquals(storedTask, task) 
     } 
     @Test 
     internal fun `retrieve all Tasks`() { 
         val task1 = Task( 1 , "Task description" ) 
         val task2 = Task( 2 , "Another task description" ) 
         dynamoDBHelper.save(task1, task2) 
         val tasks = dynamoDbTaskRepository.all() 
         assertEquals(listOf(task2, task1), tasks) 
     }  } 

现在,将DynamoDBHelperDynamoDbTaskRepository提取为字段,另一个需要做的更改是在每次测试之前删除表。 重新创建表很容易,因为没有办法删除所有项目,最好的方法是删除表并创建一个新表。 这是存储库已经在做的事情,为设置所有内容而进行的更改是:

使setupTable公开可用:

 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     fun setupTable() { 
         deleteTable() 
         createTable() 
     }  } 

并使测试在每次测试之前重新创建表:

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     private val dynamoDBHelper: DynamoDBHelper = DynamoDBHelper.connect() 
     private lateinit var dynamoDbTaskRepository: DynamoDbTaskRepository 
     @BeforeEach 
     internal fun setUp() { 
         dynamoDbTaskRepository = DynamoDbTaskRepository(dynamoDBHelper.dynamoDbClient) 
         dynamoDBHelper.setupTable() 
     } 
     ...  } 
 It's important to mention here, `Scan` will the items in descending order. So return the items in descending order. So the items in descending order. So if the order is something important for you, a sorting step will have to take place after retrieving the items for the database. In the database. In case of a `Query` instead of a `Scan` the parameter `ScanIndexForward` can be set ` true ` and DynamoDB will return the items in ascending order. 

## 4 –删除我们的东西

错误是人类,删除是忘记。 是时候实现delete方法了。 与往常一样,我们从测试开始,即向数据库中插入一些内容,然后删除刚刚插入的内容,然后检查数据库中是否没有这些内容。

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `delete Task from the table`() { 
         val task = Task( 1 , "Task description" ) 
         dynamoDBHelper.save(task) 
         dynamoDbTaskRepository.delete(task.id) 
         assertThrows<ItemNotFoundInTable> { 
             dynamoDBHelper.findById(task.id.toString()) 
         } 
     }  } 
 class DynamoDBHelper(val dynamoDbClient: DynamoDbClient) { 
     fun findById(taskId: String): Task { 
         val item = dynamoDbClient.getItem( 
             GetItemRequest.builder() 
                 .tableName(tableName) 
                 .key(mapOf(primaryKey to AttributeValue.builder().n(taskId).build())) 
                 .build() 
         ).item() 
         if (item.isEmpty()) 
             throw ItemNotFoundInTable() 
         return Task.from(item) 
     }  } 

此测试中唯一的新事物是assertThrows<ItemNotFoundInTable> ,它检查方法调用是否将引发异常,而ItemNotFoundInTable是创建的异常,由辅助程序抛出以防万一没有返回任何项目。 运行测试,由于正确的原因,它们失败了,因此现在该着手实施了。

 DynamoDbTaskRepository( class DynamoDbTaskRepository( private val dynamoDbClient: DynamoDbClient) : TaskRepository { 
     private val tableName = "tasqui" 
     override fun delete(id: Int) { 
         dynamoDbClient.deleteItem { delete -> 
             delete.tableName(tableName) 
             delete.key(mapOf( "task_id" to id.toAttributeValue())) 
         } 
     } 
     ...  } 

这是最简单的操作,只需要通知tableNamekey ,然后删除就会发生。 这部分没有太多可重构的,因此我们现在可以跳过。

5 –最终倒数(或计数器)

最后要实现的方法是nextId ,即主键生成器。 必须检索最后一个项目,然后将项目ID递增1。 在这种情况下,由于Scan按降序排列,因此限制为一项的Scan将具有所需的效果。

第一个测试可以在一条愉快的道路上开始,那里数据库中已经有一个项目:

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `retrieve the last inserted id plus one`() { 
         val task = Task( 1 , "Task description" ) 
         dynamoDBHelper.save(task) 
         val nextId = dynamoDbTaskRepository.nextId() 
         assertEquals( 2 , nextId) 
     }  } 

实现是:

 DynamoDbTaskRepository( class DynamoDbTaskRepository( private val dynamoDbClient: DynamoDbClient) : TaskRepository { 
     private val tableName = "tasqui" 
     override fun nextId(): Int { 
         val items = dynamoDbClient.scan { scan -> 
             scan.tableName(tableName) 
             scan.attributesToGet( "task_id" ) 
         }.items() 
         val lastId = items 
             .map { it[ "task_id" ]!!.n().toInt() } 
             .max() ?: 0 
         return lastId + 1 
     } 

这是一个类似于all()Scan操作,但带有scan.attributesToGet("task_id")因此响应仅包含task_id并且通常较小。 然后将该结果转换为最大整数。 Kotlin具有elvis运算符?:有助于处理null值,因此,如果不返回任何项目,则该值将为零。 为了解决这种情况,我们添加了一个测试,而没有在安排部分插入任何任务:

 DynamoDbTaskRepositoryShould { class DynamoDbTaskRepositoryShould { 
     @Test 
     internal fun `first id should be 1 `() { 
         val nextId = dynamoDbTaskRepository.nextId() 
         assertEquals( 1 , nextId) 
     }  } 

不需要做很多事情,只需检查DynamoDB的响应是否为空,然后返回1。

所有测试都通过了,存储库的所有内容都已实现,唯一缺少的是真正连接到DynamoDB客户端。

6 –请论文!

实施了存储库的所有方法之后,就可以将应用程序更改为使用DynamoDBTaskRepository

 Runner { class Runner { 
     companion object { 
         @JvmStatic 
         fun main(args: Array<String>) { 
             val taskRepository = DynamoDbTaskRepository() 
             val console = Console() 
             Tasqui() 
                 .subcommands(Add(taskRepository),Tasks(taskRepository, console), Delete(taskRepository)) 
                 .main(args) 
         } 
     }  } 

唯一的问题是,当我们尝试创建新的存储库DynamoDbClient需要注入DynamoDbClient 。 我们没有任何生产代码,因此我们必须创建一些。 帮助器中已经创建了一个连接,我们可以使用存储库:

 DynamoDBConnection { class DynamoDBConnection { 
     companion object { 
         fun connect() : DynamoDbClient { 
             return DynamoDbClient.builder() 
                 .build() ?: throw IllegalStateException() 
         } 
     }  } 
 Runner { class Runner { 
     companion object { 
         @JvmStatic 
         fun main(args: Array<String>) { 
             val taskRepository = DynamoDbTaskRepository(DynamoDBConnection.connect()) 
             val console = Console() 
             Tasqui() 
                 .subcommands(Add(taskRepository),Tasks(taskRepository, console), Delete(taskRepository)) 
                 .main(args) 
         } 
     }  } 

并更改命令以使用TaskRepository接口代替实现。

 Tasqui : CliktCommand() { class Tasqui : CliktCommand() { 
     override fun run() = Unit  }  class Add( private val taskRepository: TaskRepository) : CliktCommand( "Add new task" ) { 
     val description by argument( private val description by argument( "description" , "Task description" ) 
     override fun run() { 
         taskRepository.save(Task(taskRepository.nextId(), description)) 
     }  }  class Tasks( private val taskRepository: TaskRepository, private val console: Console) 
     : CliktCommand( "Prints all tasks" ) { 
     override fun run() { 
         val tasks = taskRepository.all() 
         tasks.map { "${it.id} - ${it.description}" } 
             .forEach(console::print) 
     }  }  class Delete( private val taskRepository: TaskRepository) : CliktCommand( "Delete a task" ) { 
     val taskId by argument(help = private val taskId by argument(help = "Id of the task to be deleted" ). int () 
     override fun run() { 
          taskRepository.delete(taskId) 
     }  } 

这是一种非常简单的连接方式,它将从主文件夹中的.aws/credentials文件获取default配置文件凭据。 如果您需要其他配置文件,Amazon提供ProfileCredentialsProvider 。 您可以在此处查看有关其他身份验证方式的更多信息。

包装我们的应用程序

完成所有您真正想将其用作应用程序的更改之后,可以使用gradle打包,命令gradle assembleDist将在build/distributions文件夹内生成一个ziptar 。 您可以在该文件夹中使用tasqui ,而无需调用java -jar或传递除应用程序的参数之外的任何其他参数。

翻译自: https://www.javacodegeeks.com/2019/02/working-dynamodb.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DynamoDB 是一个 NoSQL 数据库,它使用索引来提高查询性能。在 DynamoDB 中,有两种类型的索引:主键索引和辅助索引。 主键索引是 DynamoDB 表的默认索引,它可以是一个简单的主键(仅由一个属性组成)或复合主键(由两个属性组成)。主键索引可以根据主键属性的值进行查找,可以使用等值查询或范围查询。范围查询可以根据主键的排序顺序返回一系列项目。 辅助索引是基于表的非主键属性创建的索引。辅助索引允许你根据非主键属性进行查询,而不仅仅是根据主键属性。辅助索引可以是局部辅助索引(仅包含表的一部分项目)或全局辅助索引(包含整个表的所有项目)。你可以在创建表时定义局部辅助索引,或在表已创建后添加全局辅助索引。 要使用 DynamoDB 索引并进行排序,你需要考虑以下几点: 1. 主键索引:如果你想根据主键属性进行排序,你可以使用范围查询来获取按顺序排列的项目。请注意,只有复合主键才能支持范围查询,简单主键只支持等值查询。 2. 辅助索引:如果你想根据非主键属性进行排序,你可以创建一个辅助索引,并在查询时指定该索引。根据辅助索引的类型(局部或全局),你可以使用等值查询或范围查询来获取排序的结果。 需要注意的是,DynamoDB 不支持在查询时对结果进行排序。排序需要在应用程序中进行处理。你可以通过在查询结果中使用 SortKey 来获取按顺序排列的项目。 希望这个回答能对你有所帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值