こんにちは!
株式会社iimonでエンジニアをしている遠藤です。
NoSQLデータベースってどんなタイプのものがあって、それぞれどういう特徴があるのか、概念的なことは何となくわかってきたような気がする(?)けど、実際にNoSQLのデータベースを使用して開発するイメージ(DBとSQLを使わずにやりとりするイメージ)があまり沸かないな…と思ったので、
今回はDynamoDB Localでローカルにデータベースを立てて、Djangoでデータをやり取りする簡単なAPIを生成することで、少しでもイメージを掴んでみよう!という記事になります。
この記事でやることは以下になります。
記述に間違っているところがあればご指摘いただけますと幸いです。
■前提
- Docker Composeをインストール済みであること
■使用するツールについて
-
AWSが公開しているもので、ローカル環境で動作することができるDynamoDB互換のデータベースです。
ただ、WebサービスのDynamoDBとはいくつか異なる点があるので注意が必要です。(ここでは詳細は省略します。) docs.aws.amazon.com
ローカルで動かすため料金がかからないのと、今回はとりあえずローカルで簡単に試してみたいという気持ちなので、こちらを使用します。
javaまたはDocker上で動作するので、今回はDockerで起動します。
-
DynamoDBを使用する際に、GUIでテーブルを管理することができるツールです。
Django AdminはデフォルトでRDBとの連携をサポートしていますが、今回はRDBではないため、こちらを使用します。
■ローカル環境の構築手順
1. プロジェクト用の空のディレクトリを生成
mkdir django-dynamodb-sample
2. プロジェクトディレクトリに移動
cd django-dynamodb-sample
3. requirements.txtを作成
Djangoとboto3をインストールするため、requirements.txt
ファイルを作成します。
DjangoでAPIを作成し、DynamoDBとのデータの操作にboto3を使用します。
echo "Django==5.0" >> requirements.txt
echo "boto3==1.34.153" >> requirements.txt
4. Dockerfileの生成
cat <<EOF > Dockerfile FROM python:3.12 ENV PYTHONUNBUFFERED 1 RUN mkdir /src WORKDIR /src COPY requirements.txt /src/ RUN pip install -r requirements.txt COPY . /src/ EOF
RUN pip install -r requirements.txt
で、先ほどrequirements.txtに記述した必要なソフトフェアをインストールします。
5. docker-compose.ymlを作成
DjangoアプリケーションとDynamoDB Localとdynamodb-adminを同時に実行するため、各サービスと、サービスが利用するDockerイメージを記述します。
cat <<EOF > docker-compose.yml services: api: container_name: "api" build: . depends_on: - dynamodb-local volumes: - .:/src working_dir: /src command: python3 manage.py runserver 0.0.0.0:8000 ports: - "9000:8000" dynamodb-local: container_name: dynamodb-local command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" image: "amazon/dynamodb-local:latest" ports: - 8000:8000 volumes: - "./docker/dynamodb:/home/dynamodblocal/data" working_dir: /home/dynamodblocal dynamodb-admin: container_name: dynamodb-admin image: aaronshaf/dynamodb-admin:latest environment: - DYNAMO_ENDPOINT=dynamodb-local:8000 ports: - 8001:8001 depends_on: - dynamodb-local EOF
6. Djangoプロジェクトの作成
Composeに対し、コンテナ内においてdjango-admin startproject src
を実行します。
docker-compose run api django-admin startproject src .
7. DynamoDBLocalの設定をsrc/settings.pyに追加
今回はローカルで動かして触りたいだけなので、諸々環境変数に入れずにベタ書きしています。
DynamoDBにアクセスするため、boto3でリソースを取得します。
import boto3 DYNAMODB_LOCAL = True if DYNAMODB_LOCAL: DYNAMODB_ENDPOINT_URL = 'http://dynamodb-local:8000' else: DYNAMODB_ENDPOINT_URL = None session = boto3.Session( aws_access_key_id='dummyAccessKey', aws_secret_access_key='dummySecretKey', region_name='ap-northeast-1' ) dynamodb = session.resource( 'dynamodb', endpoint_url=DYNAMODB_ENDPOINT_URL )
8. Dockerを起動
docker-compose up -d --build
9. アプリケーションの作成
apiコンテナに入る
docker-compose exec api bash
apiコンテナ内で以下のコマンドを実行します。appはアプリケーション名となります。
python manage.py startapp app
src/settings.pyのINSTALLED_APPS
に作成したアプリケーションを追加します
INSTALLED_APPS = [ 'django.contrib.admin', ... 'app', # 追加 ]
http://localhost:8001/にアクセスして以下のような画面になっていればOKです。
http://localhost:9000/adminにもアクセスできるかと思います。
(ただ、今回はDjango Adminは使用しないので、後ほどエンドポイントを削除します。)
■実際に触ってみる
ローカルで触れる環境が整いました! 実際に簡単なAPIを構築してデータを操作してみます。
1. テーブルの作成
今回はコマンドを実行して、注文を管理するためのテーブルを作成できるようにします。
- TableName: Order
- Hash Attribute (パーティションキー): id(string)
フォルダの作成
mkdir -p app/management/commands
ファイルを作成し、コマンドを追加
cat <<EOF > app/management/commands/create_order_tables.py from django.core.management.base import BaseCommand from src.settings import dynamodb class Command(BaseCommand): help = 'Create Order tables' def handle(self, *args, **options): table_name = 'Order' try: existing_tables = dynamodb.meta.client.list_tables().get('TableNames', []) if table_name in existing_tables: self.stdout.write(self.style.SUCCESS(f'Table {table_name} already exists.')) return table = dynamodb.create_table( TableName=table_name, KeySchema=[ {'AttributeName': 'id', 'KeyType': 'HASH'}, ], AttributeDefinitions=[ {'AttributeName': 'id', 'AttributeType': 'S'}, ], ProvisionedThroughput={ 'ReadCapacityUnits': 3, 'WriteCapacityUnits': 3 } ) table.meta.client.get_waiter('table_exists').wait(TableName=table_name) self.stdout.write(self.style.SUCCESS('Successfully created table')) except Exception as e: self.stdout.write(self.style.ERROR(f'Error creating table: {e}')) EOF
apiコンテナ内でコマンドを実行
python manage.py create_order_tables
http://localhost:8001/にアクセスするとテーブルが追加されていることを確認できるかと思います。
コマンドの内容について少し触れます
KeySchema:
テーブルのプライマリキーを構成する属性、またはインデックスのキー属性を指定します。パーティションキー(必須):
「ハッシュ属性」とも呼ばれます。DynamoDBは内部のハッシュ関数を使用して、パーティションキーの値に基づいて、パーティション間のデータ項目を均等に分散します。ソートキー(任意):
「範囲属性」とも呼ばれます。DynamoDBは、ソートキー値で並べ替えられた順に同じパーティションキーを持つ項目同士を物理的に近くに保存します。
ソートキーを設定しない場合は、パーティションキーがプライマリキー、
ソートキーを設定する場合は、 パーティションキーとソートキーを複合したものがプライマリキーになります。
今回はid
をパーティションキーに指定しており、ソートキーは指定していないため、id
がプライマリキーになります。DynamoDBでは、アイテムごとに異なる属性を追加することができますが、パーティションキーやソートキーに設定している属性は必ず含めなければいけません。
AttributeDefinitions:
KeySchemaプロパティの属性を定義する必要があります。 属性名と属性の型を指定します。ProvisionedThroughput:
料金形態の設定になります。- オンデマンドキャパシティモード:
テーブルに対して実際のリクエスト数に対して料金が発生する。 プロビジョンキャパシティモード:
テーブルに対して必要なスループットをキャパシティユニットとしてあらかじめ割り当てておく。割り当てた分の料金を支払う。- ReadCapacityUnits: 読み込みに対するキャパシティ
- WriteCapacityUnits: 書き込みに対するキャパシティ
どちらの形態を利用するかは
BillingMode
で指定します。デフォルトはプロビジョンキャパシティモードです。
DynamoDB Localで実行される場合にはこちらの値は使用されません。- オンデマンドキャパシティモード:
2. ビューの定義
app/view.pyを以下のコードに置き換えます。
今回はローカル環境を構築して、実際に触ってみたいだけなので、csrf_exempt
デコレーターをインポートして、CSRFチェックを無効にしています。
DjangoのORMは使えないので、boto3で取得したリソースを使用して、データの操作を行います。
from django.http import JsonResponse, HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from src.settings import dynamodb import json @method_decorator(csrf_exempt, name='dispatch') class OrderList(View): # テーブルの取得 table = dynamodb.Table('Order') def get(self, request): try: response = self.table.scan() items = response.get('Items', []) return JsonResponse(items, safe=False) except Exception: return HttpResponse(status=400) def post(self, request): try: data = json.loads(request.body) self.table.put_item(Item=data) return JsonResponse(data, status=201) except Exception: return HttpResponse(status=400) @method_decorator(csrf_exempt, name='dispatch') class OrderDetail(View): # テーブルの取得 table = dynamodb.Table('Order') def get(self, request, id): try: # Keyにはプライマリキーを指定 response = self.table.get_item(Key={'id': id}) item = response.get('Item') if item: return JsonResponse(item) return HttpResponse(status=404) except Exception: return HttpResponse(status=400) def delete(self, request, id): try: # Keyにはプライマリキーを指定 self.table.delete_item(Key={'id': id}) return HttpResponse(status=204) except Exception: return HttpResponse(status=400)
3. URLの設定
- app/urls.py
ファイルを生成
touch app/urls.py
ファイルに追記
from django.urls import path from .views import OrderList, OrderDetail urlpatterns = [ path('order-list/', OrderList.as_view(), name='order-list'), path('order-detail/<str:id>/', OrderDetail.as_view(), name='order-detail'), ]
- src/urls.py
以下に書き換える
from django.urls import path, include urlpatterns = [ path('app/', include('app.urls')), ]
4. 動作確認
- 注文の登録
一覧の全取得と指定したキーの値の取得を比較するために、2つデータを入れておきます。
curl -X POST http://localhost:9000/app/order-list/ -H "Content-Type: application/json" -d '{"id": "1", "item": "coffee", "quantity": 3}'
curl -X POST http://localhost:9000/app/order-list/ -H "Content-Type: application/json" -d '{"id": "2", "item": "greenTea", "quantity": 2}'
- 注文一覧の取得
curl -X GET http://localhost:9000/app/order-list/
正しく動いていれば、先ほど登録したデータが全て取得できます。
[ { "item": "coffee", "id": "1", "quantity": "3", }, { "item": "greenTea", "id": "2", "quantity": "2", }, ]%
- 指定した注文の取得
id1の注文を取得します。
curl -X GET http://localhost:9000/app/order-detail/1/
以下のようなレスポンスが返ってくるかと思います。
{"item": "coffee", "id": "1", "quantity": "3"}%
- 指定した注文の削除
id1の注文を削除します
curl -X DELETE http://localhost:9000/app/order-detail/1/
http://localhost:8001/tables/Orderを見るとid1の注文が削除されています。
■まとめ
とりあえず簡単なcrud処理を実装することができました。
頭の中がリレーショナルリレーショナルしていたので、DjangoのORM使わずにどうやって操作するんだろう?とかなど色々思っていたのですが、DynamoDBはAWSのサービスだからboto3使えばいいのかーと腑に落ちました。 少しは実装のイメージが湧いたような気がしています。
今回はとりあえず触ってデータをやり取りするイメージを掴んでみようという回だったので、次回はもう少し複雑なデータの扱いを試したり、DynamoDB自体についてももっと詳しく調べてみたいと思います!
最後まで読んでくださりありがとうございます!
この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いいたします!!