Налаштування DynamoDB для веб-додатку
DynamoDB — керована NoSQL база від AWS з гарантованою затримкою в мілісекунди при будь-якому масштабі. Немає серверів для обслуговування, нема ручного шардування, немає проблем із лагом реплік. Ідеально для serverless-архітектур і додатків із непередбачуваними піками навантаження.
Ключові концепції перед проектуванням
DynamoDB — не реляційна база і не MongoDB. Немає JOIN, немає агрегатних запитів без повного скану, немає гнучких фільтрів. Усі запити будуються навколо Partition Key (PK) та Sort Key (SK). Single-table design — стандартний підхід: одна таблиця для всіх сутностей додатку з перевантаженими PK/SK.
Створення таблиці через AWS CDK (TypeScript)
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
import { RemovalPolicy } from 'aws-cdk-lib'
const table = new dynamodb.Table(this, 'AppTable', {
tableName: 'MyApp',
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // або PROVISIONED + auto scaling
pointInTimeRecovery: true,
deletionProtection: true,
removalPolicy: RemovalPolicy.RETAIN,
// Streams для реакції на зміни
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
})
// GSI для пошуку за email
table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
})
Single-table design: приклад схеми
Сутність: User
PK: USER#<userId> SK: METADATA
GSI1PK: EMAIL#<email> GSI1SK: USER#<userId>
Сутність: Order
PK: USER#<userId> SK: ORDER#<orderId>
GSI1PK: ORDER#<orderId> GSI1SK: USER#<userId>
Сутність: OrderItem
PK: ORDER#<orderId> SK: ITEM#<itemId>
Сутність: Product
PK: PRODUCT#<productId> SK: METADATA
GSI1PK: CATEGORY#<cat> GSI1SK: PRODUCT#<productId>
DynamoDB клієнт (AWS SDK v3)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
QueryCommand,
UpdateCommand,
DeleteCommand,
TransactWriteCommand
} from '@aws-sdk/lib-dynamodb'
const client = new DynamoDBClient({ region: process.env.AWS_REGION })
const db = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true }
})
const TABLE = 'MyApp'
Репозиторій користувачів
export class UserRepository {
async create(user: CreateUserInput): Promise<User> {
const id = crypto.randomUUID()
const now = new Date().toISOString()
await db.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName: TABLE,
Item: {
PK: `USER#${id}`,
SK: 'METADATA',
GSI1PK: `EMAIL#${user.email}`,
GSI1SK: `USER#${id}`,
id,
email: user.email,
name: user.name,
passwordHash: user.passwordHash,
role: 'user',
createdAt: now,
updatedAt: now,
_type: 'User'
},
ConditionExpression: 'attribute_not_exists(PK)'
}
}
]
}))
return { id, ...user, role: 'user', createdAt: now, updatedAt: now }
}
async findByEmail(email: string): Promise<User | null> {
const result = await db.send(new QueryCommand({
TableName: TABLE,
IndexName: 'GSI1',
KeyConditionExpression: 'GSI1PK = :pk',
ExpressionAttributeValues: { ':pk': `EMAIL#${email}` },
Limit: 1
}))
return (result.Items?.[0] as User) ?? null
}
async findById(id: string): Promise<User | null> {
const result = await db.send(new GetCommand({
TableName: TABLE,
Key: { PK: `USER#${id}`, SK: 'METADATA' }
}))
return (result.Item as User) ?? null
}
}
Замовлення з розбиванням на сторінки
export class OrderRepository {
async listForUser(userId: string, limit = 25, lastKey?: Record<string, unknown>) {
const result = await db.send(new QueryCommand({
TableName: TABLE,
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':prefix': 'ORDER#'
},
ScanIndexForward: false, // новіші спочатку
Limit: limit,
ExclusiveStartKey: lastKey
}))
return {
items: result.Items as Order[],
nextKey: result.LastEvaluatedKey
}
}
async createWithItems(order: CreateOrderInput): Promise<Order> {
const orderId = crypto.randomUUID()
const now = new Date().toISOString()
const items: TransactWriteItem[] = [
{
Put: {
TableName: TABLE,
Item: {
PK: `USER#${order.userId}`,
SK: `ORDER#${now}#${orderId}`, // now в SK для сортування за датою
GSI1PK: `ORDER#${orderId}`,
GSI1SK: `USER#${order.userId}`,
id: orderId,
userId: order.userId,
status: 'pending',
total: order.total,
createdAt: now,
_type: 'Order'
}
}
},
...order.items.map(item => ({
Put: {
TableName: TABLE,
Item: {
PK: `ORDER#${orderId}`,
SK: `ITEM#${item.productId}`,
orderId,
productId: item.productId,
quantity: item.quantity,
price: item.price,
_type: 'OrderItem'
}
}
}))
]
await db.send(new TransactWriteCommand({ TransactItems: items }))
return { id: orderId, ...order, status: 'pending', createdAt: now }
}
}
Налаштування DynamoDB Streams + Lambda
// Обробка змін у реальному часі
export const handler = async (event: DynamoDBStreamEvent) => {
for (const record of event.Records) {
if (record.eventName !== 'MODIFY') continue
const newImage = unmarshall(record.dynamodb!.NewImage!)
const oldImage = unmarshall(record.dynamodb!.OldImage!)
if (newImage._type === 'Order' && newImage.status !== oldImage.status) {
await notifyOrderStatusChange(newImage.id, newImage.status)
}
}
}
Моніторинг через CloudWatch
Ключові метрики: ConsumedReadCapacityUnits, ConsumedWriteCapacityUnits, SuccessfulRequestLatency, SystemErrors, ThrottledRequests. На ThrottledRequests > 0 — негайний алерт.
Терміни
Проектування single-table схеми + базова інтеграція з Node.js/Python: 3–5 днів. Додавання GSI, Streams, Lambda-обробників та моніторингу: ще 3–5 днів. Міграція з реляційної бази в DynamoDB з переглядом шаблонів доступу: 2–4 тижні — це не механічна конвертація, а переосмислення моделі даних.







