DjangoでDynamoDB、Sessionを使いたい

#概要
DjangoをSqliteを使わずにDynamoDBだけで実装したいときに役に立ちそうなことを記事にしました。
今回はDjangoからDynamORMを使ってDynamoDBを操作したり、session機能をDynamoDBで実装したりします。

# Djangoの設定

まず、以下のライブラリをインストールします。

```
pip install dynamorm
pip install marshmallow
pip install django-dynamodb-sessions 
```


今回はセッション情報もDynamoDBに保存します。`AWS_ACCESS_KEY_ID`などは`AWS`の`IAM`で取得してください。

```python:settings.py
#セッションを使うための設定
INSTALLED_APPS = [
    'django.contrib.sessions',
]
MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware',
]
SESSION_ENGINE = 'dynamodb_sessions.backends.dynamodb'

#セッションの有効期間などを設定
SESSION_COOKIE_AGE = 3600
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True

#AWSのアクセスIDとシークレットIDを追記
DYNAMODB_SESSIONS_AWS_ACCESS_KEY_ID = 'YOUR_AWS_ACCESS_KEY_ID'
DYNAMODB_SESSIONS_AWS_SECRET_ACCESS_KEY = 'YOUR_AWS_SECRET_ACCESS_KEY_ID'
DYNAMODB_SESSIONS_AWS_REGION_NAME = 'ap-northeast-1'
#DynamORMの設定
AWS_ACCESS_KEY_ID = 'YOUR_AWS_ACCESS_KEY_ID'
AWS_SECRET_ACCESS_KEY = 'YOUR_AWS_SECRET_ACCESS_KEY_ID'

```
#DynamoDBの操作



まずmodels.pyで以下のモジュールをインポートしてください。
今回は`DynamORM`を使用します。


```python:models.py
from dynamorm import DynaModel
from marshmallow import fields
```
モデルは以下のようにします。
nameに操作したいDynamoDBのテーブル名を、hash_keyにユニークなIDを入れます。

```python:models.py
class SampleTable(DynaModel):
    class Table:
        name = "SmapleTable"
        hash_key = 'SampleId'
        read = 25
        write = 5
    class Schema:
        SampleId = fields.String(required=True)
        name = fields.String()
        age = fields.Number()

```
ここまででDynamoDBを使う準備ができました。これからは実際に動かしていきます。


##操作


では実際にDynamoDBにデータを作成してみます。まず,DyanmoDBのページで`SampleTable`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/0cf2c5b1-e439-2379-738a-1d1819033026.png)

次にviews.pyに以下のコードを入れて実行してみます。

```python:views.py
from .models import SampleTable

def index(request):
    s = SampleTable(SampleId="sample1", name="neko", age="21")
    s.save()
    return HttpResponse("Hello, world. You're at the index.")
```
そしてindexを実行するとDynamoDBにデータが作成されているのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/d141d647-c47c-a6cb-2a96-fc55618d056e.png)

DynamORMで操作できる一部の例を紹介します。

```python:views.py
from .models import SampleModel

#データ作成
s = SampleModel(id="sample1", name="neko", age="21")
s.save()

#データ取得
s = SampleModel.get(id='sample1')

#データを更新
# partial=Trueを付けない場合他の属性も更新される
s = SampleModel.get(id='sample1')
s.name = "inu"
s.save(partial=True)

#以下の記述でも可能
s.update(name="inu")

```

DynamORMについて詳しく知りたい方は以下のサイトから

- [DynamORM](https://nerdwalletoss.github.io/dynamorm/index.html)


#Session

ここまでDynamORMでのデータベース操作を行ってきました。
ここからはsessionの機能をdynamodbで実装していきます。
まず、dynamoDBでセッション用のテーブル```sessions```を作成します。プライマリキーは`SessionId`に設定しました。



###Sessionの保存
`request.session.save()`がなくても保存されますがこちらのほうがより確実です。

```python:views.py
request.session['Id'] = SampleId
request.session.save()
```
これを実行すると以下のように```SessionId```と```data```が出てくると思います。
SessionIdにはsession_keyの値が入り、dataにはSampleIdの情報が入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/647f357a-a159-3760-9bea-b8c0867bb4b8.png)

###session_keyの取得
```python:views.py
 session_key = request.session.session_key
```

###データの取得

```python:views.py
 Id = request.session.get('Id')
```
###sessionを削除

```python:views.py
request.session.flush()
```
sessionに関する他の操作は以下のサイトが役立ちます。
- [【django】sessionの使い方（基本編）](https://idealive.jp/blog/2018/11/21/%E3%80%90django%E3%80%91session%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9%EF%BC%88%E5%9F%BA%E6%9C%AC%E7%B7%A8%EF%BC%89/)


###sessionの詳細設定

settings.pyにsessionの詳細設定を追記して反映します。
参考：
- [Django でログインセッションの有効期限を設定するのが意外と大変だった](https://qiita.com/3244/items/9eb03e733b986c20ee16)
- [django-dynamodb-sessions](https://pypi.org/project/django-dynamodb-sessions/)

| 設定 | 説明 |デフォルト値 |
|:-:|:-:|:-:|
|  DYNAMODB_SESSIONS_TABLE_NAME | DynamoDBのテーブル名。 |　sessions|
| DYNAMODB_SESSIONS_TABLE_HASH_ATTRIB_NAME  | sessionテーブルの属性名。  | session_key |
| SESSION_COOKIE_AGE  | sessionの有効期限を設定  | 2週間 |
| SESSION_EXPIRE_AT_BROWSER_CLOSE  | ブラウザを閉じたときにsessionを消す  | False |
|  SESSION_SAVE_EVERY_REQUEST  | requestが来るたびにsessionの有効期限をリセット | False |

##使わなくなったSessionを削除

sessionはログアウト時に実行される```request.session.flush()```によって削除されます。
また、```setting.py```に記述した設定によって期限が過ぎた場合などに`sessoin`は自動的に無効になります。しかし、ログアウトを実行せずにsessionが無効かされた場合は`DynamoDB`には残り続けます。
自分でいらなくなった`session`を`DynamoDB`から消してもいいですが、`DynamoDB`の`TTL`という機能を使えば自動的に削除できます。

###TTL

TTLというのはDynamoDBのデータを自動的に削除してくれる機能です。`DynamoDb`の`概要`から設定できます。
今回は後に示すサンプルコードで`sessions`に`ExpireTime`という属性を追加しました。
この`ExpireTime`を`TTL`に監視してもらい、期限を過ぎたら削除してもらいます。
注意点としては`TTL`が監視してくれる時間はUNIX時間でないといけません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/0209ccd9-7b53-70af-c229-37872cdd110a.png)



##サンプル
実際に動くサンプルコードを用意しました。
このサンプルコードでは`sessions`テーブルに`Expiretime`を追加して`TTL`を行えるようにしています。
また,htmlを作成していないため動かす際はURLから直接実行してください。

###urls.py
```python:urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('login', views.login, name='login'),
    path('logout', views.logout, name='logout'),
]
```

###models.py
```python:models.py
from django.db import models
from dynamorm import DynaModel
from marshmallow import fields

class Session(DynaModel):
    class Table:
        name = "sessions" #DyanmoDBのSessionを保存するテーブル名
        hash_key = 'SessionId'#プライマルキー
        read = 25
        write = 5
    class Schema:
        SessionId = fields.String(required=True)
        ExpireTime = fields.Decimal()
```

###views.py
```python:views.py
from django.shortcuts import render
from django.http import HttpResponse
from .models import Session
from datetime import datetime 
from decimal import Decimal
from dateutil.relativedelta import relativedelta

#User name=keita id=1 がいると仮定すると
def index(request):
    return HttpResponse("Hello, world. You're at the index.")

def login(request):
     if request.session.session_key:   
        Id = request.session.get('Id')
        return HttpResponse(f"You're already logined ID={Id}")
    else:
        #sessoinを発行
        request.session['Id'] = 1
        request.session.save()
        #sessionにExpireTime(１か月)を追加
        date_time = Decimal(int((datetime.now()+relativedelta(months=1)).timestamp()))
        s = Session.get(SessionId=request.session.session_key)
        s.update(ExpireTime=date_time)
        s.save(partial=True)
        return HttpResponse("You're Logging")

def logout(request):
    if request.session.session_key:
        request.session.flush()
        return HttpResponse("You're Logout")
    else:
        return HttpResponse("You're not Logging")

```


```python manage.py runserver```を実行すると、Hello, world. You're at the index.というページが出てきます。
###ログイン　 
今回はId=１のユーザーがログインすることを仮定して行います。 今回はパスワードなどはなしで実装しています。
`session`に`ExpireTime`という属性を追加していますが、これは後でsessionを自動的に削除するために必要なものです。
また、`request.session.keys()`で自分のsessionが存在するか確認できます。返り値は`dict_keys(['Id'])`のようになっていて、sessionに何が入っているかを確認できます。

```python:views.py
def login(request):
     if request.session.keys():   #ログインしていたら(sessionキーが存在していたら)
        Id = request.session.get('Id')
        return HttpResponse(f"You're already logined ID={Id}")
    else:#ログインしていなかったら
        #sessoinを発行
        request.session['Id'] = 1
        request.session.save()
        #sessionにExpireTime(１か月)を追加
        date_time = Decimal(int((datetime.now()+relativedelta(months=1)).timestamp()))
        s = Session.get(SessionId=request.session.session_key)
        s.update(ExpireTime=date_time)
        s.save(partial=True)
        return HttpResponse("You're Logging")
```


URLを```http://127.0.0.1:8000/login```としてクリックすると``` You're Logging ```と表示されるはずです。この時DynamoDbのsessionsテーブルを確認すると以下のようになっているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/12b8ff2c-a39f-d472-9d77-067a4b7af79f.png)

もう一度URLを```http://127.0.0.1:8000/login```を訪問すると、sessionが存在していれば```You're already logined ID=1```と出てきます。こうなっていれば、ログイン成功です！

###ログアウト

```python:views.py
def logout(request):
    #ログインしていればsessionを削除
    if request.session.keys(): 
        request.session.flush()
        return HttpResponse("You're Logout")
    else:
        return HttpResponse("You're not Logging")
```

では次は```http://127.0.0.1:8000/logout```を訪問してみます。すると```You're Logout```と出てくると思います。sessionが削除されてログアウトされている状態です。下の画像のようにDynamoDBのsessioinからデータが消えているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/972861/4e143ec5-fd07-7fc7-cb06-0dcab40101bd.png)

#まとめ
DjangoでDynamoDBを使いたいと思ったときに役に立ちそうなことを書きました。
sessionを使ってログイン機能等を作れると思うので参考になったらうれしいです
何か間違っているところがあったらコメントいただけるとありがたいです。

#参考
- [DynamORM](https://nerdwalletoss.github.io/dynamorm/index.html)
- [Django でログインセッションの有効期限を設定するのが意外と大変だった](https://qiita.com/3244/items/9eb03e733b986c20ee16)
- [Django ドキュメント](https://djangoproject-jp.vercel.app/doc/ja/1.0/topics/http/sessions.html)
- [【django】sessionの使い方（基本編）](https://idealive.jp/blog/2018/11/21/%E3%80%90django%E3%80%91session%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9%EF%BC%88%E5%9F%BA%E6%9C%AC%E7%B7%A8%EF%BC%89/)
- [How I use DynamoDB with Django](https://www.youtube.com/watch?v=pfPLYuWDlvM)