在Golang中一个简单的AWS DynamoDB CRUD例子

478 阅读3分钟

这是一个用Golang编写的简单的AWS DynamoDB CRUD例子。请记住,有些文件需要改进。有一些硬编码的部分,重复的部分等等。我必须让这个帖子尽可能的简短。请随意重构它。

结构

├── internal
│   ├── domain
│   │   └── error.go
│   ├── pkg
│   │   └── storage
│   │       ├── aws
│   │       │   ├── aws.go
│   │       │   └── user_storage.go
│   │       └── user_storer.go
│   └── user
│       ├── controller.go
│       └── models.go
├── main.go
└── migrations
    └── users.json

前提条件

首先,你需要创建一个users 表,以uuid 字段作为分区键。

$ aws --profile localstack --endpoint-url http://localhost:4566 dynamodb create-table --cli-input-json file://migrations/users.json
# migrations/users.json

{
  "TableName": "users",
  "AttributeDefinitions": [
    {
      "AttributeName": "uuid",
      "AttributeType": "S"
    }
  ],
  "KeySchema": [
    {
      "AttributeName": "uuid",
      "KeyType": "HASH"
    }
  ],
  "ProvisionedThroughput": {
    "ReadCapacityUnits": 5,
    "WriteCapacityUnits": 5
  }
}

文件

main.go

不要在意端点,因为不应该有这样的后缀!

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/you/aws/internal/pkg/storage/aws"
	"github.com/you/aws/internal/user"
)

func main() {
	// Create a session instance.
	ses, err := aws.New(aws.Config{
		Address: "http://localhost:4566",
		Region:  "eu-west-1",
		Profile: "localstack",
		ID:      "test",
		Secret:  "test",
	})
	if err != nil {
		log.Fatalln(err)
	}

	// Instantiate HTTP app
	usr := user.Controller{
		Storage: aws.NewUserStorage(ses, time.Second*5),
	}

	// Instantiate HTTP router
	rtr := http.NewServeMux()
	rtr.HandleFunc("/api/v1/users/create", usr.Create)
	rtr.HandleFunc("/api/v1/users/find", usr.Find)
	rtr.HandleFunc("/api/v1/users/delete", usr.Delete)
	rtr.HandleFunc("/api/v1/users/update", usr.Update)

	// Start HTTP server
	log.Fatalln(http.ListenAndServe(":8080", rtr))
}

controller.go

package user

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/you/aws/internal/domain"
	"github.com/you/aws/internal/pkg/storage"

	"github.com/google/uuid"
)

type Controller struct {
	Storage storage.UserStorer
}

// POST /api/v1/users/create
func (c Controller) Create(w http.ResponseWriter, r *http.Request) {
	var req User
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	id := uuid.New().String()

	err := c.Storage.Insert(r.Context(), storage.User{
		UUID:      id,
		Name:      req.Name,
		Level:     req.Level,
		IsBlocked: req.IsBlocked,
		CreatedAt: req.CreatedAt,
		Roles:     req.Roles,
	})
	if err != nil {
		switch err {
		case domain.ErrConflict:
			w.WriteHeader(http.StatusConflict)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusCreated)
	_, _ = w.Write([]byte(id))
}

// GET /api/v1/users/find?id={UUID}
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
	res, err := c.Storage.Find(r.Context(), r.URL.Query().Get("id"))
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusNotFound)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	user := User{
		UUID:      res.UUID,
		Name:      res.Name,
		Level:     res.Level,
		IsBlocked: res.IsBlocked,
		CreatedAt: res.CreatedAt,
		Roles:     res.Roles,
	}

	data, err := json.Marshal(user)
	if err != nil {
		log.Println(err)

		w.WriteHeader(http.StatusInternalServerError)
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_, _ = w.Write(data)
}

// DELETE /api/v1/users/delete?id={UUID}
func (c Controller) Delete(w http.ResponseWriter, r *http.Request) {
	err := c.Storage.Delete(r.Context(), r.URL.Query().Get("id"))
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusNotFound)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// PATCH /api/v1/users/update?id={UUID}
func (c Controller) Update(w http.ResponseWriter, r *http.Request) {
	var req User
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	err := c.Storage.Update(r.Context(), storage.User{
		UUID:  r.URL.Query().Get("id"),
		Name:  req.Name,
		Level: req.Level,
		Roles: req.Roles,
	})
	if err != nil {
		switch err {
		case domain.ErrNotFound:
			w.WriteHeader(http.StatusConflict)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

models.go

package user

import "time"

type User struct {
	UUID      string    `json:"uuid"`
	Name      string    `json:"name"`
	Level     int       `json:"level"`
	IsBlocked bool      `json:"is_blocked"`
	CreatedAt time.Time `json:"created_at"`
	Roles     []string  `json:"roles"`
}

error.go

package domain

import "errors"

var (
	ErrInternal = errors.New("internal")
	ErrNotFound = errors.New("not found")
	ErrConflict = errors.New("conflict")
)

user_storer.go

package storage

import (
	"context"
	"time"
)

type User struct {
	UUID      string    `json:"uuid"`
	Name      string    `json:"name"`
	Level     int       `json:"level"`
	IsBlocked bool      `json:"is_blocked"`
	CreatedAt time.Time `json:"created_at"`
	Roles     []string  `json:"roles"`
}

type UserStorer interface {
	Insert(ctx context.Context, user User) error
	Find(ctx context.Context, uuid string) (User, error)
	Delete(ctx context.Context, uuid string) error
	Update(ctx context.Context, user User) error
}

user_storage.go

package aws

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/you/aws/internal/domain"
	"github.com/you/aws/internal/pkg/storage"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

var _ storage.UserStorer = UserStorage{}

type UserStorage struct {
	timeout time.Duration
	client  *dynamodb.DynamoDB
}

func NewUserStorage(session *session.Session, timeout time.Duration) UserStorage {
	return UserStorage{
		timeout: timeout,
		client:  dynamodb.New(session),
	}
}

func (u UserStorage) Insert(ctx context.Context, user storage.User) error {
	ctx, cancel := context.WithTimeout(ctx, u.timeout)
	defer cancel()

	item, err := dynamodbattribute.MarshalMap(user)
	if err != nil {
		log.Println(err)
		return domain.ErrInternal
	}

	input := &dynamodb.PutItemInput{
		TableName: aws.String("users"),
		Item:      item,
		ExpressionAttributeNames: map[string]*string{
			"#uuid": aws.String("uuid"),
		},
		ConditionExpression: aws.String("attribute_not_exists(#uuid)"),
	}

	if _, err := u.client.PutItemWithContext(ctx, input); err != nil {
		log.Println(err)

		if _, ok := err.(*dynamodb.ConditionalCheckFailedException); ok {
			return domain.ErrConflict
		}

		return domain.ErrInternal
	}

	return nil
}

func (u UserStorage) Find(ctx context.Context, uuid string) (storage.User, error) {
	ctx, cancel := context.WithTimeout(ctx, u.timeout)
	defer cancel()

	input := &dynamodb.GetItemInput{
		TableName: aws.String("users"),
		Key: map[string]*dynamodb.AttributeValue{
			"uuid": {S: aws.String(uuid)},
		},
	}

	res, err := u.client.GetItemWithContext(ctx, input)
	if err != nil {
		log.Println(err)

		return storage.User{}, domain.ErrInternal
	}

	if res.Item == nil {
		return storage.User{}, domain.ErrNotFound
	}

	var user storage.User
	if err := dynamodbattribute.UnmarshalMap(res.Item, &user); err != nil {
		log.Println(err)

		return storage.User{}, domain.ErrInternal
	}

	return user, nil
}

func (u UserStorage) Delete(ctx context.Context, uuid string) error {
	ctx, cancel := context.WithTimeout(ctx, u.timeout)
	defer cancel()

	input := &dynamodb.DeleteItemInput{
		TableName: aws.String("users"),
		Key: map[string]*dynamodb.AttributeValue{
			"uuid": {S: aws.String(uuid)},
		},
	}

	if _, err := u.client.DeleteItemWithContext(ctx, input); err != nil {
		log.Println(err)

		return domain.ErrInternal
	}

	return nil
}

func (u UserStorage) Update(ctx context.Context, user storage.User) error {
	ctx, cancel := context.WithTimeout(ctx, u.timeout)
	defer cancel()

	roles := make([]*dynamodb.AttributeValue, len(user.Roles))
	for i, role := range user.Roles {
		roles[i] = &dynamodb.AttributeValue{S: aws.String(role)}
	}

	input := &dynamodb.UpdateItemInput{
		TableName: aws.String("users"),
		Key: map[string]*dynamodb.AttributeValue{
			"uuid": {S: aws.String(user.UUID)},
		},
		ExpressionAttributeNames: map[string]*string{
			"#name":  aws.String("name"),
			"#level": aws.String("level"),
			"#roles": aws.String("roles"),
		},
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":name":  {S: aws.String(user.Name)},
			":level": {N: aws.String(fmt.Sprint(user.Level))},
			":roles": {L: roles},
		},
		UpdateExpression: aws.String("set #name = :name, #level = :level, #roles = :roles"),
		ReturnValues:     aws.String("UPDATED_NEW"),
	}

	if _, err := u.client.UpdateItemWithContext(ctx, input); err != nil {
		log.Println(err)

		return domain.ErrInternal
	}

	return nil
}

aws.go

package aws

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
)

type Config struct {
	Address string
	Region  string
	Profile string
	ID      string
	Secret  string
}

func New(config Config) (*session.Session, error) {
	return session.NewSessionWithOptions(
		session.Options{
			Config: aws.Config{
				Credentials:      credentials.NewStaticCredentials(config.ID, config.Secret, ""),
				Region:           aws.String(config.Region),
				Endpoint:         aws.String(config.Address),
				S3ForcePathStyle: aws.Bool(true),
			},
			Profile: config.Profile,
		},
	)
}

测试

# Create

curl --location --request POST 'http://localhost:8080/api/v1/users/create' \
--data-raw '{
    "name": "inanzzz",
    "level": 3,
    "is_blocked": false,
    "created_at": "2020-01-31T23:59:00Z",
    "roles": ["accounts", "admin"]
}'
# Find

curl --location --request GET 'http://localhost:8080/api/v1/users/find?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'
# Delete

curl --location --request DELETE 'http://localhost:8080/api/v1/users/delete?id=80638f40-d248-49be-90ce-88d5b1b4ecd4'
# Update

curl --location --request PATCH 'http://localhost:8080/api/v1/users/update?id=347ac592-b024-4001-9d1b-925abe10c236' \
--data-raw '{
    "name": "inanzzz",
    "level": 1,
    "roles": ["accounts", "admin"]
}'