65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AWS DynamoDB 低级 Java API – Spring Boot REST 应用程序

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2019 年 5 月 19 日

CPOL

25分钟阅读

viewsIcon

15201

如何使用 AWS Java 2 API 创建一个使用 Spring Boot 的 REST 应用程序,该应用程序可以读写 DynamoDB 数据库

引言

在本教程中,我们使用 Amazon Web Services Java 2 应用程序编程接口 (API) 创建一个使用 Spring Boot 的 Rest 应用程序,该应用程序可读写 DynamoDB 数据库。本教程假定读者熟悉 AWS、具有 Java 编程经验和 Spring Boot 经验。但是,即使没有这些经验,本教程也应该很有用,因为它提供了大量的补充资源供您查阅。

在这里,我们创建一个简单的数据库,其中包含通过摄像头收集的“观测站”和“观测数据”。不管怎样……请暂时抛开怀疑,继续阅读。现在,假设这些站点需要一种将观测数据上传到关联的 DynamoDB 表格的方法。我们决定使用 Rest API 让站点上传数据。我们使用 Spring Boot Rest 应用程序来实现这个 API。同样,如果这一切听起来可疑,请暂时抛开怀疑,专注于 AWS 代码而非应用程序设计。

在本教程中,我们将:

  • 使用 DynamoDB 控制台创建两个数据库表
  • 使用控制台创建几个项目
  • 创建一个 IAM 编程用户
  • 创建一个提供 Rest 端点的 Spring Boot 应用程序,以便客户端应用程序可以:
    • 写入观测数据
    • 读取观测数据
    • 更新观测数据
    • 删除观测数据
    • 批量写入多个观测数据
    • 有条件地查询站点的观测数据
    • 并有条件地更新观测数据
  • 并使用 Postman 测试 Rest 端点

本教程的目的是探索 DynamoDB,而不是介绍 Spring Boot、Rest 或 JSON,并假设读者对这三者都有基本了解。但是,如果对其中任何一个主题不熟悉,则提供了链接供您在继续之前学习。

NoSQL 数据库

DynamoDB 是一个键值和文档 NoSQL 数据库。如果您不熟悉 NoSQL 文档数据库,应在继续之前熟悉它。以下是介绍 NoSQL 数据库的入门视频。

以下是两篇写得很好的关于 NoSQL 和 DynamoDB 的入门文章。

请注意,Amazon 还提供了 DocumentDB,我们可以将其用作 DynamoDB 的替代方案。但是,DocumentDB 将在不同的教程中介绍。

从最根本的层面来看,DynamoDB 数据库可以描述如下。表由项目组成。一个项目有一个或多个属性。在一个表中,您定义分区键,并可选地定义排序键。分区键是一个键值对,它不仅唯一标识一个项目,还决定了该项目在计算机存储上的分布方式。排序键不仅对项目进行逻辑排序,还相应地存储这些项目。显然,NoSQL 物理存储及其如何实现可扩展性还有更多内容,但这超出了本教程的范围。

Amazon Web Services 和 DynamoDB

Amazon DynamoDB 是一种 NoSQL 键值和文档数据库,作为云服务提供。它完全托管,允许用户避免与托管企业数据库相关的管理任务。与 Amazon 的几乎所有产品一样,它都可以通过 Rest API 进行访问。

Amazon 提供了软件开发工具包 (SDK) 来简化 Rest API 的使用。支持的语言有 Java、C++、C#、Ruby、Python、JavaScript、NodeJs、PHP、Objective-C 和 Go。在本文中,我们使用 Java API。目前 API 有两个版本,在本教程中,我们使用 Java 2 SDK。

Java 2 AWS SDK 是 Java 1.1 AWS SDK 的重写,它从更传统的编程范式(使用构造函数实例化对象,然后使用 setter 设置属性)转变为流式接口/构建器编程风格。

流式接口

流式接口是 Martin Fowler 和 Eric Evans 创造的一个术语。它指的是一种编程风格,其中 public 方法(API)可以链式调用以执行任务。AWS Java SDK 2.0 在使用构建器时就采用了这种风格。构建器任务执行任务,但随后返回构建器的一个实例。这允许将方法链式调用。有关流式接口和构建器的更多信息,请参阅这篇博客文章:Java 的另一种构建器模式

DynamoDB 低级 API

与所有 AWS API 一样,DynamoDB 通过 Rest 端点公开。AWS SDK 提供了一个抽象层,使您无需直接调用 Rest。在该层之上,Java SDK 提供了一个名为 DynamoDBMapper 的类,它允许以类似于 Java Persistence Framework (JPA) 的方式使用 DynamoDB。尽管有用,但使用低级 API 并不难。此外,在许多情况下,您不希望在对象模型中创建依赖于 DynamoDB 的依赖项。

例如,假设我们实现了一个将小部件存储在 DynamoDB 中的系统。如果使用 DynamoDBMapper,则 Widget 模型类将通过将该类映射到 Widgets 表格的注解而依赖于 DynamoDB

或者,如果我们不想使用 DynamoDBMapper,我们可以实现类似于以下图表的内容。它是一个典型的 DAO 模式,其中对 AWS SDK 的唯一直接依赖是 WidgetDaoImpl 类。有关 DAO 设计模式的更多信息,请参阅以下入门文章:DAO 设计模式

在本教程中,我们使用 SDK 直接调用底层的 DynamoDB Rest 端点。另外,请注意,我们不使用 DAO 设计模式,而是为了简洁起见,将数据访问逻辑直接放入控制器类中。但是,我们确实使用了基于 Rest 的 Spring MVC 设计模式。

  • 注意:如果您愿意让您的应用程序依赖于 DynamoDBMapper,您应该考虑使用它,因为它大大简化了与 DynamoDB 的交互。
  • Java: DynamoDBMapper 文档

教程用例 – 站点观测数据

想象一下,我们有负责拍摄照片观测的站点。一个站点有一个坐标、一个地址和一个名称。一个站点有一个坐标。一个站点有一个地址。一个站点可以有无限的观测数据。

尽管本教程不讨论 NoSQL 数据库设计,但从下图中可以看出,我们需要两个表,StationObservation,这是合理的。此外,由于 Observation 表的写入强度非常高——站点将持续向应用程序发送观测数据——因此,不将观测数据作为 Station 实例中的集合包含,而是将其作为一个单独的表保留,这是有意义的。请记住,这些是 JSON 文档,而不是关系表。将 Observations 设计为 Station 内的项目列表是不合理的,并且会导致数据库过大和难以管理。

如果有足够的 Station,为了更高的效率,我们可能会为每个 stationobservations 创建一个单独的表。这将允许更大的吞吐量,用于写入和读取观测数据。但是,在本教程中,我们只是定义一个 stationid 来识别 observationstation,并将在该值上创建一个索引。

DynamoDB 控制台

AWS 管理控制台提供了一种简单易用的基于 Web 的方式来使用 Amazon 的云服务。虽然本教程未涵盖,但对于不熟悉 AWS 的人来说,这里有一个 Amazon 解释管理控制台的短视频。请注意,AWS 还提供命令行界面和应用程序编程接口 (API) 来访问其云服务。

  • AWS 基础知识:LinuxAcademy 提供的 AWS 控制台导航方法。

在开始本教程的编程部分之前,我们必须创建 DynamoDB 数据库。

创建站点表

  • 进入 AWS 管理控制台后,导航到 DynamoDB 控制台。
  • 点击 创建表 按钮。
  • 提供 Station 作为表的名称,id 作为表的主键。

创建站点项目

请记住,DynamoDB 是无模式的。我们创建一个项目,但不定义表的模式。相反,我们创建几个具有所需结构的项目。

  • 点击 项目 选项卡并点击 创建项目 按钮。
  • 创建 idname 属性,将 id 指定为 Number 数据类型,将 name 指定为 String。分别赋值为 221 和 “Potomac Falls”。

  • 创建一个名为 address 的属性,并将其指定为 Map 数据类型。
  • address 映射中添加 citystreetzip 属性作为 String 数据类型。在下面的示例中,我将 Potomac230 Falls Street22333 分配为属性值。
  • 创建一个 coordinate 作为 Map,并为其分配 latitudelongitude 属性作为 Number 数据类型。我将 38.993465-77.249247 分别赋值给 latitudelongitude

  • 再重复一个站点。

我们在 Station 表中创建了两个项目。以下是这两个项目的 JSON 格式。

{
  "address": {
    "city": "Potomac",
    "street": "230 Falls Street",
    "zip": "22333"
  },
  "coordinate": {
    "latitude": 38.993465,
    "longitude": -77.249247
  },
  "id": 221,
  "name": "Potomac Falls"
}
{
  "address": {
    "city": "Frederick",
    "street": "9871 River Street",
    "zip": "221704"
  },
  "coordinate": {
    "latitude": 39.23576,
    "longitude": -77.4545
  },
  "id": 234,
  "name": "Monocacy River"
}

您可以通过选择项目,然后选择弹出窗口中的文本视图来查看创建项目后的 JSON。

请注意,前面的 JSON 文档是通用 JSON。实际的 JSON,即 DynamoDB 存储的 JSON(包括数据类型),如下所示。其中 M、S、N、SS 等代表元素数据类型。

{
  "address": {
    "M": {
      "city": {
        "S": "Frederick"
      },
      "street": {
        "S": "9871 River Street"
      },
      "zip": {
        "S": "221704"
      }
    }
  },
  "coordinate": {
    "M": {
      "latitude": {
        "N": "39.23576"
      },
      "longitude": {
        "N": "-77.4545"
      }
    }
  },
  "id": {
    "N": "234"
  },
  "name": {
    "S": "Monocacy River"
  }
}

DynamoDB 数据类型为:

  • 字符串 = S
  • 数字 = N
  • 布尔值 = BOOL
  • 二进制 = B
  • 日期 = S (存储为字符串)
  • 字符串集 = SS
  • 数字集 = NS
  • 二进制集 = BS
  • 映射 = M
  • 和列表 = L

例如,在下面的 JSON 文档中,一个观测的 addresscoordinate 都是 Map 数据类型,citystreetzipString 数据类型,而 latitudelongitudeNumber 数据类型。

您可以在弹出窗口中切换 JSON 和 DynamoDB JSON,如下所示(请注意 DynamoDB JSON checkbox)。

创建观测表

创建 Station 表后,我们需要创建 Observation 表。

  • 创建一个名为 Observation 的新表。
  • 为其分配分区键 id 和排序键 stationid

复合键(分区键和排序键)

分区键是表的主键,由单个属性组成。DynamoDB 使用此键创建哈希,以确定项目的存储方式。单独使用时,分区键唯一标识一个项目,因为没有两个项目可以具有相同的分区键。但是,当还定义了排序键时,一个或多个项目可以具有相同的分区键,前提是分区键与排序键的组合是唯一的。可以将其视为复合键。

排序键有助于 DynamoDB 更有效地存储项目,因为它将具有相同排序键的项目分组在一起(因此得名排序键,因为它使用此键对项目进行排序)。

一个观测数据应该有一个标识它的 id,并且观测数据应该按站点排序,所以我们将 stationid 定义为表的排序键。

创建示例观测数据

Station 表一样,创建一些 Observation 项目而不是定义模式。

  • 找到三个小尺寸的图像用于本项目。如果您愿意,可以使用本教程 Git 项目中的三个示例图像。
  • 导航到 Code Beautify 的 将图像转换为 Base64 网页,并将这三张图像转换为 Base64 字符串
  • 创建观测数据的 JSON 列表。
  • 或者,如果您愿意,只需使用本教程 Git 项目中提供的 JSON sampleData.json 文件。

以下是包含四个观测数据的 JSON 列表。图像 base64 字符串 被截断,以便在此处轻松显示。您可以从本教程的 Git 项目中获取名为 observations.json 的原始文件。

{
  [
    {
      "stationid": 221,
      "date": "1992-03-12",
      "time": "091312",
      "image": "/9j/4AAQSkZJRgABAQAAYABg <snip> kf/9k="
    },
    {
      "stationid": 221,
      "date": "1999-09-22",
      "time": "071244",
      "image": "/9j/4AAQSkZJ <snip> D9KhoA//2Q=="
    },
    {
      "stationid": 234,
      "date": "2001-11-09",
      "time": "111322",
      "image": "/9j/4AAQSkZ <snip> WoGf/9k="
    },
    {
      "stationid": 234,
      "date": "2013-01-12",
      "time": "081232",
      "image": "/9j/4AAQS <snip> q5//2Q=="
    }
  ]
}

Base64 编码

图像是二进制的。但是,所有二进制数据都可以通过正确编码和解码的 String 来表示。Base64 是一种编码方案,可将二进制数据转换为 string。它很有用,因为它允许在文本文件(例如网页或 JSON 文档)中嵌入二进制数据(例如图像)。DynamoDB 在传输数据时使用 Base64 格式将二进制数据编码为 string,并在存储数据时将字符串解码为二进制数据。因此,发送到我们创建的 Rest 端点的图像应进行 base64 编码。

创建 IAM 应用程序用户

在开始 Spring Boot 应用程序之前,我们需要一个具有对 AWS DynamoDB API 编程访问权限的用户。如果您不熟悉 IAM,以下入门视频应该会有所帮助。否则,我们来创建一个用户。

  • 导航到 IAM 控制台并单击“添加用户”。
  • 创建一个名为 DynamoDBUser 的新用户。

  • DynamoDBUser 分配 编程访问权限

  • 创建一个名为 dynamo_users 的新组,并授予 AmazonDynamoDBFullAccess 权限。
  • DynamoDBUser 分配给 dynamo_users 组。

  • 如果您正确创建了用户,您应该会看到以下 摘要 屏幕。
  • 将凭证文件 credentials.csv 保存到本地硬盘。

Spring Boot 应用程序

现在我们已经创建了两个所需的表并创建了一个用户,我们可以开始示例应用程序了。我们创建一个 Rest API,供站点保存、检索、更新和删除观测数据。Spring Boot 的解释不多,所以如果您从未创建过 Spring Boot Rest 应用程序,您可能需要考虑完成一两个关于 Spring Boot 和 Rest 的教程。以下是两个教程的链接;但是,网上还有很多。

设置项目

  • 创建一个新项目,我使用 Eclipse 创建了一个新的 Maven 应用程序。
  • 修改 Maven POM 文件以匹配以下 POM。
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>com.tutorial.aws</groupId>
      <artifactId>DynamoDbTutorial</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <parent>
        <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-parent</artifactId>
          <version>2.0.5.RELEASE</version>
      </parent>
      <properties>
        <java.version>1.8</java.version>
      </properties>
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>software.amazon.awssdk</groupId>
    	<artifactId>bom</artifactId>
    	<version>2.5.25</version>
    	<type>pom</type>
    	<scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>com.jayway.jsonpath</groupId>
          <artifactId>json-path</artifactId>
          <scope>test</scope>
        </dependency>
        <dependency>
          <artifactId>auth</artifactId>
          <groupId>software.amazon.awssdk</groupId>
        </dependency>
        <dependency>
          <artifactId>aws-core</artifactId>
          <groupId>software.amazon.awssdk</groupId>
        </dependency>
        <dependency>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>auth</artifactId>
        </dependency>
        <dependency>
          <artifactId>dynamodb</artifactId>
          <groupId>software.amazon.awssdk</groupId>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.1</version>
            <executions>
              <execution>
    	    <phase>package</phase>
    	    <goals>
                  <goal>jar</goal>
    	    </goals>
    	    <configuration>
     	      <classifier>client</classifier>
    	      <includes>
    	        <include>**/factory/*</include>
    	      </includes>
    	    </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </project>

    在 POM 中,我们定义了 AWS 物料清单 (BOM) 和所需的 AWS 库。请注意,当使用 BOM 时,无需指定库版本,因为 BOM 管理版本。我们还定义了所需的 Spring Boot 库。

  • resources 文件夹中创建一个 application.properties 文件。打开 credentials.csv 并将凭据添加到文件中,属性名称如下。
  • 注意:此用户在发布本教程之前已删除。
    cloud.aws.credentials.accessKey=AK <snip> WP
    cloud.aws.credentials.secretKey=yLJJ <snip> asUR
    cloud.aws.region.static=us-east-1

创建 Spring Boot 应用程序类

  • com.tutorial.aws.dynamodb.application 包中创建一个名为 SiteMonitorApplication 的新类。
  • 使用 @SpringBootApplication 注解该类。
  • 创建 main 方法并让它启动 Spring Boot 应用程序。
    package com.tutorial.aws.dynamodb.application;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.ComponentScan;
    
    @SpringBootApplication
    @ComponentScan({ "com.tutorial.aws.dynamodb" })
    public class SiteMonitorApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SiteMonitorApplication.class, args);
        }
    }

创建观测数据对象

  • com.tutorial.aws.dynamodb.model 包中创建一个名为 Observation 的类。
  • 创建与上面创建的 JSON 数据中具有相同名称和类型的变量。
    package com.tutorial.aws.dynamodb.model;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import java.util.List;
    
    public class Observation {
    	private long stationid;
    	private String date;
    	private String time;
    	private String image;
            private List<String> tags;
    	
    	public long getStationid() {
    		return stationid;
    	}
    
    	public void setStationid(long stationid) {
    		this.stationid = stationid;
    	}
    
    	public String getDate() {
    		return date;
    	}
    
    	public void setDate(String date) {
    		this.date = date;
    	}
    
    	public String getTime() {
    		return time;
    	}
    
    	public void setTime(String time) {
    		this.time = time;
    	}
    
    	public String getImage() {
    		return image;
    	}
    
    	public void setImage(String image) {
    		this.image = image;
    	}
    
    	public void setTags(List<String> tags) {
    		this.tags = tags;
    	}
    	
    	public List<String> getTags() {
    		return this.tags;
    	}
    	
    	@Override
    	public String toString() {
    		try {
    			ObjectMapper mapper = new ObjectMapper();
    			return mapper.writeValueAsString(this);
    		} catch (JsonProcessingException e) {
    			e.printStackTrace();
    			return null;
    		}
    	}
    }

    Observation 对象的属性与 JSON Observation 文档中的属性相同。请注意,在 toString 方法中,我们使用了 Jackson 库中的 ObjectMapper。我们没有在 POM 中包含此库,因为 spring-boot-starter-web 库包含此库。

    ObjectMapper 将 JSON 映射到对象,并将对象映射到 JSON。这是 Spring Rest 完成此任务的方式。在 toString 方法中,我们告诉 ObjectMapper 实例将 Observation 对象写入为 JSON string。有关 ObjectMapper 的更多信息,这里有一个更深入地解释该类的教程:Jackson ObjectMapper

创建 Rest 控制器

Rest 控制器提供外部 API,供站点向我们的应用程序发送数据。通过 API,客户端应用程序将数据传输到 DynamoDB 数据库。不同的站点可以使用任何支持 Rest 的语言开发自己的客户端应用程序。唯一的要求是站点的数据遵循预期的 JSON 格式。

  • 注意:我们通过将数据访问直接放入控制器来违反 MVC 设计模式。请暂时忽略这种反模式。

让我们创建一个 Rest 控制器来定义我们应用程序的 API。

  • com.tutorial.aws.dynamodb.api 包中创建一个名为 ObservationApiController 的类,并使用 @RestController 注解对其进行标注。
  • 将其顶级路径指定为 /observations
  • 创建一个用于上传新 Observation 的 Rest 端点。为其分配 /observation 映射,并将方法命名为 createObservation
  • 让该方法将 Observation 作为请求的正文。
  • 让该方法将上传的 Observation 打印到命令行。
    package com.tutorial.aws.dynamodb.api;
    
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import com.tutorial.aws.dynamodb.model.Observation;
    
    @RestController
    @RequestMapping(value = "/observations")
    public class ObservationApiController {
    
    	@PostMapping("/observation")
    	public void createObservation(@RequestBody Observation 
              observation) {
    	  System.out.println(observation.toString());
    	}	
    }
  • 使用 Maven 编译应用程序并启动应用程序。
  • 应用程序启动后,我们可以使用 Postman 进行测试。

使用 Postman 进行测试

Postman 是一个用于测试 JSON 端点的有用工具。如果您从未使用过 Postman,您可能需要先完成一两个教程。

  • Postman 学习中心:入门
  • 创建一个名为 AddObservation 的新请求,该请求执行 Rest 端点。
    https://:8080/observations/observation
  • 将之前创建的 JSON 文档中的一个观测数据放入请求的 Body 中。将类型指定为 JSON (application/json)。

    Postman 中用于保存观测数据的 JSON 请求。
  • 点击 发送 将请求发送到 Spring Rest 端点。如果一切正确,您应该会看到 Observation 以 JSON 格式打印到命令行。
    {"stationid":221,"date":"1992-03-12", "time":"091312",
     "image":"/9j/4AAQSkZJRgAB <snip> Wxkf/9k=","tags":null}
  • 复制图像 base64 字符串 并导航到 CodeBeautify 网站的 将您的 Base64 转换为图像 网页。将 string 粘贴到提供的 textarea 中,然后点击 生成图像。如果 base64 字符串 发送正确,您应该会看到发送到 Rest 端点的相同图像。

创建 DynamoDB 客户端

现在我们已经有了基本的 Spring Boot 应用程序,我们可以开始构建实际的 DynamoDB API 了。但在使用 DynamoDB 之前,我们需要创建一个 DynamoDBClient 实例。

  • com.tutorial.aws.dynamodb.service 包中创建一个名为 ObservationService 的类。
  • 添加 spring 的 @Service 注解,以便 spring 将此类视为控制器。
  • 添加 keysecretKey 参数,并使用 @Value 注解表示它们是应用程序 application.properties 文件中的参数(Spring Framework 文档)。
  • 创建 @PostConstruct@PreDestroy 方法(或实现一个 Spring <a href="https://docs.springframework.org.cn/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/InitializingBean.html" rel="noreferrer noopener">InitializingBean</a>)。
  • 创建一个名为 dynamoDbClient 的成员变量,类型为 DynamoDbClient
  • initialize 方法中实例化并加载 dynamoDbClient 的凭据。
  • preDestroy 方法中关闭 dynamoDbClient
    package com.tutorial.aws.dynamodb.service;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import com.tutorial.aws.dynamodb.model.Observation;
    import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
    import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
    import software.amazon.awssdk.regions.Region;
    import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
    
    @Service
    public class ObservationService {
    
      @Value("${cloud.aws.credentials.accessKey}")
      private String key;
    
      @Value("${cloud.aws.credentials.secretKey}")
      private String secretKey;
    
      private DynamoDbClient dynamoDbClient;
    
      @PostConstruct
      public void initialize() {
        AwsBasicCredentials awsCreds = AwsBasicCredentials.create(key, secretKey);
        DynamoDbClient client = DynamoDbClient.builder()
            .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
            .region(Region.US_EAST_1).build();
    
        this.dynamoDbClient = client;
      }
    
      @PreDestroy
      public void preDestroy() {
        this.dynamoDbClient.close();
      }
    }

DynamoDBClient

DynamoDBClient 提供对 DynamoDB API 的访问。所有与 DynamoDB 的交互都通过此类完成。它具有用于读取、写入、更新以及与 DynamoDB 表和项目进行其他交互的方法。有关更多信息,请参阅 API 文档

写入观测数据

我们首先将一个 Observation 写入 DynamoDB。或者,您可以说我们将一个项目 PUT 到 DynamoB,因为我们正在向 DynamoDB 发出 HTTP Put 请求。我们通过使用 DynamoDBClientputItem 方法结合 PutItemRequest 来完成此操作。

修改服务类

  • 创建一个名为 writeObservation 的方法,该方法将 Observation 作为参数。
  • 创建一个 HashMap,使用 String 作为键,AttributeValue 作为值。
  • 将每个 Observation 变量放入 HashMap 中,确保键名正确。键名应与 JSON 完全相同。
  • 为每个变量创建 AttributeValueBuilder 时,请确保使用正确的数据类型方法。
  • 构建一个新的 PutItemRequest,然后让 dynamoDbClient 调用其 putItem 方法,将观测数据写入 Observation DynamoDB 表。
    package com.tutorial.aws.dynamodb.service;
    
    import java.util.HashMap;
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import com.tutorial.aws.dynamodb.model.Observation;
    import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
    import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
    import software.amazon.awssdk.core.SdkBytes;
    import software.amazon.awssdk.regions.Region;
    import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
    import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
    import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
    
    <snip>
    
    public void writeObservation(Observation observation) {
       HashMap<String, AttributeValue> observationMap = new HashMap<String,
           AttributeValue>();
    
        observationMap.put("id", AttributeValue.builder()
          .s(observation.getStationid() + observation.getDate() + 
          observation.getTime()).build());
    
        observationMap.put("stationid", AttributeValue.builder()
          .n(Long.toString(observation.getStationid())).build());
    
        observationMap.put("date", AttributeValue.builder()
          .s(observation.getDate()).build());
    
        observationMap.put("time", AttributeValue.builder()
          .s(observation.getTime()).build());
    
        observationMap.put("image", AttributeValue.builder()
          .b(SdkBytes.fromUtf8String(observation.getImage())).build());
    
        if (observation.getTags() != null) {
          observationMap.put("tags", AttributeValue.builder()
            .ss(observation.getTags()).build());
        }
    
        PutItemRequest request = PutItemRequest.builder()
          .tableName("Observation").item(observationMap).build();
    
        this.dynamoDbClient.putItem(request);
      }
    }

AttributeValue

DynamoDB Java API 中有四种不同的 AttributeValue 类。在这里,我们使用 software.amazon.awssdk.services.dynamodb.model 包中的那个(API 文档)。请记住,表存储项目。一个项目由一个或多个属性组成。一个 AttributeValue 保存属性的值。AttributeValue 有一个构建器(API 文档),用于构建 AttributeValue 实例。属性值可以是字符串、数字、二进制数据、列表或集合。您使用与数据类型对应的适当方法来设置 AttributeValue 对象的值。例如,对于 String 使用 s(String value),二进制使用 b(SdkBytes b),对于 string 的集合,使用 ss(Collection ss)。有关完整列表,请参阅 API 文档。

AttributeValue 实例被放置在一个 Map 中,其中键是数据库表中的属性名称。Observation 的属性使用适当的构建器方法进行映射。

  • Observation.id 是一个 String,所以我们使用
    .s(observation.getStationid() + observation.getDate() + 
           observation.getTime()).build()
  • image,尽管已编码,是二进制的,因此我们使用
    observationMap.put("image", AttributeValue.builder()
          .b(SdkBytes.fromUtf8String(observation.getImage())).build());
  • tags 是一个可选的字符串列表,所以我们将其包装在一个条件中并使用
    if (observation.getTags() != null) {
      observationMap.put("tags", AttributeValue.builder()
         .ss(observation.getTags()).build());
    }

PutItemRequest

PutItemRequest 包装了发送到 DynamoDBClient putItem 方法的 JSON 请求。PutItemRequestBuilder 构建一个 PutItemRequest。上面,我们首先添加了表名,然后是要放置的项目。该项目是一个观测数据属性的键值映射。构建 PutItemRequest 实例后,DynamoDBClient 实例使用该请求将观测数据写入 DynamoDB Observation 表。

PutItemRequest request = PutItemRequest.builder().tableName("Observation")
   .item(observationMap).build();
this.dynamoDbClient.putItem(request);

有关更多信息,请参阅 API 文档

创建 Rest 端点

  • 使用 @Autowired 注解将 ObservationService 自动装配到 ObservationApiController
  • 修改 saveObservation 方法以调用服务的 writeObservation 方法。
    package com.tutorial.aws.dynamodb.api;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorial.aws.dynamodb.model.Observation;
    import com.tutorial.aws.dynamodb.service.ObservationService;
    
    @RestController
    @RequestMapping(value = "/observations")
    public class ObservationApiController {
      @Autowired
      ObservationService observationService;
    	
      @PostMapping("/observation")
      public void saveObservation(@RequestBody Observation observation) {
        this.observationService.writeObservation(observation);
      }	
    }
  • 构建应用程序并启动 Spring Boot 应用程序。

使用 Postman 进行测试

  • 返回到之前在 Postman 中创建的请求,然后单击 发送 按钮。
  • 转到 AWS 控制台中的表,然后单击“项目”。
  • 如果一切操作正确,您应该会在 Observation 表中看到两个项目。

    {
      "date": {
        "S": "1999-09-22"
      },
      "id": {
        "S": "2211999-09-22071244"
      },
      "image": {
        "B": "LzlqLzRBQ <snip> JRPT0="
      },
      "stationid": {
        "N": "221"
      },
      "tags": {
        "SS": [
          "observation",
          "river",
          "sample"
        ]
      },
      "time": {
        "S": "071244"
      }
    }

读取观测数据

现在我们已经将 Observation 写入 DynamoDB,接下来我们创建一个 Rest 端点来获取一个 Observation

修改服务类

  • 修改 ObservationService,使其具有一个 getObservation 方法,该方法将观测数据的 id 作为参数。
  • 创建一个 HashMap 来存储 AttributeValue 集合。这些用于查询数据库。
  • observationId 添加到 HashMap
  • 构建一个 GetItemRequest,将 HashMap 赋值为键。
  • 从响应中获取项目,并使用其值创建一个新的 Observation
  • 由于 tags 是一个项目列表,因此您必须遍历它们才能将它们添加到 Observation 对象实例中。
    public Observation getObservation(String observationId) {
      HashMap<String,AttributeValue> key = new 
        HashMap<String,AttributeValue>();
    
      key.put("id", AttributeValue.builder().s(observationId).build());
      GetItemRequest request = GetItemRequest.builder()
        .tableName("Observation").key(key).build();
      
      Map<String,AttributeValue> responseItem = 
      this.dynamoDbClient.getItem(request).item();
    
      Observation observation = new Observation();
      observation.setDate(responseItem.get("date").s());
      observation.setTime(responseItem.get("time").s());
      observation.setImage(responseItem.get("image").b().asUtf8String());
      observation.setStationid(Long.parseLong(responseItem
        .get("stationid").n()));
    		
      if(responseItem.get("tags") != null && responseItem.get("tags")
        .ss().size() > 0) {
    
        HashSet<String> vals = new HashSet<>();
        responseItem.get("tags").ss().stream().forEach(x->vals.add(x));
        observation.setTags(vals);
      }
      return observation;
    }

GetItemRequest

GetItemRequest 封装了一个发送到 DynamoDB 的 JSON Get 请求。要获取特定的 Observation,我们必须向 Get 请求提供 idkeyAttributeValue 项的 Map。在这种情况下,我们只添加了一个属性,即 id

GetItemRequest request = GetItemRequest.builder()
    .tableName("Observation").key(key).build();

创建 Rest 端点

  • 添加一个名为 getObservation 的方法,该方法将 observationid 作为路径变量。
  • 调用 ObservationServicegetObservation 方法并返回结果。
    @GetMapping("/observation/{observationid}")
    public Observation getObservation(@PathVariable("observationid") String 
      observationId) {
    
      return this.observationService.getObservation(observationId);
    
    }

使用 Postman 进行测试

  • 在 Postman 中创建一个新请求,URL 如下。
  • 添加之前添加的观测数据中的一个 id。
    https://:8080/observations/observation/2211992-03-12091312
  • 创建请求后,单击 发送,如果使用了 211992-03-12091312,则应显示以下响应。
    {
        "stationid": 221,
        "date": "1992-03-12",
        "time": "091312",
        "image": "/9j/4AA <snip> kf/9k=",
        "tags": [
            "rapids",
            "rocks",
            "observation",
            "cold"
        ]
    }

删除观测数据

到目前为止,我们已经向 DynamoDB 添加并获取了一个 Observation。现在我们来删除一个 Observation

修改服务类

  • 添加一个 deleteObservation 方法,该方法将观测数据的 id 作为参数。
  • 创建一个 HashMap 来保存属性。
  • 构建一个新的 DeleteItemRequest,并使用 HashMap 作为 key
  • 使用 dynamoDbClient 删除观测数据。
    public void deleteObservation(String observationId) {
      HashMap<String,AttributeValue> key = new HashMap<>();
      key.put("id", AttributeValue.builder().s(observationId).build());
      DeleteItemRequest deleteRequest = DeleteItemRequest.builder()
        .key(key).tableName("Observation").build();
      this.dynamoDbClient.deleteItem(deleteRequest);
    }

DeleteItemRequest

DeleteItemRequest 封装了一个 JSON Delete HTTP 请求。与所有请求一样,我们使用构建器。构建器使用表和键来删除 Observation

创建 Rest 端点

  • 创建一个新的 Rest 端点来删除观测数据。
  • 将观测数据的 id 作为路径变量传递给端点,并且只在变量后添加 /delete
  • 调用 ObservationServicedeleteObservation 方法。
    @DeleteMapping("/observation/{observationid}/delete")
    public void deleteObservation(@PathVariable("observationid") String 
      observationId) {
      this.observationService.deleteObservation(observationId);
    }

使用 Postman 进行测试

  • 使用 Postman 创建一个新 Request
  • 从下拉列表中选择 DELETE 以表示这是一个 Http Delete 请求。
    https://:8080/observations/observation/2211992-03-12091312/delete

  • 单击 发送,记录应该会被删除。导航到 AWS 控制台中的项目以确保 Observation 已删除。

更新观测数据

一个 Observation 可以有一个或多个 tags。这似乎很可能在以后添加和/或修改。让我们创建一个端点,允许添加/修改观测数据的标签。

更新服务类

  • 创建一个名为 updateObservationTags 的方法,该方法将标签列表和观测数据 id 作为参数。
  • 创建一个 HashMap 来保存 AttributeValue 对象。
  • 使用 AttributeBuilderValue 构建器将标签添加到 HashMap 中,并将 :tagval 作为键。
  • 创建第二个 HashMap 来保存 observation 的 ID。
  • 构建一个使用 update 表达式的 UpdateItemRequest
    public void updateObservationTags(List<String> tags, String observationId) {
      HashMap<String, AttributeValue> tagMap = new HashMap<String, 
        AttributeValue>();
    
      tagMap.put(":tagval", AttributeValue.builder().ss(tags).build());
      HashMap<String, AttributeValue> key = new HashMap<>();
      key.put("id", AttributeValue.builder().s(observationId).build());
    
      UpdateItemRequest request = UpdateItemRequest.builder()
        .tableName("Observation").key(key)
        .updateExpression("SET tags = :tagval")
        .expressionAttributeValues(tagMap).build();
    
      this.dynamoDbClient.updateItem(request);
    }

UpdateItemRequest

DynamoDBClient 实例使用 UpdateItemRequest 构建更新项目的请求。与获取和删除一样,它需要一个键来正确选择正确的项目。但它还需要更新的值。您提供一个更新表达式,然后提供属性。请注意,属性的键 :tagval 与表达式匹配。然后,请求使用键和 update 表达式来更新项目。

添加 Rest 端点

  • 添加一个端点,该端点将 observationid 作为路径变量,并将 JSON 标签数组作为请求正文。
  • 调用 ObservationServiceupdateObservationTags 方法
    @PostMapping("/observation/{observationid}/updatetags")
    public void updateObservationTags(@PathVariable("observationid") String 
      observationId, @RequestBody List<String> tags) {
      
      this.observationService.updateObservationTags(tags, observationId);
    
    }

使用 Postman 进行测试

  • 创建新的 Http Post 请求,URL 如下。
  • id 值直接作为路径参数添加到 URL 中。
    https://:8080/observations/observation/2211992-03-12091312/updatetags
  • 将以下值作为 Body 添加,并将其类型指定为 JSON (application/json)
    ["observation","rocks","rapids","cold"]

  • 点击 发送,然后导航到 AWS 控制台查看观测数据。Observation 应该已添加了标签。

批量写入观测数据

有时,需要将多个项目写入数据库。

更新服务类

public void batchWriteObservations(List<Observation> observations) {
  ArrayList<WriteRequest> requests = new ArrayList<>();
  HashMap<String, AttributeValue> observationMap = new HashMap<>();
  for(Observation observation : observations) {
    observationMap.put("id", AttributeValue.builder()
      .s(observation.getStationid() + observation.getDate() + 
      observation.getTime()).build());

    observationMap.put("stationid", AttributeValue.builder()
      .n(Long.toString(observation.getStationid())).build());

    observationMap.put("date", AttributeValue.builder()
      .s(observation.getDate()).build());

    observationMap.put("time", AttributeValue.builder()
      .s(observation.getTime()).build());

    observationMap.put("image", AttributeValue.builder()
      .b(SdkBytes.fromUtf8String(observation.getImage())).build());

    if (observation.getTags() != null) {
      observationMap.put("tags", AttributeValue.builder()
        .ss(observation.getTags()).build());
    }

    WriteRequest writeRequest = WriteRequest.builder()
      .putRequest(PutRequest.builder().item(observationMap)
      .build()).build();

    requests.add(writeRequest);
  }

  HashMap<String,List<WriteRequest>> batchRequests = new HashMap<>();
  batchRequests.put("Observation", requests);
  BatchWriteItemRequest request = BatchWriteItemRequest.builder()
    .requestItems(batchRequests).build();

  this.dynamoDbClient.batchWriteItem(request);
}

DynamoDbClient batchWriteItem 方法将 BatchWriteItemRequest 作为参数。BatchWriteItem 可以一次写入或删除最多 25 个项目,并且数据量限制为 16 MB。请注意,它仍然会进行与您拥有的项目一样多的调用;但是,它会并行进行这些调用。

您创建一个 List 来保存每个 ObservationWriteRequest。每个 Observation 都以键值对的形式写入 Map。该映射被添加到 WriteRequest 中,然后 WriteRequest 被添加到列表中,直到所有观测数据都准备好作为 WriteRequest 实例。

 WriteRequest writeRequest = WriteRequest.builder()
      .putRequest(PutRequest.builder().item(observationMap)
      .build()).build();

每个 WriteRequest 实例列表都被添加到另一个映射中。表名是键,列表是值。通过这种方式,一次批量写入可以写入多个表。创建 WriteRequest 实例列表的映射后,整个内容用于创建 BatchWriteItemRequest,然后由 DynamoDbClient batchWriteItem 方法使用。

 HashMap<String,List<WriteRequest>> batchRequests = new HashMap<>();
  batchRequests.put("Observation", requests);
  BatchWriteItemRequest request = BatchWriteItemRequest.builder()
    .requestItems(batchRequests).build();

创建 Rest 端点

  • 添加一个 Rest 端点,该端点将观测数据列表作为 JSON 放置在 request 正文中。
  • 调用 ObservationService 中的 batchWriteObservations 方法。
    @PostMapping("/observation/batch")
    public void batchSaveObservation(@RequestBody List<Observation> 
      observations) {
    
      this.observationService.batchWriteObservations(observations);
    
    }
  • 构建并运行应用程序。

使用 Postman 进行测试

  • 在 Postman 中创建一个新的 Post 请求,URL 如下。
    https://:8080/observations/observation/batch
  • 将以下内容添加到 Body 中,并将其类型指定为 JSON (application/json)
    [
        {
          "stationid": 221,
          "date": "2007-12-12",
          "time": "180000",
          "image": "/9j/4AAQSkZJRgABAQA <snip> kf/9k="
        },
        {
          "stationid": 221,
          "date": "2009-05-22",
          "time": "043455",
          "image": "/9j/4AAQSkZJRgABAQAA <snip> /8AD9KhoA//2Q=="
        },
        {
          "stationid": 234,
          "date": "2019-10-18",
          "time": "121459",
          "image": "/9j/4AAQSkZJRgABAQA <snip> VWoGf/9k="
        },
        {
          "stationid": 234,
          "date": "2017-09-22",
          "time": "093811",
          "image": "/9j/4AAQSkZJRgAB <snip> 5//2Q=="
        }
      ]

  • 点击 发送,然后导航到 AWS 控制台观测表中的项目,观测数据应该已添加。

有条件地获取观测数据

一个常见的需求是根据特定条件获取记录。例如,假设我们希望获取属于特定站点的所有观测数据。使用 DynamoDB 时,用于查询的任何变量都必须被索引。因此,在创建查询之前,我们首先在 Observation 表的 stationid 变量上创建索引。

创建索引

  • 导航到 AWS 控制台中的 Observation 表。
  • 点击 创建索引
  • 选择 stationid 作为索引的分区键,并确保将其定义为 Number 类型。
  • 点击 创建索引 以创建索引。

二级索引

二级索引允许使用除主键之外的属性从表中检索数据。您从索引而不是表中检索数据。有关 DynamoDB 二级索引的更多信息,请参阅 LinuxAcademy 的以下文章:DynamoDB 二级索引快速指南

更新服务类

public List<Observation> getObservationsForStation(String stationId){
  ArrayList<Observation> observations = new ArrayList<>();
  Condition condition = Condition.builder()
    .comparisonOperator(ComparisonOperator.EQ)
    .attributeValueList(AttributeValue.builder()
    .n(stationId).build()).build();

  Map<String, Condition> conditions = new HashMap<String, Condition>();
  conditions.put("stationid",condition);

  QueryRequest request = QueryRequest.builder().tableName("Observation")
    .indexName("stationid-index").keyConditions(conditions).build();

  List<Map<String, AttributeValue>> results = this.dynamoDbClient
    .query(request).items();
		
  for(Map<String,AttributeValue> responseItem: results) {
    Observation observation = new Observation();
    observation.setDate(responseItem.get("date").s());
    observation.setTime(responseItem.get("time").s());
    observation.setImage(responseItem.get("image").b().asUtf8String());
    observation.setStationid(Long.parseLong(
      responseItem.get("stationid").n()));
			
    if(responseItem.get("tags") != null && responseItem.get("tags").ss()
      .size() > 0) {
      HashSet<String> vals = new HashSet<>();
      responseItem.get("tags").ss().stream().forEach(x->vals.add(x));
      observation.setTags(vals);
    }
    observations.add(observation);
  }		
  return observations;		
}

首先,我们使用其关联的构建器创建了一个 Condition。条件是“=<传递给函数的站点 ID>”。

Condition condition = Condition.builder()
    .comparisonOperator(ComparisonOperator.EQ)
    .attributeValueList(AttributeValue.builder()
    .n(stationId).build()).build();

然后我们将 Condition 添加到一个映射中,并指定 stationid 为键,condition 为值。然后我们使用其关联的构建器构建了 QueryRequest

 QueryRequest request = QueryRequest.builder().tableName("Observation")
    .indexName("stationid-index").keyConditions(conditions).build();

创建 Rest 端点

  • 创建一个新的 Rest 端点,将 stationid 指定为 path 变量。
  • 调用 ObservationServicegetObservationsForStation 方法。
    @GetMapping("/station/{stationid}")
    public List<Observation> getObservations(@PathVariable("stationid") String 
      stationId) {
      return this.observationService.getObservationsForStation(stationId);
    }
  • 构建并运行应用程序。

使用 Postman 进行测试

  • 在 Postman 中创建一个新的 Get 请求,其中包含 URL 中的站点 ID。
  • 单击 发送,然后站点 221 的 Observation 项目应显示为响应正文。
    https://:8080/observations/station/221

    [
        {
            "stationid": 221,
            "date": "2009-05-22",
            "time": "043455",
            "image": "/9j/4AA <snip> 0/8AD9KhoA//2Q==",
            "tags": null
        },
        {
            "stationid": 221,
            "date": "2007-12-12",
            "time": "180000",
            "image": "/9j/4 <snip> /rn+q07/sHxfyNUK0Wxkf/9k=",
            "tags": null
        },
        {
            "stationid": 221,
            "date": "1992-03-12",
            "time": "091312",
            "image": "/9j/4AAQSkZJRgABAQAAYAB <snip> K0Wxkf/9k=",
            "tags": [
                "rapids",
                "rocks",
                "observation",
                "cold"
            ]
        },
        {
            "stationid": 221,
            "date": "1999-09-22",
            "time": "071244",
            "image": "/9j/4n0g27Qu <snip> A//2Q==",
            "tags": [
                "observation",
                "river",
                "sample"
            ]
        }
    ]

进一步的主题

本教程中未探讨几个主题。首先,您可以扫描数据库表。扫描表时,将返回表中的所有项目。其次,本教程未讨论有条件地更新或删除项目。但是,原理与有条件地查询表中的项目相同。此外,探索使用 DynamoDB 的命令行示例也很有帮助,因为它们有助于理解 SDK。最后,我们没有涵盖 Java 1.1 AWS SDK。

从 Java 1.1 AWS SDK 到 Java 2 AWS SDK

网上有许多使用 Java 1.1 API 而不是 Java 2 API 的示例和教程。但是,这两个版本的主要区别在于构建器模式。许多(如果不是大多数)Java 1.1 教程仍然有用。模式是相同的:

  • 创建一个请求类型
  • 使用所需参数设置请求
  • 将请求传递给 DynamoDB 客户端
  • 获取结果

在 Java 1.1 SDK 中,您使用构造函数、setter 和 getter 执行这些步骤。在 Java 2 SDK 中,您使用构建器。Java 2 AWS SDK 中的几乎所有类都使用构建器。如果您有一个特别好的使用 Java 1.1 SDK 的教程,请将其作为起点。虽然并非万无一失,但这样做帮助我将许多 Java 1.1 示例转换为 Java 2 SDK。

更多资源

结论

在本教程中,我们探讨了 Java 2 SDK 的低级 API。我们写入了一个项目,更新了一个项目,删除了一个项目,并批量上传了项目。我们还探讨了有条件地查询项目。

与所有 SDK 一样,它基于构建器、请求和客户端。您构建一个请求,将其传递给 DynamoDBClient,然后 DynamoDBClient 返回一个响应。您不会创建一个新的请求实例并通过 setter 设置属性,而是使用构建器来构建请求。

DynamoDB 是一个非关系型数据库,因此您不能在任何字段上直接编写条件查询。您只能在已建立索引的字段上使用查询。如果您考虑到 DynamoDB 是为大量相对非结构化数据设计的,这似乎是合乎逻辑的。

Github 项目

历史

  • 2019 年 5 月 19 日:初始版本
© . All rights reserved.