[TIL] Django REST framework の学習メモ
チュートリアルをやっていくが、そのままコピペしてもアレなので、ちょっと変えてやっていく。
適当にプロジェクトとアプリを作る。
django-admin startproject backend
cd backend/
python manage.py startapp monitoring
お決まり通りに INSTALLED_APPS
を更新する。
DATABASES
は SQLite を使うのでそのまま。
TIME_ZONE
は Asia/Tokyo
に変更。
models.py
にモデルを追加していくと肥大化するので分割する。
こんな感じでモデルを定義した。
class Sensor(models.Model):
serial_number = models.CharField(
verbose_name="S/N",
primary_key=True,
max_length=64,
blank=False,
null=False,
validators=[validators.RegexValidator(
regex='^[0-9a-zA-Z-]+$',
message='numeric, alphabet or hylien'
)]
)
display_name = models.CharField(
verbose_name="Display Name",
max_length=128,
blank=True,
null=False
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.serial_number
管理画面から編集できるように admin.py
に追加する。
from django.contrib import admin
from monitoring.models import Sensor
admin.site.register(Sensor)
実行してみる。
python manage.py makemigrations monitoring
python manage.py migrate
python manage.py runserver
これで、管理顔面からデータを CURD できるようになった。
同様に、こんな感じでモデルを追加。
class SensingData(models.Model):
sensor = models.ForeignKey(
Sensor,
to_field='serial_number',
on_delete=models.CASCADE
)
timestamp = models.DateTimeField(default=datetime.now)
tempareture = models.FloatField()
humidity = models.FloatField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.sensor}_{self.timestamp.isoformat()}'
シリアライザはこんな感じ。チュートリアルを読むと、実際にどんな処理が行われているのかがよくわかる。
class SensorSerializer(serializers.ModelSerializer):
class Meta:
model = Sensor
fields = ['serial_number', 'display_name', 'created_at', 'updated_at']
class SensingDataSerializer(serializers.ModelSerializer):
class Meta:
model = SensingData
fields = ['sensor', 'timestamp',
'tempareture', 'humidity', 'created_at']
View を作る。 /sensor
および /sensor/{pk}
に対して CRUD できるようにするには、以下のようなコードになる。
@csrf_exempt
def sensor_list(request):
if request.method == 'GET':
sensor = Sensor.objects.all()
serializeer = SensorSerializer(sensor, many=True)
return JsonResponse(serializeer.data, safe=False)
elif request.method == 'POST':
data = JSONParser().parse(request)
serializer = SensorSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
@csrf_exempt
def sensor_detail(request, pk):
try:
sensor = Sensor.objects.get(pk=pk)
except:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SensorSerializer(sensor)
return JsonResponse(serializer.data)
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SensorSerializer(sensor, data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data)
return JsonResponse(serializer.erros, status=400)
elif request.method == 'DELETE':
sensor.delete()
return HttpResponse(status=204)
urls.py
を設定。プライマリーキーを数値ではなく文字列にしたので、 int
ではなく slug
になる。
urlpatterns = [
path('sensor', views.sensor_list),
path('sensor/<slug:pk>', views.sensor_detail)
]
プロジェクトの方の urls.py
も更新する。
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('monitoring.urls')),
]
CURD してみる。
import requests
import json
import pprint
url = "http://localhost:8000/sensor/"
serial_number = 'BMTL-123456'
display_name1 = 'テスト機'
display_name2 = u'テスト機(改)'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# Create a new sensor
print('## Create a new sensor')
response = requests.request(
"POST", url, headers=headers, data=json.dumps({
'serial_number': serial_number,
'display_name': display_name1
}))
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')
# Get a list of sensors
print('## Create a sensor list')
response = requests.request("GET", url, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')
# Get sensor details
print('## Create sensor details')
response = requests.request("GET", url + serial_number, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')
# Update sensor details
print('## Update the sensor details')
response = requests.request(
"PUT", url + serial_number, headers=headers, data=json.dumps({
'serial_number': serial_number,
'display_name': display_name2
}))
print(f'Status Code = {response.status_code}')
pprint.pprint(response.json())
print('')
# delete the sensor
print('## Delete the sensor')
response = requests.request("DELETE", url + serial_number, headers=headers)
print(f'Status Code = {response.status_code}')
pprint.pprint(response.text)
print('')
実行結果
$ python 001_crud_sensor.py
## Create a new sensor
Status Code = 201
{'created_at': '2021-07-22T13:58:22.741190+09:00',
'display_name': 'テスト機',
'serial_number': 'BMTL-123456',
'updated_at': '2021-07-22T13:58:22.741264+09:00'}
## Create a sensor list
Status Code = 200
[{'created_at': '2021-07-21T23:40:32.587705+09:00',
'display_name': '一号機',
'serial_number': 'ADX-123987',
'updated_at': '2021-07-21T23:40:32.587741+09:00'},
{'created_at': '2021-07-22T13:28:25.635808+09:00',
'display_name': '二号機(改)',
'serial_number': 'DMT-774321',
'updated_at': '2021-07-22T13:28:25.635885+09:00'},
{'created_at': '2021-07-22T13:32:41.579019+09:00',
'display_name': '二号機(改)',
'serial_number': 'DMT-774322',
'updated_at': '2021-07-22T13:32:41.579073+09:00'},
{'created_at': '2021-07-22T13:35:11.876769+09:00',
'display_name': '二号機(改)',
'serial_number': 'DMT-774323',
'updated_at': '2021-07-22T13:35:11.876807+09:00'},
{'created_at': '2021-07-22T13:58:22.741190+09:00',
'display_name': 'テスト機',
'serial_number': 'BMTL-123456',
'updated_at': '2021-07-22T13:58:22.741264+09:00'}]
## Create sensor details
Status Code = 200
{'created_at': '2021-07-22T13:58:22.741190+09:00',
'display_name': 'テスト機',
'serial_number': 'BMTL-123456',
'updated_at': '2021-07-22T13:58:22.741264+09:00'}
## Update the sensor details
Status Code = 200
{'created_at': '2021-07-22T13:58:22.741190+09:00',
'display_name': 'テスト機(改)',
'serial_number': 'BMTL-123456',
'updated_at': '2021-07-22T13:58:22.800639+09:00'}
## Delete the sensor
Status Code = 204
''
APIView
クラスを使うとこんな感じになる。
class SensorList(APIView):
def get(self, request, format=None):
sensor = Sensor.objects.all()
serializeer = SensorSerializer(sensor, many=True)
return Response(serializeer.data)
def post(self, request, format=None):
serializer = SensorSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class SensorDetail(APIView):
def get_object(self, pk):
try:
return Sensor.objects.get(pk=pk)
except Sensor.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
sensor = self.get_object(pk)
serializeer = SensorSerializer(sensor)
return Response(serializeer.data)
def put(self, request, pk, format=None):
sensor = self.get_object(pk)
serializer = SensorSerializer(sensor, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.erros, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
sensor = self.get_object(pk)
sensor.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Viewset を使うと、普通に CRUD するだけなら、たったこれだけのコードになってしまう。
class SensorViewSet(viewsets.ModelViewSet):
queryset = Sensor.objects.all()
serializer_class = SensorSerializer
queryset = Sensor.objects.all()
となっているが、detail の時も、PK を使わずに、DBからは全件データを取ってきてしまっているのだろうか?(さすがに、そんなことはしないと思うのだが…。)
→ 発行された SQL を確認したら、ちゃんと SELECT ••• FROM "monitoring_sensor" WHERE "monitoring_sensor"."serial_number" = '''BMTL-123456''' LIMIT 21
となっていた。
今度は /sensor/{pk}/data/
で、そのセンサーのデータを登録・取得したい。その場合は、 @action
を使うらしい。
class SensorViewSet(viewsets.ModelViewSet):
queryset = Sensor.objects.all()
serializer_class = SensorSerializer
@action(detail=True, methods=['GET', 'POST'])
def data(self, request, *args, **kwargs):
... (snip) ...
- メソッド :
request.method
- Body :
request.data
- PK :
kwargs['pk']
- パラメータ :
request.GET.get('val')
POST
でデータを投入する。
def post_data(payload, pk):
data = payload
data['sensor'] = pk
if 'timestamp' not in data:
data['timestamp'] = datetime.now().astimezone(tz=JST).isoformat()
serializer = SensingDataSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
... (snip) ...
class SensorViewSet(viewsets.ModelViewSet):
queryset = Sensor.objects.all()
serializer_class = SensorSerializer
@action(detail=True, methods=['GET', 'POST'])
def data(self, request, *args, **kwargs):
if request.method == 'GET':
pass
elif request.method == 'POST':
return post_data(request.data, kwargs['pk'])
return Response({}, status=status.HTTP_400_BAD_REQUEST)
GET
でセンサーデータを取得できるようにする。パラメータで範囲指定できるようにする。省略された場合は、現在時刻から1時間前までの範囲。
/sensor/{pk}/data/?from=<エポック秒>&to=<エポック秒>
コードはこんな感じになる。
def now():
return int(datetime.now(timezone.utc).strftime('%s'))
def epoch_to_iso_jst(epoch):
try:
v = int(epoch)
except Exception:
v = now()
return datetime.fromtimestamp(v).replace(tzinfo=timezone.utc).astimezone(tz=JST).isoformat()
def search_data(request, pk):
try:
sensor = Sensor.objects.get(pk=pk)
except Sensor.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
try:
from_dt = int(request.GET.get('from'))
except Exception:
from_dt = now() - 3600
finally:
from_dt = epoch_to_iso_jst(from_dt)
try:
to_dt = int(request.GET.get('to'))
except Exception:
to_dt = now()
finally:
to_dt = epoch_to_iso_jst(to_dt)
data = SensingData.objects.filter(
sensor=sensor, timestamp__gte=from_dt, timestamp__lte=to_dt)
serializer = SensingDataSerializer(data, many=True)
return Response(serializer.data)
... (snip) ...
@ action(detail=True, methods=['GET', 'POST'])
def data(self, request, *args, **kwargs):
if request.method == 'GET':
return search_data(request, kwargs['pk'])
センサーの一覧を Web 画面に表示する場合。
def sensor_list(request):
if request.method == 'GET':
sensor = Sensor.objects.all()
serializer = SensorSerializer(sensor, many=True)
print(serializer.data)
content = {'sensor_list': json.loads(json.dumps(serializer.data))}
return render(request, 'dashboard/sensor_list.html', content)
<div class="sample">{{ sensor_list }}</div>
単純に JSON が表示されるので、あとは装飾する。